"""Core business logic."""

import contextlib
import time
from collections.abc import Callable
from typing import cast

from mm_clikit import CliError, is_process_running
from pydantic import BaseModel, ConfigDict

from mb_stash.config import Config
from mb_stash.core.stash import Stash
from mb_stash.daemon.client import DaemonClient
from mb_stash.daemon.process import ensure_daemon, is_connectable, is_daemon_available, stop_daemon
from mb_stash.daemon.protocol import Response


class HealthStatus(BaseModel):
    """Daemon health status."""

    model_config = ConfigDict(frozen=True)

    running: bool
    locked: bool


class Service:
    """Main application service — single entry point for all operations."""

    def __init__(self, stash: Stash, cfg: Config) -> None:  # noqa: D107
        self._cfg = cfg
        self._stash = stash
        self._client = DaemonClient(cfg)

    # --- Setup (no daemon needed) ---

    def change_password(self, old_password: str, new_password: str) -> None:
        """Re-encrypt the store with a new password. Stops daemon first."""
        stop_daemon(self._cfg)
        self._stash.change_password(old_password, new_password)

    # --- Daemon lifecycle ---

    def unlock(self, password: str) -> None:
        """Ensure daemon is running and unlock the stash."""
        ensure_daemon(self._cfg)
        resp = self._client.unlock(password)
        if not resp.ok:
            raise CliError(resp.message, resp.error)

    def ensure_unlocked(self, password_prompt: Callable[[], str]) -> None:
        """Ensure daemon is running and stash is unlocked, prompting if needed."""
        ensure_daemon(self._cfg)
        resp = self._client.health()
        if resp.ok and not resp.data.get("unlocked"):
            password = password_prompt()
            unlock_resp = self._client.unlock(password)
            if not unlock_resp.ok:
                raise CliError(unlock_resp.message, unlock_resp.error)

    def lock(self) -> None:
        """Lock the stash. No-op if daemon is not running."""
        if not is_daemon_available(self._cfg):
            return
        resp = self._client.lock()
        if not resp.ok:
            raise CliError(resp.message, resp.error)

    def stop(self) -> None:
        """Stop the daemon."""
        pid_alive = is_process_running(self._cfg.daemon_pid_path, command_contains="mb-stash")
        reachable = is_connectable(self._cfg.daemon_sock_path)

        if pid_alive:
            stop_daemon(self._cfg)
        elif reachable:
            self._client.stop()

        # Give socket-based stop a moment to complete
        if not pid_alive and reachable:
            time.sleep(0.5)

        if is_daemon_available(self._cfg):
            raise CliError("Daemon is still running after stop attempt.", "STOP_FAILED")

    def health(self) -> HealthStatus:
        """Check daemon health status."""
        if not is_daemon_available(self._cfg):
            return HealthStatus(running=False, locked=True)
        resp = self._client.health()
        if not resp.ok:
            raise CliError(resp.message, resp.error)
        return HealthStatus(running=True, locked=not resp.data.get("unlocked", False))

    # --- CRUD ---

    def get(self, key: str, *, password_prompt: Callable[[], str] | None = None) -> str:
        """Get a secret by key."""
        ensure_daemon(self._cfg)
        resp = self._send("get", {"key": key}, password_prompt=password_prompt)
        return str(resp.data["value"])

    def list_keys(self, filter_: str | None = None, *, password_prompt: Callable[[], str] | None = None) -> list[str]:
        """List stored keys, optionally filtered by substring."""
        ensure_daemon(self._cfg)
        params: dict[str, str] = {}
        if filter_:
            params["filter"] = filter_
        resp = self._send("list", params, password_prompt=password_prompt)
        return cast(list[str], resp.data["keys"])

    def add(self, key: str, value: str, *, password_prompt: Callable[[], str] | None = None) -> None:
        """Add or update a secret."""
        ensure_daemon(self._cfg)
        self._send("add", {"key": key, "value": value}, password_prompt=password_prompt)

    def delete(self, key: str, *, password_prompt: Callable[[], str] | None = None) -> None:
        """Delete a secret."""
        ensure_daemon(self._cfg)
        self._send("delete", {"key": key}, password_prompt=password_prompt)

    def rename(self, key: str, new_key: str, *, password_prompt: Callable[[], str] | None = None) -> None:
        """Rename a secret key without changing its value."""
        ensure_daemon(self._cfg)
        self._send("rename", {"key": key, "new_key": new_key}, password_prompt=password_prompt)

    # --- Clipboard ---

    def schedule_clipboard_clear(self, value: str) -> int | None:
        """Schedule clipboard auto-clear via daemon. Returns timeout in seconds, or None."""
        with contextlib.suppress(Exception):
            resp = self._client.send("schedule_clipboard_clear", {"value": value})
            if resp.ok:
                raw = resp.data.get("clipboard_timeout")
                if isinstance(raw, int):
                    return raw
        return None

    # --- Private helpers ---

    def _send(self, command: str, params: dict[str, str], *, password_prompt: Callable[[], str] | None = None) -> Response:
        """Send a command to daemon, optionally auto-unlocking."""
        if password_prompt is not None:
            resp = self._client.send_auto_unlock(command, params, password_prompt=password_prompt)
        else:
            resp = self._client.send(command, params)
        if not resp.ok:
            raise CliError(resp.message, resp.error)
        return resp
