"""
cortex/core/contradiction_store.py — Async CRUD for the ``contradictions`` table.

Follows the same pattern as ``cortex/commitments/store.py``:
  - Pure async functions that accept a StorageAdapter instance.
  - All writes serialised through ``storage._write_lock``.
  - Parameterised queries only (no string interpolation of user data).

Public API
----------
    insert_contradiction(storage, claim_a_id, claim_b_id, owner_id, ...) -> int
    find_pair(storage, claim_a_id, claim_b_id, owner_id)               -> Optional[dict]
    get_unresolved(storage, owner_id)                                   -> list[dict]
    mark_resolved(storage, contradiction_id, resolution, resolved_at)  -> bool
    list_contradictions(storage, owner_id, resolved)                   -> list[dict]
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import Any, List, Optional

logger = logging.getLogger(__name__)

# Valid resolution values (mirrors the API/issue spec)
VALID_RESOLUTIONS = frozenset({"kept_a", "kept_b", "merged"})


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


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


def _row_to_dict(row: Any) -> dict:
    """Convert an aiosqlite Row (or dict-like) to a plain dict."""
    return dict(row)


# ---------------------------------------------------------------------------
# Write operations
# ---------------------------------------------------------------------------


async def insert_contradiction(
    storage: Any,
    claim_a_id: str,
    claim_b_id: str,
    owner_id: str = "default",
    reason: str = "",
    resolution: Optional[str] = None,
    detected_at: Optional[str] = None,
) -> int:
    """Insert a new contradiction record and return its auto-generated id.

    Idempotent: if a row for the same (claim_a_id, claim_b_id, owner_id)
    triple already exists, the existing row's id is returned unchanged.

    Args:
        storage:     StorageAdapter instance with ``_conn`` and ``_write_lock``.
        claim_a_id:  The new/winning claim ID.
        claim_b_id:  The old/superseded claim ID.
        owner_id:    Multi-tenant scope key.
        reason:      Free-text description of why the contradiction was flagged.
        resolution:  Optional initial resolution (NULL = unresolved).
        detected_at: ISO-8601 timestamp (defaults to now).

    Returns:
        The ``id`` (INTEGER) of the new or existing row.

    Raises:
        ValueError: If ``resolution`` is provided but not a valid value.
    """
    if resolution is not None and resolution not in VALID_RESOLUTIONS:
        raise ValueError(
            f"Invalid resolution {resolution!r}. "
            f"Must be one of: {sorted(VALID_RESOLUTIONS)} or None."
        )

    detected_at = detected_at or _now()

    # Idempotency: return existing row if pair already recorded
    existing = await find_pair(storage, claim_a_id, claim_b_id, owner_id)
    if existing is not None:
        logger.debug(
            "Contradiction already persisted for pair (%s, %s) owner=%s → id=%d",
            claim_a_id, claim_b_id, owner_id, existing["id"],
        )
        return existing["id"]

    async with storage._write_lock:
        conn = await storage._get_conn()
        async with conn.execute(
            """
            INSERT INTO contradictions
                (claim_a_id, claim_b_id, owner_id, reason, resolution, detected_at)
            VALUES (?, ?, ?, ?, ?, ?)
            """,
            (claim_a_id, claim_b_id, owner_id, reason, resolution, detected_at),
        ):
            pass
        await conn.commit()
        async with conn.execute("SELECT last_insert_rowid() AS id") as cur:
            row = await cur.fetchone()
    new_id = row["id"]
    logger.debug(
        "Contradiction persisted: id=%d pair=(%s, %s) owner=%s resolution=%s",
        new_id, claim_a_id, claim_b_id, owner_id, resolution,
    )
    return new_id


async def mark_resolved(
    storage: Any,
    contradiction_id: int,
    resolution: str,
    resolved_at: Optional[str] = None,
) -> bool:
    """Mark a contradiction row as resolved.

    Args:
        storage:         StorageAdapter instance.
        contradiction_id: Primary key of the row to update.
        resolution:      One of 'kept_a', 'kept_b', 'merged'.
        resolved_at:     ISO-8601 timestamp (defaults to now).

    Returns:
        True if the row was found and updated; False if not found.

    Raises:
        ValueError: If ``resolution`` is not a valid value.
    """
    if resolution not in VALID_RESOLUTIONS:
        raise ValueError(
            f"Invalid resolution {resolution!r}. "
            f"Must be one of: {sorted(VALID_RESOLUTIONS)}."
        )

    resolved_at = resolved_at or _now()

    async with storage._write_lock:
        conn = await storage._get_conn()
        async with conn.execute(
            """
            UPDATE contradictions
               SET resolution = ?, resolved_at = ?
             WHERE id = ?
            """,
            (resolution, resolved_at, contradiction_id),
        ) as cur:
            updated = cur.rowcount
        await conn.commit()

    found = updated > 0
    if not found:
        logger.warning("mark_resolved: contradiction id=%d not found.", contradiction_id)
    else:
        logger.debug(
            "Contradiction id=%d resolved: resolution=%s resolved_at=%s",
            contradiction_id, resolution, resolved_at,
        )
    return found


# ---------------------------------------------------------------------------
# Read operations
# ---------------------------------------------------------------------------


async def find_pair(
    storage: Any,
    claim_a_id: str,
    claim_b_id: str,
    owner_id: str = "default",
) -> Optional[dict]:
    """Return the first contradiction row for (claim_a_id, claim_b_id, owner_id), or None.

    Args:
        storage:    StorageAdapter instance.
        claim_a_id: The new/winning claim ID.
        claim_b_id: The old/superseded claim ID.
        owner_id:   Multi-tenant scope key.

    Returns:
        A dict of column → value, or None if not found.
    """
    conn = await storage._get_conn()
    async with conn.execute(
        """
        SELECT id, claim_a_id, claim_b_id, detected_at, resolution, resolved_at,
               owner_id, reason
          FROM contradictions
         WHERE claim_a_id = ? AND claim_b_id = ? AND owner_id = ?
         LIMIT 1
        """,
        (claim_a_id, claim_b_id, owner_id),
    ) as cur:
        row = await cur.fetchone()
    return _row_to_dict(row) if row else None


async def get_unresolved(
    storage: Any,
    owner_id: str = "default",
) -> List[dict]:
    """Return all unresolved (resolution IS NULL) contradictions for *owner_id*.

    Ordered by ``detected_at`` ascending (oldest first).

    Args:
        storage:  StorageAdapter instance.
        owner_id: Multi-tenant scope key.

    Returns:
        List of dicts, each with keys:
        id, claim_a_id, claim_b_id, detected_at, resolution,
        resolved_at, owner_id, reason.
    """
    conn = await storage._get_conn()
    async with conn.execute(
        """
        SELECT id, claim_a_id, claim_b_id, detected_at, resolution, resolved_at,
               owner_id, reason
          FROM contradictions
         WHERE owner_id = ? AND resolution IS NULL
         ORDER BY detected_at ASC
        """,
        (owner_id,),
    ) as cur:
        rows = await cur.fetchall()
    return [_row_to_dict(r) for r in rows]


async def list_contradictions(
    storage: Any,
    owner_id: str = "default",
    resolved: Optional[bool] = None,
    limit: int = 100,
    offset: int = 0,
) -> List[dict]:
    """List contradictions for *owner_id*, with optional resolved/unresolved filter.

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

    Returns:
        List of dicts ordered by detected_at DESC.
    """
    base_sql = """
        SELECT id, claim_a_id, claim_b_id, detected_at, resolution, resolved_at,
               owner_id, reason
          FROM contradictions
         WHERE owner_id = ?
    """
    params: list = [owner_id]

    if resolved is True:
        base_sql += " AND resolution IS NOT NULL"
    elif resolved is False:
        base_sql += " AND resolution IS NULL"

    base_sql += " ORDER BY detected_at DESC LIMIT ? OFFSET ?"
    params.extend([limit, offset])

    conn = await storage._get_conn()
    async with conn.execute(base_sql, params) as cur:
        rows = await cur.fetchall()
    return [_row_to_dict(r) for r in rows]
