"""TeamManager -- RBAC and team management with JSON persistence.

Manages teams, members, roles, model allowlists, budgets, and spending alerts.
Persistence: JSON file at ~/.llmhosts/team.json (like mesh/manager.py pattern).
"""

from __future__ import annotations

import fnmatch
import hashlib
import json
import logging
import uuid
from datetime import datetime, timezone
from pathlib import Path  # noqa: TC003 -- used at runtime

from llmhosts.middleware.redact import safe_log_email
from llmhosts.rbac.models import (
    AccessDecision,
    Role,
    SpendingAlert,
    Team,
    TeamMember,
)

logger = logging.getLogger(__name__)

_TEAM_FILENAME = "team.json"


def _hash_api_key(team_id: str, api_key: str) -> str:
    """Produce a stable hash for API key lookup. Uses SHA-256 with team salt."""
    payload = f"{team_id}:{api_key.strip()}"
    return hashlib.sha256(payload.encode("utf-8")).hexdigest()


def _model_matches_allowlist(model: str, patterns: list[str]) -> bool:
    """Return True if model matches any pattern in allowlist.

    Empty patterns = allow all. Uses fnmatch: 'llama*', 'gpt-4*', etc.
    """
    if not patterns:
        return True
    model_lower = model.lower()
    return any(fnmatch.fnmatch(model_lower, pattern.lower()) for pattern in patterns)


