"""Quest state conflict resolution for multi-player sessions.

When multiple agents attempt to advance the same quest simultaneously
the arbiter serialises access via a per-quest lock, records the
authoritative state, and rejects conflicting updates with a logged
``ConflictRecord``.  First-come-first-served semantics are used.
"""

from __future__ import annotations

import logging
import threading
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import StrEnum

logger = logging.getLogger(__name__)


class QuestStage(StrEnum):
    """Canonical quest progression stages."""

    AVAILABLE = "available"
    ACTIVE = "active"
    COMPLETED = "completed"
    FAILED = "failed"


@dataclass
class QuestLock:
    """Lock held by a player on a quest."""

    player_id: str
    quest_id: str
    stage: QuestStage
    acquired_at: datetime = field(default_factory=lambda: datetime.now(UTC))


@dataclass
class ConflictRecord:
    """Record of a detected quest-state conflict."""

    quest_id: str
    winner: str
    loser: str
    resolution: str
    timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))


class QuestArbiter:
    """Serialise quest progression across multiple concurrent agents.

    The first player to ``claim`` a quest holds the authoritative lock.
    Subsequent claimants are rejected — their attempt is recorded in
    ``conflicts`` for audit purposes.

    Thread-safe: all mutations are protected by an internal lock.
    """

    def __init__(self) -> None:
        self._locks: dict[str, QuestLock] = {}
        self._conflicts: list[ConflictRecord] = []
        self._mu = threading.Lock()

    # -- public API --------------------------------------------------------

    def claim(self, player_id: str, quest_id: str, stage: QuestStage) -> bool:
        """Attempt to claim *quest_id* for *player_id* at *stage*.

        Returns ``True`` if the claim was granted (no existing lock, or same
        player updating their own lock), ``False`` on conflict.
        """
        with self._mu:
            existing = self._locks.get(quest_id)
            if existing is None:
                self._locks[quest_id] = QuestLock(
                    player_id=player_id,
                    quest_id=quest_id,
                    stage=stage,
                )
                logger.info(
                    "QuestArbiter: %s claimed '%s' at stage %s",
                    player_id,
                    quest_id,
                    stage,
                )
                return True

            if existing.player_id == player_id:
                existing.stage = stage
                return True

            # Conflict: different player already holds the lock
            conflict = ConflictRecord(
                quest_id=quest_id,
                winner=existing.player_id,
                loser=player_id,
                resolution=(f"first-come-first-served; {existing.player_id} retains lock"),
            )
            self._conflicts.append(conflict)
            logger.warning(
                "QuestArbiter: conflict on '%s' — %s rejected (held by %s)",
                quest_id,
                player_id,
                existing.player_id,
            )
            return False

    def release(self, player_id: str, quest_id: str) -> bool:
        """Release *player_id*'s lock on *quest_id*.

        Returns ``True`` if released, ``False`` if the player didn't hold it.
        """
        with self._mu:
            lock = self._locks.get(quest_id)
            if lock is not None and lock.player_id == player_id:
                del self._locks[quest_id]
                logger.info("QuestArbiter: %s released '%s'", player_id, quest_id)
                return True
            return False

    def advance(
        self,
        player_id: str,
        quest_id: str,
        new_stage: QuestStage,
    ) -> bool:
        """Advance a quest the player already holds to *new_stage*.

        Returns ``True`` on success.  Locks for COMPLETED/FAILED stages are
        automatically released after the advance.
        """
        with self._mu:
            lock = self._locks.get(quest_id)
            if lock is None or lock.player_id != player_id:
                logger.warning(
                    "QuestArbiter: %s cannot advance '%s' — not the lock holder",
                    player_id,
                    quest_id,
                )
                return False
            lock.stage = new_stage
            logger.info(
                "QuestArbiter: %s advanced '%s' to %s",
                player_id,
                quest_id,
                new_stage,
            )
            if new_stage in (QuestStage.COMPLETED, QuestStage.FAILED):
                del self._locks[quest_id]
            return True

    def get_stage(self, quest_id: str) -> QuestStage | None:
        """Return the authoritative stage for *quest_id*, or ``None``."""
        with self._mu:
            lock = self._locks.get(quest_id)
            return lock.stage if lock else None

    def lock_holder(self, quest_id: str) -> str | None:
        """Return the player_id holding the lock for *quest_id*, or ``None``."""
        with self._mu:
            lock = self._locks.get(quest_id)
            return lock.player_id if lock else None

    @property
    def active_lock_count(self) -> int:
        with self._mu:
            return len(self._locks)

    @property
    def conflict_count(self) -> int:
        return len(self._conflicts)

    @property
    def conflicts(self) -> list[ConflictRecord]:
        return list(self._conflicts)
