"""
Commit coordinator - manages sequential commits with conflict detection.

Ensures that commits happen in strict sequence order, even when executions
complete out of order. Detects conflicts and triggers re-execution with the
same seq number (not re-enqueue, just re-execute).
"""

import asyncio
import logging
from typing import Optional

from .context import ExecutionContext
from .store import VersionedStore
from .types import CommitIntent, ExecutionRecord, ExecutionState, Trigger

logger = logging.getLogger(__name__)


class CommitCoordinator:
    """
    Manages sequential commits with optimistic concurrency control.

    Key responsibilities:
    - Maintain strict sequential commit order (seq N+1 commits after seq N)
    - Detect conflicts between execution snapshot and committed state
    - Trigger re-execution (not re-enqueue) on conflicts with same seq
    - Track in-flight executions for debugging and monitoring
    """

    def __init__(self):
        self._stores: dict[str, VersionedStore] = {}
        self._current_seq = 0  # Highest committed seq across all operations
        self._next_commit_seq = 1  # Next seq we're waiting to commit
        self._pending_commits: dict[int, tuple[ExecutionRecord, list[CommitIntent]]] = {}
        self._execution_records: dict[int, ExecutionRecord] = {}
        self._lock = asyncio.Lock()  # Only for coordinator internal state, not stores

    def register_store(self, store: VersionedStore) -> None:
        """Register a versioned store with the coordinator"""
        self._stores[store.name] = store
        logger.info(f"Registered store: {store.name}")

    def get_store(self, name: str) -> Optional[VersionedStore]:
        """Get a registered store by name"""
        return self._stores.get(name)

    async def start_execution(self, seq: int, trigger: Trigger) -> ExecutionRecord:
        """
        Record that an execution is starting.

        Args:
            seq: Sequence number assigned to this execution
            trigger: The trigger being executed

        Returns:
            ExecutionRecord for tracking
        """
        async with self._lock:
            record = ExecutionRecord(
                seq=seq,
                trigger=trigger,
                state=ExecutionState.EXECUTING,
            )
            self._execution_records[seq] = record
            logger.info(f"Started execution: seq={seq}, trigger={trigger.name}")
            return record

    async def submit_for_commit(
        self,
        seq: int,
        commit_intents: list[CommitIntent],
    ) -> None:
        """
        Submit commit intents for sequential commit.

        The commit will only happen when:
        1. All prior seq numbers have committed
        2. No conflicts detected with committed state

        Args:
            seq: Sequence number of this execution
            commit_intents: List of changes to commit (empty if no changes)
        """
        async with self._lock:
            record = self._execution_records.get(seq)
            if not record:
                logger.error(f"No execution record found for seq={seq}")
                return

            record.state = ExecutionState.WAITING_COMMIT
            record.commit_intents = commit_intents
            self._pending_commits[seq] = (record, commit_intents)

            logger.info(
                f"Seq {seq} submitted for commit with {len(commit_intents)} intents"
            )

        # Try to process commits (this will handle sequential ordering)
        await self._process_commits()

    async def _process_commits(self) -> None:
        """
        Process pending commits in strict sequential order.

        Only commits seq N if seq N-1 has already committed.
        Detects conflicts and triggers re-execution with same seq.
        """
        while True:
            async with self._lock:
                # Check if next expected seq is ready to commit
                if self._next_commit_seq not in self._pending_commits:
                    # Waiting for this seq to complete execution
                    break

                seq = self._next_commit_seq
                record, intents = self._pending_commits[seq]

                logger.info(
                    f"Processing commit for seq={seq} (current_seq={self._current_seq})"
                )

                # Check for conflicts
                conflicts = self._check_conflicts(intents)

                if conflicts:
                    # Conflict detected - re-execute with same seq
                    logger.warning(
                        f"Seq {seq} has conflicts in stores: {conflicts}. Re-executing..."
                    )

                    # Remove from pending (will be re-added when execution completes)
                    del self._pending_commits[seq]

                    # Mark as evicted
                    record.state = ExecutionState.EVICTED
                    record.retry_count += 1

                    # Advance to next seq (this seq will be retried in background)
                    self._next_commit_seq += 1
                    self._current_seq += 1

                    # Trigger re-execution (outside lock)
                    asyncio.create_task(self._retry_execution(record))

                    continue

                # No conflicts - commit all intents atomically
                for intent in intents:
                    store = self._stores.get(intent.store_name)
                    if not store:
                        logger.error(
                            f"Seq {seq}: Store '{intent.store_name}' not found"
                        )
                        continue

                    store.commit(intent.new_data, seq)

                # Mark as committed
                record.state = ExecutionState.COMMITTED
                del self._pending_commits[seq]

                # Advance seq counters
                self._current_seq = seq
                self._next_commit_seq = seq + 1

                logger.info(
                    f"Committed seq={seq}, current_seq now {self._current_seq}"
                )

    def _check_conflicts(self, intents: list[CommitIntent]) -> list[str]:
        """
        Check if any commit intents conflict with current committed state.

        Args:
            intents: List of commit intents to check

        Returns:
            List of store names that have conflicts (empty if no conflicts)
        """
        conflicts = []

        for intent in intents:
            store = self._stores.get(intent.store_name)
            if not store:
                logger.error(f"Store '{intent.store_name}' not found during conflict check")
                continue

            if store.check_conflict(intent.expected_seq):
                conflicts.append(intent.store_name)

        return conflicts

    async def _retry_execution(self, record: ExecutionRecord) -> None:
        """
        Re-execute a trigger that had conflicts.

        Executes with the SAME seq number (not re-enqueued, just re-executed).
        This ensures the execution order remains deterministic.

        Args:
            record: Execution record to retry
        """
        from .context import ExecutionContext, set_execution_context, clear_execution_context

        logger.info(
            f"Retrying execution: seq={record.seq}, trigger={record.trigger.name}, "
            f"retry_count={record.retry_count}"
        )

        # Set execution context for retry
        ctx = ExecutionContext(
            seq=record.seq,
            trigger_name=record.trigger.name,
        )
        set_execution_context(ctx)

        try:
            # Re-execute trigger
            record.state = ExecutionState.EXECUTING
            commit_intents = await record.trigger.execute()

            # Submit for commit again (with same seq)
            await self.submit_for_commit(record.seq, commit_intents)

        except Exception as e:
            logger.error(
                f"Retry execution failed for seq={record.seq}: {e}", exc_info=True
            )
            record.state = ExecutionState.FAILED
            record.error = str(e)

            # Still need to advance past this seq
            async with self._lock:
                if record.seq == self._next_commit_seq:
                    self._next_commit_seq += 1
                    self._current_seq += 1

            # Try to process any pending commits
            await self._process_commits()

        finally:
            clear_execution_context()

    async def execution_failed(self, seq: int, error: Exception) -> None:
        """
        Mark an execution as failed.

        Args:
            seq: Sequence number that failed
            error: The exception that caused the failure
        """
        async with self._lock:
            record = self._execution_records.get(seq)
            if record:
                record.state = ExecutionState.FAILED
                record.error = str(error)

            # Remove from pending if present
            self._pending_commits.pop(seq, None)

            # If this is the next seq to commit, advance past it
            if seq == self._next_commit_seq:
                self._next_commit_seq += 1
                self._current_seq += 1

                logger.info(
                    f"Seq {seq} failed, advancing current_seq to {self._current_seq}"
                )

        # Try to process any pending commits
        await self._process_commits()

    def get_current_seq(self) -> int:
        """Get the current committed sequence number"""
        return self._current_seq

    def get_execution_record(self, seq: int) -> Optional[ExecutionRecord]:
        """Get execution record for a specific seq"""
        return self._execution_records.get(seq)

    def get_stats(self) -> dict:
        """Get statistics about the coordinator state"""
        state_counts = {}
        for record in self._execution_records.values():
            state_name = record.state.name
            state_counts[state_name] = state_counts.get(state_name, 0) + 1

        return {
            "current_seq": self._current_seq,
            "next_commit_seq": self._next_commit_seq,
            "pending_commits": len(self._pending_commits),
            "total_executions": len(self._execution_records),
            "state_counts": state_counts,
            "stores": {name: str(store) for name, store in self._stores.items()},
        }

    def __repr__(self) -> str:
        return (
            f"CommitCoordinator(current_seq={self._current_seq}, "
            f"pending={len(self._pending_commits)}, stores={len(self._stores)})"
        )
