"""Contradiction detection for corporate memory knowledge items.

Architecture (see docs/ADR-corporate-memory-v1.md, Decision 4):

  - Topic / content matching is performed by a single Haiku call with strict
    structured output. The brittle SQL keyword pre-filter that used to live
    here was removed.
  - Domain remains a hard SQL conjunct (cheap, scales with corpus size).
  - The same Haiku call returns the structured resolution suggestion —
    contradiction detection and resolution arrive in one shot.

Public API:

  detect_and_record(extractor, new_item, repo)
      -> list[str] of contradiction IDs persisted to knowledge_contradictions.

  find_and_judge(extractor, new_item, repo)
      -> list[dict] of contradiction records (not yet persisted).

The ``check_contradiction`` and ``check_contradictions`` helpers are retained
for callers that want the low-level path (single Haiku call + persistence
disabled).
"""

import logging

from connectors.llm import StructuredExtractor
from connectors.llm.exceptions import LLMError
from src.repositories.knowledge import KnowledgeRepository

from .prompts import (
    BATCH_CONTRADICTION_PROMPT,
    BATCH_CONTRADICTION_SCHEMA,
    format_candidates_block,
)

logger = logging.getLogger(__name__)

# Hard cap on candidates per call — keeps prompt size bounded even if a
# domain accumulates a very large corpus. Above this, callers should shard.
DEFAULT_CANDIDATE_LIMIT = 100

# "merge" is the LLM-proposed action; the API resolution field uses "merged" — intentionally different terms
_VALID_ACTIONS = {"kept_a", "kept_b", "merge", "both_valid"}
_VALID_SEVERITIES = {"hard", "soft"}


def find_candidates(
    repo: KnowledgeRepository,
    new_item: dict,
    max_candidates: int = DEFAULT_CANDIDATE_LIMIT,
) -> list[dict]:
    """Load same-domain candidates for the LLM judge.

    Domain is the only SQL narrowing applied. If the new item has no domain,
    candidates from all domains are returned (the LLM still judges each).
    """
    item_id = new_item.get("id", "")
    return repo.find_contradiction_candidates(
        new_item_id=item_id,
        domain=new_item.get("domain"),
        limit=max_candidates,
    )


def _build_resolution(judgment: dict) -> dict | None:
    """Project the flat resolution_* fields into a nested resolution dict.

    Returns None when the judgment is not a contradiction or the LLM did not
    populate resolution fields.
    """
    action = judgment.get("resolution_action")
    if not judgment.get("is_contradiction") or not action:
        return None
    if action not in _VALID_ACTIONS:
        logger.warning("Invalid resolution_action from LLM: %r — dropping", action)
        return None
    resolution: dict = {
        "action": action,
        "justification": judgment.get("resolution_justification") or "",
    }
    if action == "merge":
        merged = judgment.get("resolution_merged_content")
        if merged:
            resolution["merged_content"] = merged
    return resolution


def _judgment_to_record(judgment: dict, new_item_id: str) -> dict:
    """Convert one Haiku judgment row into the contradiction record shape."""
    severity = judgment.get("severity")
    if severity not in _VALID_SEVERITIES:
        severity = None
    record: dict = {
        "item_a_id": new_item_id,
        "item_b_id": judgment["candidate_id"],
        "explanation": judgment.get("explanation", ""),
        "severity": severity,
    }
    resolution = _build_resolution(judgment)
    if resolution is not None:
        record["suggested_resolution"] = resolution
    return record


