"""Runtime settings store.

Settings can come from three places:
1. environment variables (deploy-time defaults)
2. a legacy JSON file (local/dev compatibility and migration source)
3. PostgreSQL when ``DATABASE_URL`` is configured

When PostgreSQL is available, settings are persisted there and any legacy JSON
settings are migrated on startup. The JSON file remains as a fallback for
non-database local runs.
"""

from __future__ import annotations

import json
import os
from contextlib import contextmanager
from pathlib import Path

try:
    import psycopg
    from psycopg.rows import dict_row
except ImportError:  # pragma: no cover - exercised via runtime status instead
    psycopg = None
    dict_row = None

from services import emby_tasks as emby_tasks_service

SETTINGS_FILE = Path(os.environ.get("SETTINGS_FILE", "cache/settings.json"))
DATABASE_URL = os.environ.get("DATABASE_URL", "").strip()

FIELD_SPECS = {
    "emby_url": "string",
    "emby_api_key": "string",
    "navidrome_url": "string",
    "navidrome_user": "string",
    "navidrome_password": "string",
    "audiobookshelf_url": "string",
    "audiobookshelf_token": "string",
    "music_root": "string",
    "homescreen_db_path": "string",
    "tmdb_api_key": "string",
    "deploy_nas_host": "string",
    "deploy_nas_user": "string",
    "deploy_nas_password": "string",
    "deploy_remote_app_dir": "string",
    "deploy_music_host_path": "string",
    "preroll_enabled": "bool",
    "preroll_active_dir": "string",
    "preroll_inactive_dir": "string",
    "preroll_state_file": "string",
    "preroll_weekday": "int",
    "preroll_time": "string",
    "emby_tasks": "object",
}

FIELD_DEFAULTS = {
    "preroll_enabled": False,
    "preroll_weekday": 0,
}

SCHEMA = """
CREATE TABLE IF NOT EXISTS app_settings (
    key TEXT PRIMARY KEY,
    value_json JSONB NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
"""


def _env(name: str, legacy_name: str | None = None, default: str = "") -> str:
    if os.environ.get(name) is not None:
        return os.environ[name]
    if legacy_name and os.environ.get(legacy_name) is not None:
        return os.environ[legacy_name]
    return default


def _coerce_value(field: str, value):
    kind = FIELD_SPECS[field]
    if kind == "bool":
        if isinstance(value, bool):
            return value
        return str(value).strip().lower() in {"1", "true", "yes", "on"}
    if kind == "int":
        try:
            return int(value)
        except (TypeError, ValueError):
            return FIELD_DEFAULTS.get(field, 0)
    if kind == "object":
        if field == "emby_tasks":
            return emby_tasks_service.normalize_settings(value)
        return value if isinstance(value, dict) else {}
    if value is None:
        return ""
    return str(value)


def using_postgres() -> bool:
    return bool(DATABASE_URL and psycopg is not None)


@contextmanager
def postgres_connect():
    if not using_postgres():
        raise RuntimeError("PostgreSQL settings store is not configured.")
    with psycopg.connect(DATABASE_URL, row_factory=dict_row) as conn:
        yield conn


