"""Utility functions for the web package.

Config-dependent helpers reference ``otto.config`` through the module object
so they survive ``importlib.reload()`` in tests.
"""

import json
from html import escape
from pathlib import Path
from typing import Any

import otto.config as _cfg

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_MASKED_TOKEN = "********"
_LOG_LEVEL_OPTIONS = ("debug", "info", "warning", "error", "critical")
_BOOTSTRAP_OPTIONS = ("first_user", "disabled")

# ---------------------------------------------------------------------------
# String / parsing helpers
# ---------------------------------------------------------------------------


def clean_str(value: Any) -> str:
    if value is None:
        return ""
    return str(value).strip()


def normalize_workspace_mode(value: Any) -> str:
    if isinstance(value, str):
        m = value.strip().lower()
        if m in {"default", "strict"}:
            return m
    return "default"


def coerce_path(value: Any) -> Path | None:
    if isinstance(value, Path):
        p = value.expanduser()
    elif isinstance(value, str) and value.strip():
        p = Path(value.strip()).expanduser()
    else:
        return None
    if not p.is_absolute():
        p = Path.cwd() / p
    return p


def workspace_status(config: Any) -> tuple[str, Path]:
    workspace = getattr(config, "workspace", None)
    mode = normalize_workspace_mode(getattr(workspace, "mode", "default"))
    fallback = coerce_path(getattr(config, "workdir", None)) or Path.cwd()
    root = coerce_path(getattr(workspace, "root", None)) or fallback
    return mode, root


def parse_optional_int(value: str, *, field_name: str) -> int | None:
    if value == "":
        return None
    try:
        return int(value)
    except ValueError as exc:
        raise ValueError(f"{field_name} must be an integer.") from exc


def parse_allowed_users(value: str) -> list[int]:
    if value == "":
        return []
    parsed: list[int] = []
    for item in value.split(","):
        text = item.strip()
        if not text:
            continue
        try:
            parsed.append(int(text))
        except ValueError as exc:
            raise ValueError("allowed_users must be a comma-separated list of integers.") from exc
    return parsed


def render_options(options: tuple[str, ...], selected: str) -> str:
    rendered: list[str] = []
    if selected and selected not in options:
        esc = escape(selected, quote=True)
        rendered.append(f'<option value="{esc}" selected>{escape(selected)}</option>')
    for opt in options:
        esc = escape(opt, quote=True)
        sel = " selected" if opt == selected else ""
        rendered.append(f'<option value="{esc}"{sel}>{escape(opt)}</option>')
    return "".join(rendered)


# ---------------------------------------------------------------------------
# Auth callback helpers (use _cfg.OTTO_HOME for reload-safety)
# ---------------------------------------------------------------------------


def auth_callback_file(provider: str) -> Path:
    return _cfg.OTTO_HOME / "config" / f"auth_callback_{provider}.json"


def load_expected_state(provider: str) -> str | None:
    cb = auth_callback_file(provider)
    if not cb.exists():
        return None
    try:
        payload = json.loads(cb.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return None
    if not isinstance(payload, dict):
        return None
    es = payload.get("expected_state")
    if isinstance(es, str) and es.strip():
        return es.strip()
    if "code" in payload:
        return None
    st = payload.get("state")
    if isinstance(st, str) and st.strip():
        return st.strip()
    return None


def write_auth_callback(*, provider: str, code: str, state: str) -> None:
    cb = auth_callback_file(provider)
    cb.parent.mkdir(parents=True, exist_ok=True)
    cb.write_text(json.dumps({"code": code, "state": state}), encoding="utf-8")


# ---------------------------------------------------------------------------
# Daemon status
# ---------------------------------------------------------------------------


def _daemon_status() -> tuple[bool, int | None]:
    """Return (running, pid) using lazy daemon import."""
    try:
        import otto.daemon as daemon_mod

        running = daemon_mod.is_running()
        if running:
            pid_path = daemon_mod._pid_file()
            try:
                pid = int(pid_path.read_text(encoding="utf-8").strip())
            except (ValueError, FileNotFoundError):
                pid = None
            return True, pid
        return False, None
    except Exception:
        return False, None
