"""
database.py — SQLite-хранилище (расписания, квоты, белый список) + файловые хелперы.
Можно менять схему или добавлять методы — остальной бот не сломается.
"""

import json
import shutil
import sqlite3
from dataclasses import asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional, Tuple, List

from config import (
    USERS_DIR,
    DB_PATH,
    OFFICIAL_TDESKTOP_API_ID,
    OFFICIAL_TDESKTOP_APP_HASH,
    OFFICIAL_TDESKTOP_LANG_PACK,
)
from bot_ui.logger_setup import logger
from models import DeviceInfo, OutputJSON, ProxyResolved
from core.client_builder import get_api_credentials


# ── Filesystem helpers ────────────────────────────────────────────────────────


def ensure_owner_root(owner_id: int) -> Path:
    base = USERS_DIR / str(owner_id)
    (base / "temp").mkdir(parents=True, exist_ok=True)
    return base


def ensure_user_folder(
    owner_id: int, username: Optional[str], phone_digits: str
) -> Path:
    name = f"u{phone_digits}"
    base = USERS_DIR / str(owner_id) / name
    (base / "sessions").mkdir(parents=True, exist_ok=True)
    (base / "queues").mkdir(parents=True, exist_ok=True)
    (base / "spam").mkdir(parents=True, exist_ok=True)
    return base


def ensure_layout(
    owner_id: int, username: Optional[str], phone_digits: str
) -> Tuple[Path, Path, Path]:
    base = ensure_user_folder(owner_id, username, phone_digits) / "sessions"
    return base, base / f"{phone_digits}.session", base / f"{phone_digits}.json"


def move_session_file(src: Path, dst: Path):
    dst.parent.mkdir(parents=True, exist_ok=True)
    if src.resolve() == dst.resolve():
        return
    try:
        shutil.move(str(src), str(dst))
        logger.info("SESSION перемещён: %s -> %s", src, dst)
    except Exception as e:
        try:
            shutil.copy2(str(src), str(dst))
            logger.warning(
                "Не удалось переместить SESSION (%s). Создал копию: %s", e, dst
            )
        except Exception as e2:
            logger.warning(
                "Не удалось переместить/скопировать SESSION (%s). Оставляю как есть: %s",
                e2,
                src,
            )


def build_output_json(
    me,
    phone_digits: str,
    dev: DeviceInfo,
    proxy_json_vector: Optional[list],
    twofa: Optional[str],
) -> dict:
    api_id, api_hash = get_api_credentials()
    now = datetime.now(timezone.utc)
    now_iso = now.isoformat()
    return asdict(
        OutputJSON(
            app_id=api_id,
            app_hash=api_hash,
            sdk=dev.system_version,
            device=dev.device_model,
            app_version=dev.app_version,
            lang_pack=OFFICIAL_TDESKTOP_LANG_PACK,
            system_lang_pack=dev.system_lang_code,
            twoFA=bool(twofa) and "***" or None,
            role=None,
            id=int(me.id),
            phone=phone_digits,
            username=getattr(me, "username", None),
            date_of_birth=None,
            date_of_birth_integrity=None,
            is_premium=getattr(me, "premium", None),
            premium_expiry=None,
            first_name=getattr(me, "first_name", None),
            last_name=getattr(me, "last_name", None),
            has_profile_pic=bool(getattr(me, "photo", None)),
            spamblock=None,
            spamblock_end_date=None,
            session_file=phone_digits,
            stats_spam_count=0,
            stats_invites_count=0,
            last_connect_date=now_iso,
            session_created_date=now_iso,
            app_config_hash=None,
            user_id=int(me.id),
            device_token=None,
            device_token_secret=None,
            device_secret=None,
            signature=None,
            certificate=None,
            safetynet=None,
            perf_cat=2,
            tz_offset=0,
            register_time=None,
            last_check_time=None,
            avatar="img/default.png",
            sex=None,
            proxy=proxy_json_vector,
            ipv6=False,
            time=now.strftime("%H:%M:%S(%d.%m.%y)"),
            work=0,
        )
    )


# ── Store ─────────────────────────────────────────────────────────────────────