def find_and_judge(
    extractor: StructuredExtractor,
    new_item: dict,
    repo: KnowledgeRepository,
    max_candidates: int = DEFAULT_CANDIDATE_LIMIT,
) -> list[dict]:
    """Topic + judgment + resolution in one Haiku call.

    Loads same-domain candidates from SQL, then asks Haiku to judge each one
    in a single batched structured-output call. Returns contradiction records
    ready for persistence (item_a_id, item_b_id, explanation, severity,
    optional suggested_resolution dict).

    Empty corpus → returns immediately without an LLM call (cost guard).
    Hallucinated candidate_ids returned by Haiku are dropped.
    """
    candidates = find_candidates(repo, new_item, max_candidates)
    if not candidates:
        return []

    if len(candidates) > 50:
        logger.warning(
            "Contradiction candidate batch is large (%d items); prompt may exceed model context window. "
            "Consider domain sharding (V2 TODO).",
            len(candidates),
        )

    prompt = BATCH_CONTRADICTION_PROMPT.format(
        new_id=new_item.get("id", ""),
        new_domain=new_item.get("domain") or "unspecified",
        new_title=new_item.get("title", ""),
        new_content=new_item.get("content", ""),
        candidates_block=format_candidates_block(candidates),
    )

    try:
        result = extractor.extract_json(
            prompt=prompt,
            max_tokens=4096,
            json_schema=BATCH_CONTRADICTION_SCHEMA,
            schema_name="batch_contradiction",
        )
    except LLMError as e:
        logger.error("Batch contradiction judgment failed: %s", e)
        return []

    judgments = result.get("judgments", [])
    valid_ids = {c["id"] for c in candidates}
    new_item_id = new_item.get("id", "")
    records: list[dict] = []
    for j in judgments:
        cid = j.get("candidate_id")
        if cid not in valid_ids:
            logger.warning(
                "Dropping judgment for unknown candidate_id %r (not in input set)",
                cid,
            )
            continue
        if not j.get("is_contradiction"):
            continue
        records.append(_judgment_to_record(j, new_item_id))
    return records


def detect_and_record(
    extractor: StructuredExtractor,
    new_item: dict,
    repo: KnowledgeRepository,
    max_candidates: int = DEFAULT_CANDIDATE_LIMIT,
) -> list[str]:
    """Run find_and_judge and persist the resulting contradictions.

    Returns the list of newly created contradiction IDs.
    """
    records = find_and_judge(extractor, new_item, repo, max_candidates)
    contradiction_ids: list[str] = []
    for r in records:
        cid = repo.create_contradiction(
            item_a_id=r["item_a_id"],
            item_b_id=r["item_b_id"],
            explanation=r["explanation"],
            severity=r.get("severity"),
            suggested_resolution=r.get("suggested_resolution"),
        )
        contradiction_ids.append(cid)
        logger.info(
            "Contradiction recorded: %s vs %s (severity=%s)",
            r["item_a_id"],
            r["item_b_id"],
            r.get("severity"),
        )
    return contradiction_ids


# ---------------------------------------------------------------------------
# Legacy single-pair helpers — kept so callers that want a one-off pairwise
# judgment (e.g. tests, ad-hoc tooling) can still use them. NOT used by the
# detector pipeline.
# ---------------------------------------------------------------------------


def check_contradiction(
    extractor: StructuredExtractor,
    item_a: dict,
    item_b: dict,
) -> dict:
    """Single-pair judgment via the batched API. Always returns a dict with
    ``contradicts`` (bool), ``explanation`` (str), and optionally
    ``severity`` and ``suggested_resolution`` (dict).
    """
    prompt = BATCH_CONTRADICTION_PROMPT.format(
        new_id=item_a.get("id", "new"),
        new_domain=item_a.get("domain") or "unspecified",
        new_title=item_a.get("title", ""),
        new_content=item_a.get("content", ""),
        candidates_block=format_candidates_block([item_b]),
    )
    try:
        result = extractor.extract_json(
            prompt=prompt,
            max_tokens=1024,
            json_schema=BATCH_CONTRADICTION_SCHEMA,
            schema_name="batch_contradiction",
        )
    except LLMError as e:
        logger.error("Pair contradiction check failed: %s", e)
        return {"contradicts": False, "explanation": f"Check failed: {e}"}
    judgments = result.get("judgments", [])
    if not judgments:
        return {"contradicts": False, "explanation": "No judgment returned"}
    j = judgments[0]
    out: dict = {
        "contradicts": bool(j.get("is_contradiction")),
        "explanation": j.get("explanation", ""),
    }
    severity = j.get("severity")
    if severity in _VALID_SEVERITIES:
        out["severity"] = severity
    resolution = _build_resolution(j)
    if resolution is not None:
        out["suggested_resolution"] = resolution
    return out


def check_contradictions(
    extractor: StructuredExtractor,
    new_item: dict,
    repo: KnowledgeRepository,
    max_candidates: int = DEFAULT_CANDIDATE_LIMIT,
) -> list[dict]:
    """Backwards-compatible alias for find_and_judge."""
    return find_and_judge(extractor, new_item, repo, max_candidates)
