"""Persona registry backed by local YAML files."""

from __future__ import annotations

import fcntl
import logging
import os
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import yaml
from pydantic import ValidationError

from cleveragents.tui.persona.schema import Persona

_logger = logging.getLogger(__name__)


def _default_config_dir() -> Path:
    override = os.environ.get("CLEVERAGENTS_CONFIG_DIR", "").strip()
    if override:
        return Path(override)
    return Path.home() / ".config" / "cleveragents"


@dataclass(slots=True)
class PersonaRegistry:
    """Load and persist TUI-only personas."""

    config_dir: Path = field(default_factory=_default_config_dir)

    @property
    def personas_dir(self) -> Path:
        return self.config_dir / "personas"

    @property
    def state_path(self) -> Path:
        return self.config_dir / "tui-state.yaml"

    @property
    def personas_lock_path(self) -> Path:
        return self.config_dir / ".personas.lock"

    @property
    def state_lock_path(self) -> Path:
        return self.config_dir / ".state.lock"

    def ensure_dirs(self) -> None:
        self.personas_dir.mkdir(parents=True, exist_ok=True)

    def _lock_file(self, lock_path: Path) -> Any:
        lock_path.parent.mkdir(parents=True, exist_ok=True)
        handle = lock_path.open("a+", encoding="utf-8")
        fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
        return handle

    @staticmethod
    def _atomic_write_yaml(path: Path, payload: dict[str, Any]) -> None:
        path.parent.mkdir(parents=True, exist_ok=True)
        with tempfile.NamedTemporaryFile(
            mode="w",
            encoding="utf-8",
            dir=str(path.parent),
            prefix=f".{path.name}.",
            suffix=".tmp",
            delete=False,
        ) as handle:
            yaml.safe_dump(payload, handle, sort_keys=False, default_flow_style=False)
            temp_name = handle.name
        Path(temp_name).replace(path)

    def persona_path(self, name: str) -> Path:
        Persona.validate_name_safe(name)
        result = (self.personas_dir / f"{name}.yaml").resolve()
        if not result.is_relative_to(self.personas_dir.resolve()):
            raise ValueError(f"Invalid persona name: {name}")
        return result

    def resolve_export_path(self, output_path: Path) -> Path:
        if output_path.is_absolute():
            raise ValueError(
                "Export path must be relative to current working directory"
            )
        base = Path.cwd().resolve()
        resolved = (base / output_path).resolve()
        if not resolved.is_relative_to(base):
            raise ValueError("Export path must stay within working directory")
        return resolved

    def resolve_import_path(self, input_path: Path) -> Path:
        if input_path.is_absolute():
            raise ValueError(
                "Import path must be relative to current working directory"
            )
        base = Path.cwd().resolve()
        resolved = (base / input_path).resolve()
        if not resolved.is_relative_to(base):
            raise ValueError("Import path must stay within working directory")
        return resolved

    def list_personas(self) -> list[Persona]:
        self.ensure_dirs()
        personas: list[Persona] = []
        for file in sorted(self.personas_dir.glob("*.y*ml")):
            try:
                raw = yaml.safe_load(file.read_text(encoding="utf-8")) or {}
                if not isinstance(raw, dict):
                    continue
                personas.append(Persona.model_validate(raw))
            except (yaml.YAMLError, ValidationError) as exc:
                _logger.warning("Skipping invalid persona file %s: %s", file, exc)
        return personas

    def get(self, name: str) -> Persona | None:
        file = self.persona_path(name)
        if not file.exists():
            return None
        raw = yaml.safe_load(file.read_text(encoding="utf-8")) or {}
        if not isinstance(raw, dict):
            return None
        return Persona.model_validate(raw)

    def _validate_cycle_order(self, persona: Persona) -> None:
        if persona.cycle_order <= 0:
            return
        for existing in self.list_personas():
            if existing.name == persona.name:
                continue
            if existing.cycle_order == persona.cycle_order:
                raise ValueError("cycle_order must be unique when greater than zero")

    def save(self, persona: Persona) -> Path:
        self.ensure_dirs()
        lock = self._lock_file(self.personas_lock_path)
        try:
            self._validate_cycle_order(persona)
            file = self.persona_path(persona.name)
            payload = persona.model_dump(mode="json", exclude_none=True)
            self._atomic_write_yaml(file, payload)
            return file
        finally:
            fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
            lock.close()

    def delete(self, name: str) -> bool:
        lock = self._lock_file(self.personas_lock_path)
        try:
            file = self.persona_path(name)
            try:
                file.unlink()
                return True
            except FileNotFoundError:
                return False
        finally:
            fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
            lock.close()

    def export_persona(self, name: str, output_path: Path) -> Path:
        persona = self.get(name)
        if persona is None:
            raise ValueError(f"Persona not found: {name}")
        payload = persona.model_dump(mode="json", exclude_none=True)
        safe_output = self.resolve_export_path(output_path)
        self._atomic_write_yaml(safe_output, payload)
        return safe_output

    def import_persona(self, input_path: Path) -> Persona:
        safe_input = self.resolve_import_path(input_path)
        raw = yaml.safe_load(safe_input.read_text(encoding="utf-8")) or {}
        if not isinstance(raw, dict):
            raise ValueError("Invalid persona file")
        persona = Persona.model_validate(raw)
        self.save(persona)
        return persona

    def load_state(self) -> dict[str, Any]:
        if not self.state_path.exists():
            return {}
        raw = yaml.safe_load(self.state_path.read_text(encoding="utf-8")) or {}
        if not isinstance(raw, dict):
            return {}
        return dict(raw)

    def save_state(self, state: dict[str, Any]) -> None:
        self.ensure_dirs()
        self._atomic_write_yaml(self.state_path, state)

    def get_last_persona(self) -> str | None:
        state = self.load_state()
        value = state.get("last_persona")
        return value if isinstance(value, str) and value else None

    def set_last_persona(self, name: str) -> None:
        lock = self._lock_file(self.state_lock_path)
        try:
            state = self.load_state()
            state["last_persona"] = name
            self.save_state(state)
        finally:
            fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
            lock.close()

    def ensure_default(self, actor: str = "local/mock-default") -> Persona:
        existing = self.get("default")
        if existing is not None:
            return existing
        persona = Persona(name="default", actor=actor, description="Default persona")
        saved = Persona.model_validate(persona.model_dump())
        self.save(saved)
        self.set_last_persona(saved.name)
        return saved
