"""
cortex/core/contradictions.py — Contradiction tracking for the Reconciliation Engine.

When the ReconciliationEngine issues a SUPERSEDE action, it records a
ContradictionRecord here so that callers can audit what was overwritten,
why, and by whom.

Design: dual-layer store.
  - In-memory list (``_records``) — fast, per-process scratch-pad for the
    current reconciliation batch.
  - SQLite persistence via ``cortex.core.contradiction_store`` — durable
    audit trail across sessions (written by ReconciliationEngine after each
    record() call).

ContradictionTracker itself stays synchronous (all existing callers and tests
are unaffected).  The DB-facing helpers live in contradiction_store.py and
are called by the engine, which is already async.

Additional async method on ContradictionTracker:
  - get_unresolved(storage, owner_id) → list[dict]   — DB query
  - get_from_db(storage, owner_id)    → list[dict]   — alias, full list

Public API
----------
    ContradictionRecord  — dataclass: claim_id, contradicts_id, resolution, timestamps
    ContradictionTracker — record(), list_pending(), resolve(), get_unresolved()
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import TYPE_CHECKING, List, Optional

if TYPE_CHECKING:
    from cortex.storage.adapter import StorageAdapter

logger = logging.getLogger(__name__)


def _now() -> str:
    """Return current UTC time as ISO-8601 string."""
    return datetime.now(timezone.utc).isoformat()


@dataclass
class ContradictionRecord:
    """A record of a detected contradiction between two claims.

    Created by ContradictionTracker.record() when the ReconciliationEngine
    issues a SUPERSEDE decision (the new claim contradicts / replaces an
    older one).

    Attributes:
        claim_id:       The new claim ID that superseded the old one.
        contradicts_id: The existing claim ID that was superseded.
        resolution:     Current resolution state:
                          "pending"        — detected, not yet reviewed
                          "superseded"     — automatically resolved by engine
                          "user_overridden"— human explicitly resolved
        detected_at:    ISO-8601 UTC timestamp when the contradiction was found.
        resolved_at:    ISO-8601 UTC timestamp when it was resolved (or None).
        reason:         Optional free-text description of why this was flagged.
    """

    claim_id: str
    contradicts_id: str
    resolution: str = "pending"
    detected_at: str = field(default_factory=_now)
    resolved_at: Optional[str] = None
    reason: str = ""

    def __post_init__(self) -> None:
        valid_resolutions = {"pending", "superseded", "user_overridden"}
        if self.resolution not in valid_resolutions:
            raise ValueError(
                f"Invalid resolution {self.resolution!r}. "
                f"Must be one of: {sorted(valid_resolutions)}"
            )


class ContradictionTracker:
    """Lightweight in-memory tracker for contradiction records.

    Instantiate once per ReconciliationEngine (or per session). Thread
    safety is NOT guaranteed — single-threaded use assumed (FastAPI async
    handlers are single-threaded per event loop).

    Example usage::

        tracker = ContradictionTracker()
        tracker.record("new-claim-abc", "old-claim-xyz", reason="CONTRADICTION heuristic")
        pending = tracker.list_pending()
        tracker.resolve("new-claim-abc", "superseded")
    """

    def __init__(self) -> None:
        self._records: List[ContradictionRecord] = []

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def record(
        self,
        claim_id: str,
        contradicts_id: str,
        resolution: str = "superseded",
        reason: str = "",
    ) -> ContradictionRecord:
        """Record a new contradiction between two claims.

        Idempotent: if a record for the same (claim_id, contradicts_id) pair
        already exists, the existing record is returned unchanged (no duplicate
        added).

        Args:
            claim_id:       The new/winning claim ID.
            contradicts_id: The existing/losing claim ID.
            resolution:     Initial resolution state (default "superseded").
            reason:         Optional description (e.g. heuristic rule name).

        Returns:
            The new (or existing) ContradictionRecord.
        """
        # Idempotency check
        existing = self._find(claim_id, contradicts_id)
        if existing is not None:
            logger.debug(
                "Contradiction already recorded: claim=%s contradicts=%s",
                claim_id, contradicts_id,
            )
            return existing

        rec = ContradictionRecord(
            claim_id=claim_id,
            contradicts_id=contradicts_id,
            resolution=resolution,
            reason=reason,
        )
        self._records.append(rec)
        logger.debug(
            "Contradiction recorded: claim=%s superseded claim=%s reason=%r",
            claim_id, contradicts_id, reason,
        )
        return rec

    def list_pending(self) -> List[ContradictionRecord]:
        """Return all contradiction records with resolution == 'pending'.

        Returns:
            List of ContradictionRecord objects, oldest first.
        """
        return [r for r in self._records if r.resolution == "pending"]

    def list_all(self) -> List[ContradictionRecord]:
        """Return all contradiction records (any resolution state).

        Returns:
            List of ContradictionRecord objects, insertion-ordered.
        """
        return list(self._records)

    def resolve(
        self,
        claim_id: str,
        resolution: str,
        contradicts_id: Optional[str] = None,
    ) -> Optional[ContradictionRecord]:
        """Mark a contradiction record as resolved.

        Finds the record by claim_id (and optionally contradicts_id) and
        updates its resolution + resolved_at timestamp.

        Args:
            claim_id:       The new/winning claim ID used when record() was called.
            resolution:     New resolution state ("superseded" or "user_overridden").
            contradicts_id: Optional — narrows the match when a claim_id has
                            multiple contradiction records.

        Returns:
            The updated ContradictionRecord, or None if not found.

        Raises:
            ValueError: If the resolution value is not a valid state.
        """
        valid = {"pending", "superseded", "user_overridden"}
        if resolution not in valid:
            raise ValueError(
                f"Invalid resolution {resolution!r}. Must be one of: {sorted(valid)}"
            )

        rec = self._find(claim_id, contradicts_id)
        if rec is None:
            logger.warning(
                "No contradiction record found for claim_id=%s contradicts_id=%s",
                claim_id, contradicts_id,
            )
            return None

        rec.resolution = resolution
        rec.resolved_at = _now()
        logger.debug(
            "Contradiction resolved: claim=%s resolution=%s", claim_id, resolution
        )
        return rec

    def clear(self) -> None:
        """Clear all records. Mainly for testing / session reset."""
        self._records.clear()

    def __len__(self) -> int:
        return len(self._records)

    # ------------------------------------------------------------------
    # Async DB-backed queries
    # ------------------------------------------------------------------

    async def get_unresolved(
        self,
        storage: "StorageAdapter",
        owner_id: str = "default",
    ) -> List[dict]:
        """Return all unresolved contradictions for *owner_id* from the DB.

        This method queries the ``contradictions`` table and is therefore
        async (it bypasses the in-memory list entirely).  Use it when you
        need durable, cross-session results rather than the current-batch
        in-memory list.

        Args:
            storage:  An open StorageAdapter instance (SQLiteAdapter).
            owner_id: Multi-tenant scope key.

        Returns:
            List of dicts with keys: id, claim_a_id, claim_b_id,
            detected_at, resolution, resolved_at, owner_id, reason.
            Ordered oldest-first.
        """
        from cortex.core.contradiction_store import get_unresolved as _db_get
        return await _db_get(storage, owner_id)

    async def get_from_db(
        self,
        storage: "StorageAdapter",
        owner_id: str = "default",
        resolved: Optional[bool] = None,
        limit: int = 100,
        offset: int = 0,
    ) -> List[dict]:
        """List contradictions from DB with optional resolved/unresolved filter.

        Args:
            storage:  An open StorageAdapter instance.
            owner_id: Multi-tenant scope key.
            resolved: True → resolved only; False → unresolved only; None → all.
            limit:    Max rows returned (default 100).
            offset:   Pagination offset (default 0).

        Returns:
            List of dicts ordered by detected_at DESC.
        """
        from cortex.core.contradiction_store import list_contradictions as _db_list
        return await _db_list(storage, owner_id, resolved=resolved, limit=limit, offset=offset)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _find(
        self,
        claim_id: str,
        contradicts_id: Optional[str] = None,
    ) -> Optional[ContradictionRecord]:
        """Find a record by claim_id (and optionally contradicts_id)."""
        for rec in self._records:
            if rec.claim_id == claim_id:
                if contradicts_id is None or rec.contradicts_id == contradicts_id:
                    return rec
        return None
