"""Pydantic models for entity synchronization.

Defines the request/response models, sync state, conflict records, and
vector clocks used by the ``_cleveragents/sync/*`` extension methods.

Entity types eligible for sync: actors, skills, actions, plans, sessions.
The ``local/`` namespace is **never** synced — it exists only on the client.
"""

from __future__ import annotations

from enum import StrEnum
from typing import Any

from pydantic import BaseModel, ConfigDict, field_validator

# ---------------------------------------------------------------------------
# Enumerations
# ---------------------------------------------------------------------------


class SyncDirection(StrEnum):
    """Direction of a sync operation."""

    PULL = "pull"
    PUSH = "push"


class SyncEntityType(StrEnum):
    """Entity types eligible for synchronization."""

    ACTOR = "actor"
    SKILL = "skill"
    ACTION = "action"
    PLAN = "plan"
    SESSION = "session"


class ConflictResolution(StrEnum):
    """Strategies for resolving sync conflicts."""

    LAST_WRITER_WINS = "last_writer_wins"
    MANUAL = "manual"
    SERVER_WINS = "server_wins"
    CLIENT_WINS = "client_wins"


class SyncOperationStatus(StrEnum):
    """Status of a sync operation."""

    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    FAILED = "failed"
    CONFLICT = "conflict"
    QUEUED = "queued"


# ---------------------------------------------------------------------------
# Vector clock
# ---------------------------------------------------------------------------


class VectorClock(BaseModel):
    """Vector clock for conflict detection across distributed nodes.

    Each entry maps a node identifier to a monotonically increasing
    counter.  Two clocks are compared element-wise: if every component
    of A is ``<=`` the corresponding component of B (and at least one
    is strictly less) then A *happened before* B.  Otherwise the clocks
    are concurrent and a conflict exists.
    """

    model_config = ConfigDict(strict=False)

    entries: dict[str, int] = {}

    def increment(self, node_id: str) -> VectorClock:
        """Return a new clock with *node_id* incremented by one.

        Args:
            node_id: Identifier of the node to increment.

        Returns:
            A new ``VectorClock`` with the updated counter.

        Raises:
            ValueError: If *node_id* is empty.
        """
        if not node_id:
            raise ValueError("node_id must be a non-empty string")
        new_entries = dict(self.entries)
        new_entries[node_id] = new_entries.get(node_id, 0) + 1
        return VectorClock(entries=new_entries)

    def merge(self, other: VectorClock) -> VectorClock:
        """Return a new clock that is the element-wise max of both clocks.

        Args:
            other: The vector clock to merge with.

        Returns:
            A new ``VectorClock`` with the merged counters.

        Raises:
            TypeError: If *other* is not a ``VectorClock``.
        """
        if not isinstance(other, VectorClock):
            raise TypeError("other must be a VectorClock instance")
        all_nodes = set(self.entries) | set(other.entries)
        merged: dict[str, int] = {}
        for node in all_nodes:
            merged[node] = max(
                self.entries.get(node, 0),
                other.entries.get(node, 0),
            )
        return VectorClock(entries=merged)

    def happens_before(self, other: VectorClock) -> bool:
        """Return ``True`` if this clock causally precedes *other*.

        Args:
            other: The vector clock to compare against.

        Returns:
            ``True`` if every component of *self* is ``<=`` the
            corresponding component of *other* and at least one is
            strictly less.

        Raises:
            TypeError: If *other* is not a ``VectorClock``.
        """
        if not isinstance(other, VectorClock):
            raise TypeError("other must be a VectorClock instance")
        all_nodes = set(self.entries) | set(other.entries)
        at_least_one_less = False
        for node in all_nodes:
            self_val = self.entries.get(node, 0)
            other_val = other.entries.get(node, 0)
            if self_val > other_val:
                return False
            if self_val < other_val:
                at_least_one_less = True
        return at_least_one_less

    def is_concurrent(self, other: VectorClock) -> bool:
        """Return ``True`` if the two clocks are concurrent (conflict).

        Args:
            other: The vector clock to compare against.

        Returns:
            ``True`` if neither clock happens-before the other.

        Raises:
            TypeError: If *other* is not a ``VectorClock``.
        """
        if not isinstance(other, VectorClock):
            raise TypeError("other must be a VectorClock instance")
        return (
            not self.happens_before(other)
            and not other.happens_before(self)
            and self.entries != other.entries
        )


# ---------------------------------------------------------------------------
# Sync entity snapshot
# ---------------------------------------------------------------------------


class SyncEntitySnapshot(BaseModel):
    """A snapshot of a single entity for sync purposes."""

    model_config = ConfigDict(strict=False)

    entity_id: str
    entity_type: SyncEntityType
    namespace: str
    name: str
    version: int = 1
    data: dict[str, Any] = {}
    vector_clock: VectorClock = VectorClock()
    updated_at: str = ""

    @field_validator("entity_id", "namespace", "name")
    @classmethod
    def _must_be_non_empty(cls, value: str) -> str:
        if not value:
            raise ValueError("field must not be empty")
        return value


# ---------------------------------------------------------------------------
# Conflict record
# ---------------------------------------------------------------------------