class Store:
    """SQLite-хранилище: белый список, квоты, расписания, admin-значения."""

    def __init__(self, path: Path):
        import threading
        path.parent.mkdir(parents=True, exist_ok=True)
        self._lock = threading.RLock()
        self.conn = sqlite3.connect(str(path), timeout=10, check_same_thread=False)
        try:
            self.conn.execute("PRAGMA journal_mode=WAL")
            self.conn.execute("PRAGMA busy_timeout=5000")
            self.conn.execute("PRAGMA synchronous=NORMAL")
        except Exception:
            pass
        self._init()

    def close(self):
        """Закрываем соединение с БД."""
        try:
            with self._lock:
                self.conn.close()
        except Exception:
            pass

    def _execute(self, sql, params=()):
        """Thread-safe execute."""
        with self._lock:
            return self.conn.execute(sql, params)

    def _execute_commit(self, sql, params=()):
        """Thread-safe execute + commit."""
        with self._lock:
            self.conn.execute(sql, params)
            self.conn.commit()

    def _init(self):
        c = self.conn.cursor()
        try:
            c.execute(
                """CREATE TABLE IF NOT EXISTS admin (
                key TEXT PRIMARY KEY,
                value TEXT
            )"""
            )
            c.execute(
                """CREATE TABLE IF NOT EXISTS whitelist (
                user_id INTEGER PRIMARY KEY
            )"""
            )
            c.execute(
                """CREATE TABLE IF NOT EXISTS quota_events (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                ts INTEGER NOT NULL,
                user_id INTEGER NOT NULL,
                phone TEXT,
                kind TEXT NOT NULL
            )"""
            )
            c.execute(
                """CREATE TABLE IF NOT EXISTS schedules (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                phone TEXT,
                kind TEXT NOT NULL,
                payload TEXT NOT NULL,
                run_at INTEGER NOT NULL,
                status TEXT NOT NULL DEFAULT 'pending',
                repeat_sec INTEGER DEFAULT 0,
                phones TEXT DEFAULT ''
            )"""
            )
            self.conn.commit()
        finally:
            c.close()

    def get_admin_value(self, key: str, default: Optional[str] = None) -> Optional[str]:
        with self._lock:
            cur = self.conn.execute("SELECT value FROM admin WHERE key=?", (key,))
            row = cur.fetchone()
            return row[0] if row else default

    def set_admin_value(self, key: str, value: str):
        self._execute_commit(
            "INSERT INTO admin(key,value) VALUES(?,?) "
            "ON CONFLICT(key) DO UPDATE SET value=excluded.value",
            (key, value),
        )

    def add_whitelist(self, user_id: int):
        self._execute_commit(
            "INSERT OR IGNORE INTO whitelist(user_id) VALUES(?)", (user_id,)
        )

    def del_whitelist(self, user_id: int):
        self._execute_commit("DELETE FROM whitelist WHERE user_id=?", (user_id,))

    def is_whitelisted(self, user_id: int) -> bool:
        with self._lock:
            cur = self.conn.execute("SELECT 1 FROM whitelist WHERE user_id=?", (user_id,))
            return cur.fetchone() is not None

    def push_quota_event(self, user_id: int, phone: str, kind: str):
        self._execute_commit(
            "INSERT INTO quota_events(ts,user_id,phone,kind) VALUES(?,?,?,?)",
            (int(datetime.now(timezone.utc).timestamp()), user_id, phone, kind),
        )

    def count_in_window(self, user_id: int, phone: str, kind: str, seconds: int) -> int:
        since = int(datetime.now(timezone.utc).timestamp()) - seconds
        with self._lock:
            cur = self.conn.execute(
                """SELECT COUNT(*) FROM quota_events
                   WHERE user_id=? AND kind=? AND ts>=? AND (phone=? OR phone IS NULL)""",
                (user_id, kind, since, phone),
            )
            return int(cur.fetchone()[0])

    def add_schedule(
        self, user_id: int, phone: str, kind: str, payload: dict, run_at: datetime
    ):
        self._execute_commit(
            """INSERT INTO schedules(user_id,phone,kind,payload,run_at,status)
               VALUES (?,?,?,?,?, 'pending')""",
            (
                user_id,
                phone,
                kind,
                json.dumps(payload, ensure_ascii=False),
                int(run_at.timestamp()),
            ),
        )

    def add_schedule_v2(
        self,
        user_id: int,
        phone,
        kind: str,
        payload: dict,
        run_at,
        repeat_sec: int = 0,
        phones=None,
    ):
        with self._lock:
            try:
                self.conn.execute(
                    """INSERT INTO schedules(user_id,phone,kind,payload,run_at,status,repeat_sec,phones)
                       VALUES (?,?,?,?,?,'pending',?,?)""",
                    (
                        user_id,
                        phone,
                        kind,
                        json.dumps(payload, ensure_ascii=False),
                        int(run_at.timestamp()),
                        int(repeat_sec),
                        phones,
                    ),
                )
                self.conn.commit()
            except Exception as e:
                logger.debug("add_schedule_v2 fallback (missing columns): %s", e)
                self.conn.execute(
                    """INSERT INTO schedules(user_id,phone,kind,payload,run_at,status)
                       VALUES (?,?,?,?,?,'pending')""",
                    (
                        user_id,
                        phone,
                        kind,
                        json.dumps(payload, ensure_ascii=False),
                        int(run_at.timestamp()),
                    ),
                )
                self.conn.commit()

    def pop_due_schedules(self) -> List[tuple]:
        now = int(datetime.now(timezone.utc).timestamp())
        with self._lock:
            cur = self.conn.execute(
                """SELECT id,user_id,phone,kind,payload FROM schedules
                   WHERE status='pending' AND run_at<=? ORDER BY run_at ASC""",
                (now,),
            )
            rows = cur.fetchall()
            ids = [r[0] for r in rows]
            if ids:
                self.conn.execute(
                    f"UPDATE schedules SET status='running' WHERE id IN ({','.join('?' * len(ids))})",
                    ids,
                )
                self.conn.commit()
        return rows

    def finish_schedule(self, sched_id: int, status: str):
        self._execute_commit(
            "UPDATE schedules SET status=? WHERE id=?", (status, sched_id)
        )

    def list_schedules(self, user_id: int):
        with self._lock:
            try:
                cur = self.conn.execute(
                    """SELECT id, phone, kind, payload, run_at, status,
                              COALESCE(repeat_sec,0), COALESCE(phones,'')
                       FROM schedules
                       WHERE user_id=? AND status IN ('pending','running')
                       ORDER BY run_at ASC""",
                    (user_id,),
                )
                return cur.fetchall()
            except Exception:
                cur = self.conn.execute(
                    "SELECT id,phone,kind,payload,run_at,status,0,'' FROM schedules "
                    "WHERE user_id=? AND status IN ('pending','running') ORDER BY run_at ASC",
                    (user_id,),
                )
                return cur.fetchall()

    def cancel_schedule(self, sched_id: int, user_id: int):
        self._execute_commit(
            "UPDATE schedules SET status='cancelled' WHERE id=? AND user_id=?",
            (sched_id, user_id),
        )


