"""Hive manager -- creation, membership, lifecycle, persistence.

Persistence: ~/.llmhosts/hive.json (local membership)
Registry: ~/.llmhosts/hive_registry/ (invite code -> hive, for join flow)
"""

from __future__ import annotations

import json
import logging
import secrets
import uuid
from datetime import datetime, timezone
from pathlib import Path  # noqa: TC003 -- used at runtime
from typing import TYPE_CHECKING

from llmhosts.hive.models import Hive, HiveInvite, HiveMember, HiveRole

if TYPE_CHECKING:
    from llmhosts.hive.headscale import HeadscaleClient

logger = logging.getLogger(__name__)

_HIVE_FILENAME = "hive.json"
_REGISTRY_DIR = "hive_registry"


class HiveManager:
    """Manages Hive creation, membership, and lifecycle.

    Persistence: ~/.llmhosts/hive.json
    Registry: ~/.llmhosts/hive_registry/ for invite-code lookup (join flow).

    Headscale integration is optional — pass a HeadscaleClient to enable
    mesh networking. Without it, Hive works in local-only mode.
    """

    MAX_MEMBERS_PRO = 10

    def __init__(
        self,
        data_dir: Path,
        *,
        registry_path: Path | None = None,
        headscale: HeadscaleClient | None = None,
    ) -> None:
        self._data_dir = data_dir
        self._hive_file = data_dir / _HIVE_FILENAME
        self._registry_path = registry_path or (data_dir / _REGISTRY_DIR)
        self._hive: Hive | None = None
        self._current_member_id: str | None = None
        self._headscale = headscale

    async def initialize(self) -> None:
        """Load Hive from disk if present."""
        self._data_dir.mkdir(parents=True, exist_ok=True)
        self._registry_path.mkdir(parents=True, exist_ok=True)

        if self._hive_file.is_file():
            try:
                raw = self._hive_file.read_text(encoding="utf-8")
                data = json.loads(raw)
                if isinstance(data, dict) and "hive" in data:
                    self._hive = Hive.model_validate(data["hive"])
                    self._current_member_id = data.get("current_member_id")
                else:
                    self._hive = Hive.model_validate(data)
                    self._current_member_id = self._hive.owner_id if self._hive.members else None
                logger.info("Loaded Hive '%s' from %s", self._hive.name, self._hive_file)
            except (json.JSONDecodeError, ValueError) as exc:
                logger.warning("Failed to load Hive from %s: %s -- starting fresh", self._hive_file, exc)
                self._hive = None
                self._current_member_id = None
        else:
            logger.debug("No Hive file at %s -- no Hive joined", self._hive_file)
            self._hive = None
            self._current_member_id = None

    async def create_hive(self, name: str, owner_name: str, *, owner_email: str | None = None) -> Hive:
        """Create a new Hive. Caller becomes owner.

        If a HeadscaleClient is configured, also creates a Headscale user and
        pre-auth key so members can join the WireGuard mesh.

        Raises
        ------
        ValueError
            If user already belongs to a Hive.
        """
        if self._hive is not None:
            raise ValueError("Already in a Hive. Leave current Hive first.")

        hive_id = str(uuid.uuid4())
        invite_code = _generate_invite_code()
        now = datetime.now(tz=timezone.utc)

        owner = HiveMember(
            id=str(uuid.uuid4()),
            name=owner_name,
            email=owner_email,
            role=HiveRole.OWNER,
            joined_at=now,
            last_seen=now,
            devices=[],
            karma_score=1.0,
            compute_contributed_hours=0.0,
            compute_consumed_hours=0.0,
            active=True,
        )

        # Provision Headscale coordination plane (if available)
        headscale_user: str | None = None
        headscale_preauth_key: str | None = None
        if self._headscale is not None:
            hs_user_name = _headscale_user_name(name)
            try:
                await self._headscale.create_user(hs_user_name)
                headscale_preauth_key = await self._headscale.create_preauth_key(hs_user_name)
                headscale_user = hs_user_name
                logger.info("Provisioned Headscale user '%s' for Hive '%s'", hs_user_name, name)
            except Exception as exc:
                logger.warning("Headscale provisioning failed (mesh disabled): %s", exc)

        hive = Hive(
            id=hive_id,
            name=name,
            invite_code=invite_code,
            owner_id=owner.id,
            members=[owner],
            created_at=now,
            max_members=self.MAX_MEMBERS_PRO,
            headscale_user=headscale_user,
            headscale_preauth_key=headscale_preauth_key,
        )

        self._hive = hive
        self._current_member_id = owner.id
        await self.save()
        await self._publish_to_registry(hive)
        logger.info("Created Hive '%s' (id=%s, invite=%s)", name, hive_id, invite_code)
        return hive

    async def join_hive(
        self,
        invite_code: str,
        member_name: str,
        email: str | None = None,
    ) -> HiveMember:
        """Join a Hive using an invite code.

        Loads Hive from registry, adds self as member, saves locally and to registry.

        Raises
        ------
        ValueError
            If invite code invalid, Hive full, or already in a Hive.
        """
        if self._hive is not None:
            raise ValueError("Already in a Hive. Leave current Hive first.")

        hive = await self._load_from_registry(invite_code)
        if hive is None:
            raise ValueError(f"Invalid or expired invite code: {invite_code}")

        if len(hive.members) >= hive.max_members:
            raise ValueError(f"Hive is full (max {hive.max_members} members).")

        # Check for duplicate name
        for m in hive.members:
            if m.name.lower() == member_name.lower():
                raise ValueError(f"Member '{member_name}' already in this Hive.")

        now = datetime.now(tz=timezone.utc)
        new_member = HiveMember(
            id=str(uuid.uuid4()),
            name=member_name,
            email=email,
            role=HiveRole.MEMBER,
            joined_at=now,
            last_seen=now,
            devices=[],
            karma_score=1.0,
            compute_contributed_hours=0.0,
            compute_consumed_hours=0.0,
            active=True,
        )

        hive.members.append(new_member)
        self._hive = hive
        self._current_member_id = new_member.id
        await self.save()
        await self._publish_to_registry(hive)
        logger.info("Joined Hive '%s' as %s", hive.name, member_name)
        return new_member

    async def leave_hive(self, member_id: str | None = None) -> bool:
        """Leave the Hive. Returns True if left, False if not found.

        Use member_id to leave as a specific member, or omit to leave as current user.
        Owner cannot leave (must transfer or disband).
        """
        if self._hive is None:
            return False

        mid = member_id or self._current_member_id
        if mid is None:
            return False

        member = next((m for m in self._hive.members if m.id == mid), None)
        if member is None:
            return False

        if member.role == HiveRole.OWNER:
            raise ValueError("Owner cannot leave. Transfer ownership or delete the Hive.")

        self._hive.members = [m for m in self._hive.members if m.id != mid]

        if mid == self._current_member_id:
            self._hive = None
            self._current_member_id = None
            self._hive_file.write_text("{}", encoding="utf-8")
            logger.info("Left Hive")
        else:
            await self.save()
            await self._publish_to_registry(self._hive)

        return True

    async def get_hive(self) -> Hive | None:
        """Return the current Hive (if any)."""
        return self._hive

    def get_current_member_id(self) -> str | None:
        """Return the current user's member ID (if in a Hive)."""
        return self._current_member_id

    async def list_members(self) -> list[HiveMember]:
        """Return all members of the current Hive."""
        if self._hive is None:
            return []
        return list(self._hive.members)

    def get_sharing_settings(self) -> dict[str, bool | str | None | list[str]] | None:
        """Return the current member's sharing settings, or None if not in a Hive."""
        if self._hive is None or self._current_member_id is None:
            return None
        member = next((m for m in self._hive.members if m.id == self._current_member_id), None)
        if member is None:
            return None
        return {
            "sharing_enabled": member.sharing_enabled,
            "sharing_schedule": member.sharing_schedule,
            "shared_models": list(member.shared_models),
        }

    async def set_sharing_settings(
        self,
        *,
        sharing_enabled: bool | None = None,
        sharing_schedule: str | None = None,
        shared_models: list[str] | None = None,
    ) -> None:
        """Update the current member's sharing settings, then save and publish to registry."""
        if self._hive is None or self._current_member_id is None:
            raise ValueError("Not in a Hive. Create or join one first.")
        member = next((m for m in self._hive.members if m.id == self._current_member_id), None)
        if member is None:
            raise ValueError("Current member not found in Hive.")
        if sharing_enabled is not None:
            member.sharing_enabled = sharing_enabled
        if sharing_schedule is not None:
            member.sharing_schedule = (sharing_schedule.strip() or None) if sharing_schedule else None
        if shared_models is not None:
            member.shared_models = list(shared_models)
        await self.save()
        await self._publish_to_registry(self._hive)

    async def set_model_sharing(self, model: str, share: bool) -> None:
        """Add or remove a model from the current member's shared_models whitelist."""
        if self._hive is None or self._current_member_id is None:
            raise ValueError("Not in a Hive. Create or join one first.")
        member = next((m for m in self._hive.members if m.id == self._current_member_id), None)
        if member is None:
            raise ValueError("Current member not found in Hive.")
        if share:
            if model not in member.shared_models:
                member.shared_models = [*member.shared_models, model]
        else:
            member.shared_models = [m for m in member.shared_models if m != model]
        await self.save()
        await self._publish_to_registry(self._hive)

    async def generate_invite(self, *, expires_hours: float | None = None) -> HiveInvite:
        """Generate an invite for the current Hive."""
        if self._hive is None:
            raise ValueError("Not in a Hive. Create or join one first.")

        expires_at: datetime | None = None
        if expires_hours is not None:
            from datetime import timedelta

            expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=expires_hours)

        created_by = next((m.id for m in self._hive.members if m.role == HiveRole.OWNER), self._hive.owner_id)

        return HiveInvite(
            hive_id=self._hive.id,
            invite_code=self._hive.invite_code,
            created_by=created_by,
            expires_at=expires_at,
        )

    async def save(self) -> None:
        """Persist Hive to disk."""
        self._data_dir.mkdir(parents=True, exist_ok=True)
        if self._hive is None:
            self._hive_file.write_text("{}", encoding="utf-8")
        else:
            data = {
                "hive": self._hive.model_dump(mode="json"),
                "current_member_id": self._current_member_id,
            }
            self._hive_file.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
        logger.debug("Saved Hive to %s", self._hive_file)

    async def _publish_to_registry(self, hive: Hive) -> None:
        """Write Hive to registry so others can join via invite code."""
        self._registry_path.mkdir(parents=True, exist_ok=True)
        path = self._registry_path / f"{hive.invite_code}.json"
        data = hive.model_dump(mode="json")
        path.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
        logger.debug("Published Hive to registry at %s", path)

    async def _load_from_registry(self, invite_code: str) -> Hive | None:
        """Load Hive from registry by invite code."""
        path = self._registry_path / f"{invite_code}.json"
        if not path.is_file():
            return None
        try:
            raw = path.read_text(encoding="utf-8")
            data = json.loads(raw)
            return Hive.model_validate(data)
        except (json.JSONDecodeError, ValueError) as exc:
            logger.warning("Failed to load Hive from registry %s: %s", path, exc)
            return None

    async def _list_hives_from_registry(self) -> list[Hive]:
        """Load all Hives from the registry (all invite-code JSON files)."""
        hives: list[Hive] = []
        if not self._registry_path.is_dir():
            return hives
        for path in self._registry_path.glob("*.json"):
            hive = await self._load_from_registry(path.stem)
            if hive is not None:
                hives.append(hive)
        return hives

    async def sync_headscale_acls(
        self,
        acl_file_path: Path | None = None,
        *,
        admin_users: list[str] | None = None,
    ) -> None:
        """Update Headscale ACL policy so traffic is restricted to same-Hive only.

        Gathers all Hives from the registry, builds a Tailscale-format ACL policy
        (one group per Hive, accept only within same Hive), and writes it via
        HeadscaleClient.write_acl_policy_file if client and acl_file_path are set.
        Non-fatal if Headscale is unavailable or path not configured.

        Parameters
        ----------
        acl_file_path:
            Where to write the ACL file (e.g. Headscale config policy path).
            If None, policy is not written (Headscale has no policy REST API).
        admin_users:
            Optional Headscale user names allowed to reach all (group:admin).
        """
        if self._headscale is None:
            logger.debug("Headscale not configured — skipping ACL sync")
            return
        if acl_file_path is None:
            logger.debug("HEADSCALE_ACL_PATH not set — skipping ACL file write")
            return
        try:
            hives = await self._list_hives_from_registry()
            from llmhosts.hive.headscale import generate_acl_policy

            payloads = [{"id": h.id, "headscale_user": h.headscale_user} for h in hives if h.headscale_user]
            policy = generate_acl_policy(payloads, admin_users=admin_users)
            self._headscale.write_acl_policy_file(acl_file_path, policy)
            logger.info("Synced Headscale ACLs for %d Hive(s) to %s", len(payloads), acl_file_path)
        except Exception as exc:
            logger.warning("Headscale ACL sync failed (non-fatal): %s", exc)


def _generate_invite_code(length: int = 8) -> str:
    """Generate a short alphanumeric invite code."""
    alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"  # No ambiguous chars
    return "".join(secrets.choice(alphabet) for _ in range(length))


def _headscale_user_name(hive_name: str) -> str:
    """Convert a Hive name to a valid Headscale user name (lowercase, hyphens)."""
    import re

    name = hive_name.lower().strip()
    name = re.sub(r"[^a-z0-9]+", "-", name)
    name = name.strip("-")
    return name[:63] or "hive"  # Headscale user name max length