class TeamManager:
    """Manages teams, members, roles, and access control.

    Persistence: JSON file at ~/.llmhosts/team.json
    """

    def __init__(self, data_dir: Path) -> None:
        self._data_dir = data_dir
        self._team_file = data_dir / _TEAM_FILENAME
        self._team: Team | None = None
        self._alerts: list[SpendingAlert] = []

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    async def initialize(self) -> None:
        """Load team data from disk."""
        self._data_dir.mkdir(parents=True, exist_ok=True)
        if self._team_file.is_file():
            try:
                raw = self._team_file.read_text(encoding="utf-8")
                data = json.loads(raw)
                # Alerts are stored alongside team in same file
                alerts_raw = data.pop("alerts", [])
                self._team = Team.model_validate(data)
                self._alerts = (
                    [SpendingAlert.model_validate(a) for a in alerts_raw] if isinstance(alerts_raw, list) else []
                )
                logger.info("Loaded team '%s' from %s", self._team.name if self._team else "?", self._team_file)
            except (json.JSONDecodeError, ValueError) as exc:
                logger.warning("Failed to load team from %s: %s -- starting fresh", self._team_file, exc)
                self._team = None
                self._alerts = []
        else:
            logger.debug("No team file at %s -- no team loaded", self._team_file)
            self._team = None
            self._alerts = []

    def _get_team(self) -> Team:
        """Return the current team, raising if none exists."""
        if self._team is None:
            raise ValueError("No team loaded. Create a team first with 'llmhost team create'.")
        return self._team

    def _get_member(self, email_or_id: str) -> TeamMember | None:
        """Get member by email or id."""
        team = self._get_team()
        email_or_id_lower = email_or_id.strip().lower()
        for m in team.members:
            if m.id == email_or_id or m.email.lower() == email_or_id_lower:
                return m
        return None

    # ------------------------------------------------------------------
    # Team CRUD
    # ------------------------------------------------------------------

    async def create_team(self, name: str, owner_email: str) -> Team:
        """Create a new team. Caller becomes admin."""
        if self._team is not None:
            raise ValueError("A team already exists. Use a different data directory or remove the existing team.")

        now = datetime.now(tz=timezone.utc)
        owner_id = str(uuid.uuid4())
        owner = TeamMember(
            id=owner_id,
            email=owner_email.strip().lower(),
            name=owner_email.split("@")[0],
            role=Role.ADMIN,
            model_allowlist=[],
            monthly_budget=None,
            current_spend=0.0,
            joined_at=now,
            last_active=now,
            active=True,
        )

        team = Team(
            id=str(uuid.uuid4()),
            name=name.strip(),
            owner_id=owner_id,
            plan="team",
            max_seats=15,
            members=[owner],
            created_at=now,
        )
        self._team = team
        self._alerts = []
        await self.save()
        logger.info("Created team '%s' with owner %s", team.name, safe_log_email(owner_email))
        return team

    # ------------------------------------------------------------------
    # Member CRUD
    # ------------------------------------------------------------------

    async def add_member(self, email: str, name: str, role: Role) -> TeamMember:
        """Add a member to the team."""
        team = self._get_team()
        email_lower = email.strip().lower()

        for m in team.members:
            if m.email == email_lower:
                raise ValueError(f"Member with email '{email}' already exists in the team.")

        if len(team.members) >= team.max_seats:
            raise ValueError(f"Team has reached maximum seats ({team.max_seats}).")

        now = datetime.now(tz=timezone.utc)
        member = TeamMember(
            id=str(uuid.uuid4()),
            email=email_lower,
            name=name.strip(),
            role=role,
            model_allowlist=[],
            monthly_budget=None,
            current_spend=0.0,
            joined_at=now,
            last_active=None,
            active=True,
        )
        team.members.append(member)
        await self.save()
        logger.info("Added member %s (%s) as %s", safe_log_email(email), name, role.value)
        return member

    async def remove_member(self, member_id: str) -> bool:
        """Remove a member from the team. Cannot remove the owner."""
        team = self._get_team()
        member = self._get_member(member_id)
        if member is None:
            return False
        if member.id == team.owner_id:
            raise ValueError("Cannot remove the team owner.")
        team.members = [m for m in team.members if m.id != member.id]
        await self.save()
        logger.info("Removed member %s", member_id)
        return True

    async def update_role(self, member_id: str, role: Role) -> TeamMember | None:
        """Update a member's role."""
        self._get_team()
        member = self._get_member(member_id)
        if member is None:
            return None
        member.role = role
        await self.save()
        logger.info("Updated role for %s to %s", safe_log_email(member.email), role.value)
        return member

    async def set_model_allowlist(self, member_id: str, models: list[str]) -> TeamMember | None:
        """Set which models a member can use."""
        self._get_team()
        member = self._get_member(member_id)
        if member is None:
            return None
        member.model_allowlist = [m.strip() for m in models if m.strip()]
        await self.save()
        logger.info("Set model allowlist for %s: %s", safe_log_email(member.email), member.model_allowlist)
        return member

    async def set_budget(self, member_id: str, monthly_budget: float | None) -> TeamMember | None:
        """Set monthly spending budget for a member. Pass None to remove limit."""
        self._get_team()
        member = self._get_member(member_id)
        if member is None:
            return None
        member.monthly_budget = monthly_budget
        await self.save()
        if monthly_budget is not None:
            logger.info("Set budget for %s: $%.2f/month", safe_log_email(member.email), monthly_budget)
        else:
            logger.info("Removed budget limit for %s", safe_log_email(member.email))
        return member

    # ------------------------------------------------------------------
    # API key binding (for middleware lookup)
    # ------------------------------------------------------------------

    def set_member_api_key(self, member_id: str, api_key: str) -> None:
        """Bind an API key to a member for request authentication."""
        team = self._get_team()
        member = self._get_member(member_id)
        if member is None:
            raise ValueError(f"Member '{member_id}' not found.")
        member.api_key_hash = _hash_api_key(team.id, api_key)
        logger.info("Bound API key for member %s", safe_log_email(member.email))

    def get_member_by_api_key(self, api_key: str) -> TeamMember | None:
        """Resolve API key to team member. Returns None if no team or key doesn't match."""
        if self._team is None:
            return None
        key_hash = _hash_api_key(self._team.id, api_key)
        for m in self._team.members:
            if m.api_key_hash == key_hash:
                return m
        return None

    # ------------------------------------------------------------------
    # Access control
    # ------------------------------------------------------------------

    async def check_access(self, member_id: str, model: str) -> AccessDecision:
        """Check if a member can access a model.

        Rules:
        1. Viewer role → denied (read-only)
        2. Model not in allowlist → denied
        3. Budget exceeded → denied
        4. Otherwise → allowed
        """
        self._get_team()
        member = self._get_member(member_id)
        if member is None:
            return AccessDecision(
                allowed=False,
                reason=f"Member '{member_id}' not found",
                role=Role.VIEWER,  # safe default
                model_requested=model,
                budget_remaining=None,
            )

        if not member.active:
            return AccessDecision(
                allowed=False,
                reason="Member is inactive",
                role=member.role,
                model_requested=model,
                budget_remaining=None,
            )

        if member.role == Role.VIEWER:
            return AccessDecision(
                allowed=False,
                reason="Viewer role cannot access models",
                role=Role.VIEWER,
                model_requested=model,
                budget_remaining=None,
            )

        if not _model_matches_allowlist(model, member.model_allowlist):
            return AccessDecision(
                allowed=False,
                reason=f"Model '{model}' not in allowlist",
                role=member.role,
                model_requested=model,
                budget_remaining=member.monthly_budget - member.current_spend if member.monthly_budget else None,
            )

        if member.monthly_budget is not None and member.current_spend >= member.monthly_budget:
            return AccessDecision(
                allowed=False,
                reason="Monthly budget exceeded",
                role=member.role,
                model_requested=model,
                budget_remaining=0.0,
            )

        budget_remaining = None
        if member.monthly_budget is not None:
            budget_remaining = member.monthly_budget - member.current_spend

        return AccessDecision(
            allowed=True,
            reason="Access granted",
            role=member.role,
            model_requested=model,
            budget_remaining=budget_remaining,
        )

    # ------------------------------------------------------------------
    # Spending
    # ------------------------------------------------------------------

    async def record_spend(self, member_id: str, amount: float) -> None:
        """Record spending for a member. Checks and triggers alerts."""
        team = self._get_team()
        member = self._get_member(member_id)
        if member is None:
            logger.warning("record_spend: member '%s' not found", member_id)
            return

        member.current_spend += amount
        member.last_active = datetime.now(tz=timezone.utc)

        # Check per-member alerts
        for alert in self._alerts:
            if alert.member_id == member.id and not alert.triggered and member.current_spend >= alert.threshold:
                alert.triggered = True
                alert.triggered_at = datetime.now(tz=timezone.utc)
                logger.warning(
                    "Spending alert triggered for %s: $%.2f >= $%.2f",
                    member.email,
                    member.current_spend,
                    alert.threshold,
                )

        # Check team-wide alerts (simplified: use team total)
        team_spend = sum(m.current_spend for m in team.members)
        for alert in self._alerts:
            if (
                alert.member_id is None
                and alert.team_id == team.id
                and not alert.triggered
                and team_spend >= alert.threshold
            ):
                alert.triggered = True
                alert.triggered_at = datetime.now(tz=timezone.utc)
                logger.warning("Team spending alert triggered: $%.2f >= $%.2f", team_spend, alert.threshold)

        await self.save()

    # ------------------------------------------------------------------
    # Alerts
    # ------------------------------------------------------------------

    async def get_alerts(self) -> list[SpendingAlert]:
        """Get all triggered spending alerts."""
        return [a for a in self._alerts if a.triggered]

    async def add_alert(
        self,
        threshold: float,
        member_id: str | None = None,
        period: str = "month",
    ) -> SpendingAlert:
        """Add a spending alert."""
        team = self._get_team()
        alert = SpendingAlert(
            id=str(uuid.uuid4()),
            team_id=team.id,
            member_id=member_id,
            threshold=threshold,
            period=period,
            triggered=False,
            triggered_at=None,
        )
        self._alerts.append(alert)
        await self.save()
        logger.info("Added spending alert: $%.2f for %s", threshold, member_id or "team")
        return alert

    # ------------------------------------------------------------------
    # Queries
    # ------------------------------------------------------------------

    async def list_members(self) -> list[TeamMember]:
        """List all team members."""
        team = self._get_team()
        return list(team.members)

    def has_team(self) -> bool:
        """Return True if a team is loaded."""
        return self._team is not None

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

    async def save(self) -> None:
        """Persist team data to disk."""
        if self._team is None:
            return
        self._data_dir.mkdir(parents=True, exist_ok=True)
        data = self._team.model_dump(mode="json")
        data["alerts"] = [a.model_dump(mode="json") for a in self._alerts]
        self._team_file.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
        logger.debug("Saved team to %s", self._team_file)
