from __future__ import annotations

import json
import time
from pathlib import Path
from urllib.parse import quote

from loguru import logger

import flow_xhs.runtime.config as runtime_config


def _data_root() -> Path:
    root = Path(runtime_config.APP_DATA_DIR)
    root.mkdir(parents=True, exist_ok=True)
    return root


def _accounts_root() -> Path:
    root = _data_root() / "accounts"
    root.mkdir(parents=True, exist_ok=True)
    return root


def _settings_file() -> Path:
    return _data_root() / "settings.json"


def _task_schedules_file() -> Path:
    return _data_root() / "task_schedules.json"


def _meta_file() -> Path:
    return _accounts_root() / ".meta.json"


def _safe_user_no(user_no: str) -> str:
    return quote(str(user_no), safe="")


def _account_file(platform: str, user_no: str) -> Path:
    platform_dir = _accounts_root() / str(platform)
    platform_dir.mkdir(parents=True, exist_ok=True)
    return platform_dir / f"{_safe_user_no(user_no)}.json"


def _load_json(path: Path, default):
    if not path.exists():
        return default
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return default


def _write_json_atomic(path: Path, payload) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    tmp.replace(path)


def _iter_account_files() -> list[Path]:
    root = _accounts_root()
    return [p for p in root.rglob("*.json") if p.name != ".meta.json"]


def _next_account_id() -> int:
    meta_path = _meta_file()
    meta = _load_json(meta_path, {"next_id": 1})
    next_id = int(meta.get("next_id", 1))
    meta["next_id"] = next_id + 1
    _write_json_atomic(meta_path, meta)
    return next_id


def _normalize_storage_state(value):
    if isinstance(value, str) and value:
        try:
            return json.loads(value)
        except Exception:
            return None
    if isinstance(value, dict):
        return value
    return None


def _normalize_account_record(raw: dict) -> dict:
    record = dict(raw)
    record["storage_state"] = _normalize_storage_state(record.get("storage_state"))
    if "is_logged_in" in record:
        record["is_logged_in"] = bool(record.get("is_logged_in"))
    return record


def _ensure_file_store_layout() -> None:
    _accounts_root()
    for file_path, default in [
        (_settings_file(), {}),
        (_task_schedules_file(), {}),
        (_meta_file(), {"next_id": 1}),
    ]:
        if not file_path.exists():
            _write_json_atomic(file_path, default)


def _has_existing_file_data() -> bool:
    if _iter_account_files():
        return True
    if _load_json(_settings_file(), {}):
        return True
    return False