def env_defaults() -> dict:
    return {
        "emby_url": os.environ.get("EMBY_URL", "http://10.0.0.2:8096"),
        "emby_api_key": os.environ.get("EMBY_API_KEY", ""),
        "navidrome_url": os.environ.get("NAVIDROME_URL", "http://10.0.0.2:4533"),
        "navidrome_user": os.environ.get("NAVIDROME_USER", ""),
        "navidrome_password": os.environ.get("NAVIDROME_PASSWORD", ""),
        "audiobookshelf_url": os.environ.get("AUDIOBOOKSHELF_URL", ""),
        "audiobookshelf_token": os.environ.get("AUDIOBOOKSHELF_TOKEN", ""),
        "music_root": os.environ.get("MUSIC_ROOT", r"\\Matt-htpc\d\Music"),
        "homescreen_db_path": os.environ.get("HOMESCREEN_DB_PATH", ""),
        "tmdb_api_key": os.environ.get("TMDB_API_KEY", ""),
        "deploy_nas_host": os.environ.get("DEPLOY_NAS_HOST", ""),
        "deploy_nas_user": os.environ.get("DEPLOY_NAS_USER", ""),
        "deploy_nas_password": os.environ.get("DEPLOY_NAS_PASSWORD", ""),
        "deploy_remote_app_dir": os.environ.get("DEPLOY_REMOTE_APP_DIR", "/share/Docker/homelabtoolkit"),
        "deploy_music_host_path": os.environ.get("DEPLOY_MUSIC_HOST_PATH", "/share/Music"),
        "preroll_enabled": _coerce_value("preroll_enabled", _env("PREROLL_ENABLED", default="false")),
        "preroll_active_dir": _env("PREROLL_ACTIVE_DIR", "ACTIVE_DIR", "/media/Prerolls"),
        "preroll_inactive_dir": _env("PREROLL_INACTIVE_DIR", "INACTIVE_DIR", "/media/Prerolls - Not Active"),
        "preroll_state_file": _env("PREROLL_STATE_FILE", "STATE_FILE", "cache/preroll-state.json"),
        "preroll_weekday": _coerce_value("preroll_weekday", _env("PREROLL_WEEKDAY", "ROTATE_WEEKDAY", "0")),
        "preroll_time": _env("PREROLL_TIME", "SCHEDULE_TIME", "02:00"),
        "emby_tasks": emby_tasks_service.default_settings(),
    }


def _normalize_payload(data: dict) -> dict:
    return {k: _coerce_value(k, v) for k, v in data.items() if k in FIELD_SPECS and v is not None}


def _read_file() -> dict:
    if not SETTINGS_FILE.exists():
        return {}
    try:
        data = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
    except (OSError, ValueError):
        return {}
    return _normalize_payload(data if isinstance(data, dict) else {})


def _write_file(values: dict) -> None:
    SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
    SETTINGS_FILE.write_text(json.dumps(values, indent=2), encoding="utf-8")


def _ensure_postgres_schema() -> None:
    with postgres_connect() as conn:
        with conn.cursor() as cur:
            cur.execute(SCHEMA)
        conn.commit()


def _read_postgres() -> dict:
    with postgres_connect() as conn:
        with conn.cursor() as cur:
            cur.execute("SELECT key, value_json FROM app_settings")
            rows = cur.fetchall()
    data: dict[str, object] = {}
    for row in rows:
        data[row["key"]] = row["value_json"]
    return _normalize_payload(data)


def _write_postgres(values: dict) -> None:
    payload = _normalize_payload(values)
    with postgres_connect() as conn:
        with conn.cursor() as cur:
            for key, value in payload.items():
                cur.execute(
                    """
                    INSERT INTO app_settings (key, value_json, updated_at)
                    VALUES (%s, %s::jsonb, NOW())
                    ON CONFLICT (key)
                    DO UPDATE SET value_json = EXCLUDED.value_json, updated_at = NOW()
                    """,
                    (key, json.dumps(value)),
                )
        conn.commit()


def _migrate_file_to_postgres() -> None:
    file_values = _read_file()
    if not file_values:
        return
    current_db = _read_postgres()
    merged = dict(current_db)
    for key, value in file_values.items():
        if key not in merged or merged[key] in ("", {}, 0, False):
            merged[key] = value
    _write_postgres(merged)


def init_store() -> None:
    if using_postgres():
        _ensure_postgres_schema()
        _migrate_file_to_postgres()


def load() -> dict:
    """Effective settings: env defaults overlaid with persisted overrides."""
    values = env_defaults()
    if using_postgres():
        values.update(_read_postgres())
    else:
        values.update(_read_file())
    return values


def save(updates: dict) -> dict:
    """Persist the editable subset of ``updates`` and return effective settings."""
    current = _read_postgres() if using_postgres() else _read_file()
    for key in FIELD_SPECS:
        if key in updates and updates[key] is not None:
            value = updates[key]
            if FIELD_SPECS[key] == "string":
                current[key] = str(value).strip()
            else:
                current[key] = _coerce_value(key, value)
    if using_postgres():
        _ensure_postgres_schema()
        _write_postgres(current)
    else:
        _write_file(current)
    return load()
