"""Project-aware voice assignment via round-robin.

Each project is assigned the next unused voice from a curated English
voice pool.  Assignments persist to a JSON file so projects keep their
voice across server restarts.
"""

import asyncio
import json
import sys
import time
from pathlib import Path
from urllib.parse import unquote, urlparse

from fastmcp import Context

from .engines.base import TTSEngine
from .settings import settings


def extract_project_name(roots: list) -> str | None:
    """Extract project name from the first file:// root URI.

    Takes the last path component of the first file:// URI found.
    ``file:///home/rpm/fix-tts`` -> ``"fix-tts"``
    """
    for root in roots:
        uri = str(root.uri) if hasattr(root, "uri") else str(root)
        parsed = urlparse(uri)
        if parsed.scheme != "file":
            continue
        path = unquote(parsed.path).rstrip("/")
        if not path:
            continue
        return path.rsplit("/", 1)[-1]
    return None


def _build_identity_pool(voices: list[str]) -> list[str]:
    """Filter a voice list down to the auto-assignment pool.

    Keeps only voices matching configured prefixes, removes excluded
    voices, then interleaves by gender and accent so consecutive
    assignments sound maximally distinct.
    """
    prefixes = tuple(
        p.strip() for p in settings.voice_identity_prefixes.split(",") if p.strip()
    )
    excludes = {
        v.strip().lower()
        for v in settings.voice_identity_exclude.split(",")
        if v.strip()
    }
    eligible = [
        v for v in voices
        if v.startswith(prefixes) and v.lower() not in excludes
    ]
    return _interleave_voices(eligible)


def _interleave_voices(voices: list[str]) -> list[str]:
    """Interleave voices by category for maximum perceptual diversity.

    Groups by prefix (af_=American female, am_=American male, bf_=British
    female, etc.) then round-robins across groups so consecutive voices
    alternate gender and accent.
    """
    buckets: dict[str, list[str]] = {}
    for v in voices:
        prefix = v[:3] if len(v) >= 3 else "xx_"
        buckets.setdefault(prefix, []).append(v)

    # Order buckets for max contrast: female/male alternating, US/UK/EU
    bucket_order = ["af_", "bm_", "ef_", "am_", "bf_", "em_"]
    ordered = [k for k in bucket_order if k in buckets]
    # Append any remaining prefixes not in the predefined order
    ordered += [k for k in sorted(buckets) if k not in ordered]

    result: list[str] = []
    while any(buckets[k] for k in ordered):
        for k in ordered:
            if buckets[k]:
                result.append(buckets[k].pop(0))
    return result


class VoiceIdentityCache:
    """Server-level cache with round-robin assignment and optional persistence.

    Instead of hashing, projects are assigned voices sequentially — first
    project gets voice 0, second gets voice 1, etc.  This guarantees
    maximum diversity until the pool is exhausted, then wraps around.

    Assignments are persisted to a JSON file (if configured) so projects
    keep the same voice across server restarts.
    """

    POOL_TTL = 300  # 5 minutes

    def __init__(self, persist_path: str = "") -> None:
        self._voice_pools: dict[str, tuple[list[str], float]] = {}
        self._assignments: dict[str, dict[str, str]] = {}  # engine -> {project -> voice}
        self._persist_path: Path | None = Path(persist_path) if persist_path else None
        self._load()

    # -- Persistence ----------------------------------------------------------

    def _load(self) -> None:
        if not self._persist_path or not self._persist_path.exists():
            return
        try:
            data = json.loads(self._persist_path.read_text())
            self._assignments = data.get("assignments", {})
            count = sum(len(v) for v in self._assignments.values())
            print(
                f"  Voice identity: loaded {count} assignments from {self._persist_path}",
                file=sys.stderr,
            )
        except Exception as exc:
            print(f"  Voice identity: failed to load {self._persist_path} ({exc})", file=sys.stderr)

    def _save(self) -> None:
        if not self._persist_path:
            return
        try:
            self._persist_path.parent.mkdir(parents=True, exist_ok=True)
            self._persist_path.write_text(json.dumps({"assignments": self._assignments}, indent=2))
        except Exception as exc:
            print(f"Voice identity: failed to save ({exc})", file=sys.stderr)

    # -- Pool cache -----------------------------------------------------------

    def get_pool(self, engine_name: str) -> list[str] | None:
        entry = self._voice_pools.get(engine_name)
        if entry is None:
            return None
        pool, cached_at = entry
        if time.monotonic() - cached_at > self.POOL_TTL:
            del self._voice_pools[engine_name]
            return None
        return list(pool)

    def put_pool(self, engine_name: str, voices: list[str]) -> None:
        self._voice_pools[engine_name] = (list(voices), time.monotonic())

    # -- Assignment -----------------------------------------------------------

    def get(self, engine_name: str, project_name: str) -> str | None:
        return self._assignments.get(engine_name, {}).get(project_name)

    def assign_next(self, engine_name: str, project_name: str, pool: list[str]) -> str:
        """Assign the next unused voice from the pool via round-robin."""
        engine_assignments = self._assignments.setdefault(engine_name, {})

        # Check if already assigned
        existing = engine_assignments.get(project_name)
        if existing and existing in pool:
            return existing

        # Find voices already taken
        used = set(engine_assignments.values())

        # Pick the first unused voice
        for voice in pool:
            if voice not in used:
                engine_assignments[project_name] = voice
                self._save()
                return voice

        # Pool exhausted — wrap around, pick least-used
        usage = {}
        for v in pool:
            usage[v] = sum(1 for assigned in used if assigned == v)
        voice = min(pool, key=lambda v: usage.get(v, 0))
        engine_assignments[project_name] = voice
        self._save()
        return voice


async def get_project_name(ctx: Context) -> str | None:
    """Fetch project name from MCP roots (server->client round-trip).

    Uses a 2-second timeout because list_roots() hangs indefinitely when
    the client doesn't support roots or the transport is stateless HTTP
    with no bidirectional channel.
    """
    try:
        roots = await asyncio.wait_for(ctx.list_roots(), timeout=2.0)
        if not roots:
            return None
        return extract_project_name(roots)
    except TimeoutError:
        return None
    except Exception as exc:
        exc_type = type(exc).__name__
        print(
            f"Voice identity: roots unavailable ({exc_type}: {exc})",
            file=sys.stderr,
        )
        return None


async def resolve_voice(
    engine_name: str,
    engine: TTSEngine,
    project_name: str,
    cache: VoiceIdentityCache,
) -> str | None:
    """Resolve voice for a project using round-robin assignment."""
    # Check cache first
    cached = cache.get(engine_name, project_name)
    if cached is not None:
        return cached

    # Build or fetch the identity pool
    pool = cache.get_pool(engine_name)
    if pool is None:
        all_voices = await engine.list_voices()
        pool = _build_identity_pool(all_voices)
        if not pool:
            return None
        cache.put_pool(engine_name, pool)

    voice = cache.assign_next(engine_name, project_name, pool)
    print(f"Voice assigned: {project_name} -> {engine_name}/{voice}", file=sys.stderr)
    return voice
