"""Claim CRUD operations mixin."""
from __future__ import annotations

from typing import Any, Dict, List, Optional

from cortex.storage._helpers import _now, _row_to_claim, _row_to_episode
from cortex.validation import validate_claim_fields
from cortex.types import EpisodeEvent, SemanticClaim


class ClaimCRUDMixin:
    """Mixin providing claim create/read/update and episode logging."""

    async def log_episode(
        self,
        session_id: str,
        actor_id: str,
        kind: str,
        raw_text: str,
        owner_id: str = "default",
        **kwargs,
    ) -> EpisodeEvent:
        async with self._write_lock:
            conn = await self._get_conn()
            now = _now()
            ts = kwargs.get("ts", now)
            tool_trace_ref = kwargs.get("tool_trace_ref")
            source_uri = kwargs.get("source_uri")
            metadata_json = kwargs.get("metadata_json")

            # session_id here is the session_record.id (PK), not session_id column
            async with conn.execute(
                """
                INSERT INTO episode_event
                    (owner_id, session_id, actor_id, ts, kind,
                     raw_text, tool_trace_ref, source_uri, metadata_json, created_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                RETURNING *
                """,
                (owner_id, session_id, actor_id, ts, kind,
                 raw_text, tool_trace_ref, source_uri, metadata_json, now),
            ) as cur:
                row = await cur.fetchone()
            await conn.commit()

            # Sync to FTS
            event = _row_to_episode(row)
            await conn.execute(
                "INSERT INTO fts_episodes(event_id, content) VALUES (?, ?)",
                (event.id, raw_text or ""),
            )
            await conn.commit()
            return event

    async def create_claim(
        self,
        subject: str,
        predicate: str,
        object_value: str,
        owner_id: str = "default",
        **kwargs,
    ) -> SemanticClaim:
        """Insert a new semantic claim triple, update FTS5, and return the created SemanticClaim."""
        # Validate and sanitize all string inputs
        subject, predicate, object_value = validate_claim_fields(
            subject, predicate, object_value
        )

        conn = await self._get_conn()
        now = _now()

        cols = {
            "owner_id": owner_id,
            "subject": subject,
            "predicate": predicate,
            "object_value": object_value,
            "memory_type": kwargs.get("memory_type", "session"),
            "subject_entity_id": kwargs.get("subject_entity_id"),
            "object_entity_id": kwargs.get("object_entity_id"),
            "datatype": kwargs.get("datatype", "text"),
            # Default confidence matches CortexConfig.session_confidence_default.
            # The reconciliation engine passes the config-driven value explicitly;
            # this fallback is for direct storage calls that bypass reconciliation.
            "confidence": kwargs.get("confidence", 0.5),
            "sensitivity": kwargs.get("sensitivity", "normal"),
            "category": kwargs.get("category", "misc"),
            # v8 envelope fields: intrinsic_type and human_family (#355/#357)
            # Only included when caller provides them (None → column stays NULL).
            **({
                "intrinsic_type": kwargs["intrinsic_type"],
            } if "intrinsic_type" in kwargs else {}),
            **({
                "human_family": kwargs["human_family"],
            } if "human_family" in kwargs else {}),
            "scope": kwargs.get("scope", "user"),
            "valid_from": kwargs.get("valid_from"),
            "valid_to": kwargs.get("valid_to"),
            "status": kwargs.get("status", "active"),
            # LoCoMo fields (v10, #516)
            **({
                "memory_class": kwargs["memory_class"],
            } if "memory_class" in kwargs else {}),
            **({
                "event_date": kwargs["event_date"],
            } if "event_date" in kwargs else {}),
            **({
                "sequence_order": kwargs["sequence_order"],
            } if "sequence_order" in kwargs else {}),
            **({
                "salience": kwargs["salience"],
            } if "salience" in kwargs else {}),
            # Causal linking (#518)
            **({
                "cause": kwargs["cause"],
            } if "cause" in kwargs else {}),
            **({
                "effect": kwargs["effect"],
            } if "effect" in kwargs else {}),
            # State transitions (#519)
            **({
                "state_before": kwargs["state_before"],
            } if "state_before" in kwargs else {}),
            **({
                "state_after": kwargs["state_after"],
            } if "state_after" in kwargs else {}),
            # Temporal fields (#573, v12)
            **({
                "temporal": kwargs["temporal"],
            } if "temporal" in kwargs else {}),
            **({
                "temporal_marker": kwargs["temporal_marker"],
            } if "temporal_marker" in kwargs else {}),
            # Explicitness + stability (#609, v13)
            **({
                "explicitness": kwargs["explicitness"],
            } if "explicitness" in kwargs else {}),
            **({
                "stability": kwargs["stability"],
            } if "stability" in kwargs else {}),
            # Review queue (#v14)
            **({
                "flag_reason": kwargs["flag_reason"],
            } if "flag_reason" in kwargs else {}),
            "source_session": kwargs.get("source_session"),
            # Provenance metadata (issue #748)
            **({
                "metadata_json": kwargs["metadata_json"],
            } if "metadata_json" in kwargs else {}),
            # Mentions counter (#924, v21)
            "mentions": kwargs.get("mentions", 1),
            "last_mentioned_at": kwargs.get("last_mentioned_at", now),
            # Entity scoping (#923)
            **({
                "entity_id": kwargs["entity_id"],
            } if "entity_id" in kwargs else {}),
            **({
                "entity_type": kwargs["entity_type"],
            } if "entity_type" in kwargs else {}),
            # Source agent (#1396)
            **({
                "source_agent": kwargs["source_agent"],
            } if "source_agent" in kwargs else {}),
            "created_at": now,
            "updated_at": now,
        }

        col_names = ", ".join(cols.keys())
        placeholders = ", ".join("?" for _ in cols)
        values = list(cols.values())

        async with self._write_lock:
            async with conn.execute(
                f"INSERT INTO semantic_claim ({col_names}) VALUES ({placeholders}) RETURNING *",
                values,
            ) as cur:
                row = await cur.fetchone()
            await conn.commit()

            claim = _row_to_claim(row)

            # Sync to FTS
            fts_content = f"{subject} {predicate} {object_value}"
            await conn.execute(
                "INSERT INTO fts_claims(claim_id, content) VALUES (?, ?)",
                (claim.id, fts_content),
            )
            await conn.commit()
        return claim

    async def get_claim(
        self,
        claim_id: str,
        owner_id: Optional[str] = None,
        include_deleted: bool = False,
    ) -> Optional[SemanticClaim]:
        """Return a SemanticClaim by primary-key id, or None if not found.

        By default, soft-deleted claims (is_deleted=1) are excluded.
        Pass ``include_deleted=True`` to retrieve them (e.g. for restore).
        """
        conn = await self._get_conn()
        soft_filter = "" if include_deleted else " AND (is_deleted = 0 OR is_deleted IS NULL)"
        if owner_id is None:
            async with conn.execute(
                f"SELECT * FROM semantic_claim WHERE id = ?{soft_filter}", (claim_id,)
            ) as cur:
                row = await cur.fetchone()
        else:
            async with conn.execute(
                f"SELECT * FROM semantic_claim WHERE id = ? AND owner_id = ?{soft_filter}",
                (claim_id, owner_id),
            ) as cur:
                row = await cur.fetchone()
        return _row_to_claim(row) if row else None

    async def get_claims_batch(
        self, claim_ids: List[str],
    ) -> Dict[str, SemanticClaim]:
        """Return {claim_id: SemanticClaim} for the given IDs in one query.

        Soft-deleted claims (is_deleted=1) are excluded.
        """
        if not claim_ids:
            return {}
        conn = await self._get_conn()
        placeholders = ",".join("?" * len(claim_ids))
        async with conn.execute(
            f"SELECT * FROM semantic_claim WHERE id IN ({placeholders})"
            f" AND (is_deleted = 0 OR is_deleted IS NULL)",
            claim_ids,
        ) as cur:
            rows = await cur.fetchall()
        return {row["id"]: _row_to_claim(row) for row in rows}

    async def get_claims_sensitivity_batch(
        self, claim_ids: List[str],
    ) -> Dict[str, str]:
        """Return {claim_id: sensitivity} for the given IDs in one query."""
        if not claim_ids:
            return {}
        conn = await self._get_conn()
        placeholders = ",".join("?" * len(claim_ids))
        async with conn.execute(
            f"SELECT id, sensitivity FROM semantic_claim WHERE id IN ({placeholders})",
            claim_ids,
        ) as cur:
            rows = await cur.fetchall()
        return {row["id"]: row["sensitivity"] for row in rows}

    async def update_claim(
        self,
        claim_id: str,
        owner_id: Optional[str] = None,
        **kwargs,
    ) -> SemanticClaim:
        """Patch allowed columns on a claim by id and return the updated SemanticClaim."""
        _allowed = {
            "memory_type", "confidence", "sensitivity", "scope",
            "valid_from", "valid_to", "status", "superseded_by",
            "last_accessed", "access_count", "subject_entity_id",
            "object_entity_id", "object_value", "category",
            "flag_reason", "mentions", "last_mentioned_at",
            "is_deleted", "deleted_at",
        }
        updates = {k: v for k, v in kwargs.items() if k in _allowed}
        if not updates:
            raise ValueError(f"No valid columns. Allowed: {_allowed}")

        updates["updated_at"] = _now()
        conn = await self._get_conn()
        set_clause = ", ".join(f"{col} = ?" for col in updates)

        async with self._write_lock:
            if owner_id is None:
                values = list(updates.values()) + [claim_id]
                await conn.execute(
                    f"UPDATE semantic_claim SET {set_clause} WHERE id = ?",
                    values,
                )
            else:
                values = list(updates.values()) + [claim_id, owner_id]
                await conn.execute(
                    f"UPDATE semantic_claim SET {set_clause} WHERE id = ? AND owner_id = ?",
                    values,
                )
            await conn.commit()
            claim = await self.get_claim(claim_id, owner_id=owner_id)

            # Sync FTS: re-build content from current claim state
            if claim is not None and "object_value" in updates:
                fts_content = f"{claim.subject} {claim.predicate} {claim.object_value}"
                await conn.execute(
                    "INSERT OR REPLACE INTO fts_claims(claim_id, content) VALUES (?, ?)",
                    (claim_id, fts_content),
                )
                await conn.commit()

        return claim  # type: ignore[return-value]

    async def increment_claim_mentions(
        self,
        claim_id: str,
        owner_id: Optional[str] = None,
    ) -> None:
        """Atomically increment mentions counter and update last_mentioned_at (#924)."""
        conn = await self._get_conn()
        now = _now()
        async with self._write_lock:
            if owner_id is None:
                await conn.execute(
                    """UPDATE semantic_claim
                       SET mentions = COALESCE(mentions, 0) + 1,
                           last_mentioned_at = ?,
                           updated_at = ?
                       WHERE id = ?""",
                    (now, now, claim_id),
                )
            else:
                await conn.execute(
                    """UPDATE semantic_claim
                       SET mentions = COALESCE(mentions, 0) + 1,
                           last_mentioned_at = ?,
                           updated_at = ?
                       WHERE id = ? AND owner_id = ?""",
                    (now, now, claim_id, owner_id),
                )
            await conn.commit()

    async def get_claims_by_subject(
        self, subject: str, owner_id: str = "default"
    ) -> List[SemanticClaim]:
        """Return all claims whose subject matches a string within an owner scope."""
        conn = await self._get_conn()
        async with conn.execute(
            """
            SELECT * FROM semantic_claim
            WHERE owner_id = ? AND subject = ? AND status = 'active'
              AND (is_deleted = 0 OR is_deleted IS NULL)
            ORDER BY confidence DESC
            """,
            (owner_id, subject),
        ) as cur:
            rows = await cur.fetchall()
        return [_row_to_claim(r) for r in rows]