# Синглтон хранилища
store = Store(DB_PATH)


# ── Quota & Roles helpers ─────────────────────────────────────────────────────


def get_default_quota_limits() -> Tuple[int, int]:
    # Дефолты задраны вверх (12/60 → 300/3000): прежние значения остались от
    # бот-режима где один бот обслуживал десятки юзеров и аккаунт-донор надо
    # было беречь. В standalone-десктопе квоту по часу/суткам Telegram уже
    # сам соблюдает через FloodWait/PeerFlood — внутренний лимит только мешал
    # клиенту докатить список из 50-100 ссылок за один присест.
    per_hour = int(store.get_admin_value("quota_per_hour", "300"))
    per_day = int(store.get_admin_value("quota_per_day", "3000"))
    # Миграция: у клиентов со старым 12/60 значение лежит в admin-таблице и не
    # сбросится сменой дефолта. Если видим легаси-цифры — поднимаем разово до
    # новых дефолтов, чтобы апдейт сразу подхватился без копаний в БД руками.
    if per_hour <= 12:
        per_hour = 300
        try: store.set_admin_value("quota_per_hour", str(per_hour))
        except Exception: pass
    if per_day <= 60:
        per_day = 3000
        try: store.set_admin_value("quota_per_day", str(per_day))
        except Exception: pass
    return per_hour, per_day


def is_admin(user_id: int) -> bool:
    try:
        # Всегда проверяем ADMIN_IDS из .env — эти пользователи всегда admin
        import re as _re
        from config import ADMIN_IDS_RAW

        for s in _re.split(r"[,\s]+", (ADMIN_IDS_RAW or "").strip()):
            if s.isdigit() and int(s) == user_id:
                return True
        # Дополнительно — пользователи из whitelist тоже admin
        return store.is_whitelisted(user_id)
    except Exception:
        return False