class SyncConflict(BaseModel):
    """A record of a detected sync conflict between local and server."""

    model_config = ConfigDict(strict=False)

    conflict_id: str
    entity_id: str
    entity_type: SyncEntityType
    namespace: str
    local_snapshot: SyncEntitySnapshot
    server_snapshot: SyncEntitySnapshot
    local_clock: VectorClock
    server_clock: VectorClock
    resolution: ConflictResolution = ConflictResolution.LAST_WRITER_WINS
    resolved: bool = False
    resolved_at: str | None = None
    winner: str | None = None

    @field_validator("conflict_id", "entity_id", "namespace")
    @classmethod
    def _must_be_non_empty(cls, value: str) -> str:
        if not value:
            raise ValueError("field must not be empty")
        return value


# ---------------------------------------------------------------------------
# Sync state
# ---------------------------------------------------------------------------


class SyncState(BaseModel):
    """Tracks the sync state between client and server for a namespace."""

    model_config = ConfigDict(strict=False)

    namespace: str
    last_sync_at: str | None = None
    last_pull_at: str | None = None
    last_push_at: str | None = None
    local_clock: VectorClock = VectorClock()
    server_clock: VectorClock = VectorClock()
    pending_pushes: int = 0
    pending_pulls: int = 0
    conflicts: list[SyncConflict] = []

    @field_validator("namespace")
    @classmethod
    def _namespace_non_empty(cls, value: str) -> str:
        if not value:
            raise ValueError("namespace must not be empty")
        return value


# ---------------------------------------------------------------------------
# Request / Response models
# ---------------------------------------------------------------------------


class SyncPullRequest(BaseModel):
    """Request model for ``_cleveragents/sync/pull``."""

    model_config = ConfigDict(strict=False)

    namespace: str = ""
    entity_types: list[SyncEntityType] = []
    since: str | None = None
    force: bool = False

    @field_validator("namespace")
    @classmethod
    def _validate_namespace(cls, value: str) -> str:
        if value == "local":
            raise ValueError("Cannot sync the 'local' namespace")
        return value


class SyncPullResponse(BaseModel):
    """Response model for ``_cleveragents/sync/pull``."""

    model_config = ConfigDict(strict=False)

    entities: list[SyncEntitySnapshot] = []
    conflicts: list[SyncConflict] = []
    sync_state: SyncState | None = None
    pulled_at: str = ""
    total_pulled: int = 0


class SyncPushRequest(BaseModel):
    """Request model for ``_cleveragents/sync/push``."""

    model_config = ConfigDict(strict=False)

    namespace: str = ""
    entities: list[SyncEntitySnapshot] = []
    force: bool = False
    resolution: ConflictResolution = ConflictResolution.LAST_WRITER_WINS

    @field_validator("namespace")
    @classmethod
    def _validate_namespace(cls, value: str) -> str:
        if value == "local":
            raise ValueError("Cannot sync the 'local' namespace")
        return value


class SyncPushResponse(BaseModel):
    """Response model for ``_cleveragents/sync/push``."""

    model_config = ConfigDict(strict=False)

    accepted: list[SyncEntitySnapshot] = []
    rejected: list[SyncEntitySnapshot] = []
    conflicts: list[SyncConflict] = []
    sync_state: SyncState | None = None
    pushed_at: str = ""
    total_accepted: int = 0
    total_rejected: int = 0


class SyncStatusRequest(BaseModel):
    """Request model for ``_cleveragents/sync/status``."""

    model_config = ConfigDict(strict=False)

    namespace: str = ""
    entity_types: list[SyncEntityType] = []

    @field_validator("namespace")
    @classmethod
    def _validate_namespace(cls, value: str) -> str:
        if value == "local":
            raise ValueError("Cannot sync the 'local' namespace")
        return value


class SyncStatusResponse(BaseModel):
    """Response model for ``_cleveragents/sync/status``."""

    model_config = ConfigDict(strict=False)

    namespace: str = ""
    sync_state: SyncState | None = None
    drift_detected: bool = False
    local_ahead: list[SyncEntitySnapshot] = []
    server_ahead: list[SyncEntitySnapshot] = []
    conflicts: list[SyncConflict] = []
    checked_at: str = ""


# ---------------------------------------------------------------------------
# Offline queue entry
# ---------------------------------------------------------------------------


class SyncQueueEntry(BaseModel):
    """An offline-queued sync operation awaiting retry on reconnect."""

    model_config = ConfigDict(strict=False)

    queue_id: str
    direction: SyncDirection
    namespace: str
    entity_type: SyncEntityType
    entity_id: str
    snapshot: SyncEntitySnapshot
    status: SyncOperationStatus = SyncOperationStatus.QUEUED
    retry_count: int = 0
    max_retries: int = 5
    created_at: str = ""
    last_attempt_at: str | None = None
    error_message: str | None = None

    @field_validator("queue_id", "namespace", "entity_id")
    @classmethod
    def _must_be_non_empty(cls, value: str) -> str:
        if not value:
            raise ValueError("field must not be empty")
        return value


__all__ = [
    "ConflictResolution",
    "SyncConflict",
    "SyncDirection",
    "SyncEntitySnapshot",
    "SyncEntityType",
    "SyncOperationStatus",
    "SyncPullRequest",
    "SyncPullResponse",
    "SyncPushRequest",
    "SyncPushResponse",
    "SyncQueueEntry",
    "SyncState",
    "SyncStatusRequest",
    "SyncStatusResponse",
    "VectorClock",
]
