"""Persistent TUI settings stored in ~/.navi_code/tui.json."""

from __future__ import annotations

import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any

from clients.terminal.config import settings


@dataclass
class TuiSettings:
    """User-facing TUI configuration persisted across restarts."""

    theme: str = "gnexus-dark"
    mouse: bool = True
    scroll_speed: int = 1
    diff_style: str = "unified"
    # Maximum number of chat items rendered into the conversation panel. Older
    # items stay in the in-memory model (non-destructive) but are not laid out,
    # so per-token refresh cost is bounded regardless of history length. The
    # render cache makes even the visible window cheap to rebuild.
    max_visible_items: int = 200
    keybinds: dict[str, str] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "TuiSettings":
        field_defaults = {f.name: f.default for f in cls.__dataclass_fields__.values()}
        merged = dict(field_defaults)
        merged.update(data)
        keybinds = merged.get("keybinds", {})
        if keybinds is None or not isinstance(keybinds, dict):
            keybinds = {}
        return cls(
            theme=_coerce_str(merged.get("theme"), field_defaults["theme"]),
            mouse=_coerce_bool(merged.get("mouse"), field_defaults["mouse"]),
            scroll_speed=_coerce_int(merged.get("scroll_speed"), field_defaults["scroll_speed"]),
            diff_style=_coerce_str(merged.get("diff_style"), field_defaults["diff_style"]),
            max_visible_items=_coerce_int_bounded(
                merged.get("max_visible_items"),
                field_defaults["max_visible_items"],
                minimum=10,
            ),
            keybinds=dict(keybinds),
        )

    @property
    def settings_dir(self) -> Path:
        return settings.state_dir

    @property
    def settings_file(self) -> Path:
        return self.settings_dir / "tui.json"

    def load(self) -> "TuiSettings":
        if not self.settings_file.exists():
            return self._save_and_return()
        try:
            with self.settings_file.open("r", encoding="utf-8") as f:
                data = json.load(f)
        except (json.JSONDecodeError, OSError):
            return self._save_and_return()
        loaded = self.from_dict(data)
        loaded._ensure_dir()
        return loaded

    def save(self) -> None:
        self._ensure_dir()
        with self.settings_file.open("w", encoding="utf-8") as f:
            json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)

    def _ensure_dir(self) -> None:
        self.settings_dir.mkdir(parents=True, exist_ok=True)

    def _save_and_return(self) -> "TuiSettings":
        self.save()
        return self


# Global lazy-loaded instance.
_tui_settings: TuiSettings | None = None


def get_tui_settings() -> TuiSettings:
    """Return the loaded TUI settings, caching them for the process lifetime."""
    global _tui_settings
    if _tui_settings is None:
        _tui_settings = TuiSettings().load()
    return _tui_settings


def reload_tui_settings() -> TuiSettings:
    """Force reload from disk and return fresh settings."""
    global _tui_settings
    _tui_settings = TuiSettings().load()
    return _tui_settings


def _coerce_bool(value: Any, default: bool) -> bool:
    """Return a boolean from bool or common string representations."""
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        lowered = value.strip().lower()
        if lowered in ("true", "yes", "1", "on"):
            return True
        if lowered in ("false", "no", "0", "off"):
            return False
    return default


def _coerce_int(value: Any, default: int) -> int:
    """Return an int from int or numeric string, falling back to default."""
    if isinstance(value, int) and not isinstance(value, bool):
        return value
    if isinstance(value, str):
        try:
            return int(value)
        except ValueError:
            pass
    return default


def _coerce_int_bounded(value: Any, default: int, minimum: int) -> int:
    """Like _coerce_int but clamps the result to at least ``minimum``.

    A stored value below the floor (or a non-numeric one) falls back to
    ``default`` so a bad config can never silently shrink the window to
    something unusable.
    """
    coerced = _coerce_int(value, default)
    if coerced < minimum:
        return default
    return coerced


def _coerce_str(value: Any, default: str) -> str:
    """Return a string value if it is a non-empty string, else default."""
    if isinstance(value, str) and value:
        return value
    return default