def is_allowed_by_quota(user_id: int, phone: str, kind: str) -> Tuple[bool, str]:
    per_hour, per_day = get_default_quota_limits()
    hour_used = store.count_in_window(user_id, phone, kind, 3600)
    day_used = store.count_in_window(user_id, phone, kind, 86400)
    if hour_used >= per_hour:
        return False, f"Превышен лимит за час ({hour_used}/{per_hour})."
    if day_used >= per_day:
        return False, f"Превышен лимит за сутки ({day_used}/{per_day})."
    return True, ""


def build_info_text(uid: int) -> str:
    per_hour, per_day = get_default_quota_limits()
    pub = store.get_admin_value("public_mode", "0") == "1"
    admin = is_admin(uid)
    lines = [
        "**ℹ️ О боте**",
        "Бот для управления Telegram-аккаунтами: автоподписки по ссылкам, рассылка по чатам, расписание задач и мульти-аккинг.",
        "",
        "**📌 Как начать:**",
        "1. Нажми **➕ Подключить аккаунт** — войди через QR или код.",
        "2. Выбери активный аккаунт через **🔁 Мои аккаунты**.",
        "3. Загрузи список ссылок: **📂 Загрузить ссылки**.",
        "4. Запусти подписку или рассылку — кнопки появятся в меню.",
        "",
        "**🗂 Разделы меню:**",
        "• ➕ **Подключить** — вход через QR-код или номер телефона.",
        "• 🔁 **Аккаунты** — список, выбор и удаление аккаунтов.",
        "• 👤 **Профиль** — инфо об активном аккаунте.",
        "• 📂 / 📄 **Загрузить** — список ссылок (текст или .txt файл).",
        "• 🔍 **Проверить** — проверка ссылок без вступления (dry-run).",
        "• ▶️ **Продолжить** — возобновление последней задачи.",
        "• ✉️ **Рассылка** — отправка сообщений по чатам.",
        "• ⏱️ **Расписание** — запуск задач по времени.",
        "• ⚙️ **Настройки** — паузы, лимиты, защита аккаунта.",
        "• 🌀 **Multi-Аккинг** — работа с несколькими аккаунтами сразу.",
        "",
        f"**📊 Квоты:** {per_hour} вступлений/час, {per_day}/сутки.",
        f"**🌐 Режим доступа:** {'публичный (открыт для всех)' if pub else 'закрытый (только по списку)'}.",
        "**⌨️ Команды:** /start, /инфа (или /info)."
        + (" /admin — только для администраторов." if admin else ""),
    ]
    if admin:
        lines += [
            "",
            "**🛡️ Вы администратор.** Управляйте через /admin или кнопку «Админ-панель».",
        ]
    return "\n".join(lines)


async def autoload_existing_accounts_for(user_id: int):
    from bot_ui.ui import get_ctx
    from core.client_builder import build_client, load_device, load_proxy
    import state
    from models import Account

    if state.dev is None:
        state.dev = load_device()
    if state.proxy_res is None:
        state.proxy_res = load_proxy()

    ctx = get_ctx(user_id)
    user_root = USERS_DIR / str(user_id)

    if not user_root.exists():
        return

    for json_path in user_root.glob("u*/sessions/*.json"):
        try:
            phone = json_path.stem
            if phone in ctx.accounts:
                continue

            with open(json_path, "r", encoding="utf-8") as f:
                data = json.load(f)

            session_path = json_path.with_suffix(".session")
            if not session_path.exists():
                continue

            # Preserve device from client's JSON, generate only if missing
            if data.get("device") or data.get("sdk"):
                dev = DeviceInfo(
                    device_model=data.get("device", "PC 64bit"),
                    system_version=data.get("sdk", "Windows 10"),
                    app_version=data.get("app_version", "5.8.3 x64"),
                    lang_code=data.get("lang_pack", "en"),
                    system_lang_code=data.get("system_lang_pack", "en-US"),
                )
            else:
                dev = load_device(phone)
            cli = await build_client(session_path, dev, state.proxy_res, phone=phone)

            acc = Account(
                phone=phone,
                username=data.get("username"),
                session_path=session_path,
                json_path=json_path,
                client=cli,
                dev=dev,
                proxy=state.proxy_res,
            )

            ctx.accounts[phone] = acc
            if not ctx.active:
                ctx.active = phone
            logger.info(f"✅ Аккаунт {phone} подгружен.")
        except Exception as e:
            logger.error(f"❌ Ошибка автозагрузки: {e}")