def _migrate_from_legacy_sqlite_once() -> None:
    marker = _data_root() / ".migrated_from_sqlite"
    if marker.exists():
        return

    if _has_existing_file_data():
        marker.write_text(str(int(time.time())), encoding="utf-8")
        return

    db_file = Path(runtime_config.DB_FILE)
    if not db_file.exists():
        marker.write_text(str(int(time.time())), encoding="utf-8")
        return

    try:
        import sqlite3

        conn = sqlite3.connect(db_file)
        conn.row_factory = sqlite3.Row
        cursor = conn.cursor()

        cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'")
        has_accounts = cursor.fetchone() is not None
        accounts_count = 0
        if has_accounts:
            cursor.execute("SELECT * FROM accounts")
            rows = cursor.fetchall()
            for row in rows:
                account = _normalize_account_record(dict(row))
                platform = account.get("platform")
                user_no = account.get("user_no")
                if not platform or not user_no:
                    continue
                target = _account_file(platform, user_no)
                _write_json_atomic(target, account)
                accounts_count += 1

        cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='settings'")
        has_settings = cursor.fetchone() is not None
        settings_count = 0
        if has_settings:
            cursor.execute("SELECT key, value FROM settings")
            settings_rows = cursor.fetchall()
            settings = {str(r["key"]): r["value"] for r in settings_rows}
            _write_json_atomic(_settings_file(), settings)
            settings_count = len(settings)

        conn.close()
        marker.write_text(
            json.dumps(
                {
                    "migrated_at": int(time.time()),
                    "accounts_count": accounts_count,
                    "settings_count": settings_count,
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )
        logger.info(
            "Migrated legacy sqlite data to file store: accounts={} settings={}",
            accounts_count,
            settings_count,
        )
    except Exception as exc:
        logger.warning("Skipped sqlite migration: {}", exc)
        marker.write_text(str(int(time.time())), encoding="utf-8")


def init_db():
    """Initialize file-based store and migrate legacy sqlite once if needed."""
    _ensure_file_store_layout()
    _migrate_from_legacy_sqlite_once()


class DatabaseManager:
    """File-backed storage manager (source of truth)."""

    @staticmethod
    def upsert_account(account_data: dict):
        now = int(time.time())
        platform = str(account_data.get("platform") or "").strip()
        user_no = str(account_data.get("user_no") or "").strip()
        if not platform or not user_no:
            raise ValueError("platform and user_no are required")

        path = _account_file(platform, user_no)
        existing = _load_json(path, {}) if path.exists() else {}

        payload = {
            "id": int(existing.get("id") or _next_account_id()),
            "platform": platform,
            "user_no": user_no,
            "nickname": account_data.get("nickname"),
            "is_logged_in": bool(account_data.get("is_logged_in")),
            "check_time": account_data.get("check_time"),
            "usage_time": account_data.get("usage_time", existing.get("usage_time")),
            "storage_state": _normalize_storage_state(account_data.get("storage_state")),
            "creater": existing.get("creater", "admin"),
            "created": int(existing.get("created") or now),
            "modifier": "admin",
            "modified": now,
        }
        _write_json_atomic(path, payload)

    @staticmethod
    def get_all_accounts(only_active=False):
        records = []
        for path in _iter_account_files():
            data = _load_json(path, {})
            if not data:
                continue
            record = _normalize_account_record(data)
            if only_active and not bool(record.get("is_logged_in")):
                continue
            records.append(record)
        records.sort(key=lambda x: int(x.get("modified") or 0), reverse=True)
        return records

    @staticmethod
    def delete_account(platform, user_no):
        path = _account_file(str(platform), str(user_no))
        if path.exists():
            path.unlink()

    @staticmethod
    def get_setting(key: str, default=None):
        settings = _load_json(_settings_file(), {})
        return settings.get(key, default)

    @staticmethod
    def set_setting(key: str, value: str):
        settings = _load_json(_settings_file(), {})
        settings[str(key)] = value
        _write_json_atomic(_settings_file(), settings)

    @staticmethod
    def get_task_schedule(platform: str) -> dict:
        schedules = _load_json(_task_schedules_file(), {})
        schedule = schedules.get(platform)
        if schedule:
            return schedule
        return {
            "platform": platform,
            "task_type": "login_check",
            "cron_expression": "",
            "jitter_seconds": 0,
            "alert_enabled": 0,
            "alert_email": "",
            "is_active": 0,
        }

    @staticmethod
    def upsert_task_schedule(data: dict):
        schedules = _load_json(_task_schedules_file(), {})
        platform = str(data.get("platform") or "").strip()
        if not platform:
            raise ValueError("platform is required")
        now = int(time.time())
        existing = schedules.get(platform, {})
        schedules[platform] = {
            "platform": platform,
            "task_type": data.get("task_type", "login_check"),
            "cron_expression": data.get("cron_expression"),
            "jitter_seconds": data.get("jitter_seconds", 0),
            "next_run_time": data.get("next_run_time"),
            "alert_enabled": data.get("alert_enabled", 0),
            "alert_email": data.get("alert_email", ""),
            "is_active": data.get("is_active", 1),
            "created": int(existing.get("created") or now),
            "modified": now,
        }
        _write_json_atomic(_task_schedules_file(), schedules)

    @staticmethod
    def get_all_task_schedules():
        schedules = _load_json(_task_schedules_file(), {})
        return list(schedules.values())

    @staticmethod
    def delete_task_schedule(platform: str):
        schedules = _load_json(_task_schedules_file(), {})
        schedules.pop(platform, None)
        _write_json_atomic(_task_schedules_file(), schedules)

    @staticmethod
    def update_next_run_time(platform: str, next_run: int):
        schedules = _load_json(_task_schedules_file(), {})
        item = schedules.get(platform)
        if not item:
            return
        item["next_run_time"] = next_run
        item["modified"] = int(time.time())
        schedules[platform] = item
        _write_json_atomic(_task_schedules_file(), schedules)

    @staticmethod
    def set_account_login_status(platform: str, user_no: str, is_logged_in: bool) -> bool:
        path = _account_file(platform, user_no)
        data = _load_json(path, {})
        if not data:
            return False
        data["is_logged_in"] = bool(is_logged_in)
        data["modified"] = int(time.time())
        _write_json_atomic(path, data)
        return True

    @staticmethod
    def update_account_status(account_id: int, is_logged_in: bool, check_time: int, storage_state: dict = None):
        for path in _iter_account_files():
            data = _load_json(path, {})
            if int(data.get("id") or 0) != int(account_id):
                continue
            data["is_logged_in"] = bool(is_logged_in)
            data["check_time"] = check_time
            if storage_state is not None:
                data["storage_state"] = _normalize_storage_state(storage_state)
            data["modified"] = int(time.time())
            _write_json_atomic(path, data)
            return
