"""Byte Repository Intelligence over a local repository.

Three stores live under ``<root>/.zyte``: ``info_graph.json`` (structure),
``chat_action_graph.json`` (session weights) and ``context-store.json``
(durable notes). Legacy retrieval entry points fail closed with well-shaped
empty results. The typed intelligence operations validate their inputs and
surface errors to callers instead of disguising incomplete evidence.
"""

from __future__ import annotations

import os
from typing import Any

from . import answers as _answers
from . import facts as _facts
from . import graph as _graph
from . import notes as _notes
from . import rank as _rank
from . import repository_intelligence as _intelligence
from . import store as _store
from . import weights as _weights

__all__ = [
    "answer_lookup",
    "answer_record",
    "change_analyze",
    "claim_check",
    "claim_create",
    "fact",
    "knowledge_events",
    "knowledge_query",
    "knowledge_record_answer",
    "neighbors",
    "obligation_list",
    "obligation_resolve",
    "prime",
    "probe_get",
    "probe_plan",
    "probe_run",
    "read",
    "register_edits",
    "register_reads",
    "remember",
    "retrieve",
    "scan",
    "snapshot_capture",
    "status",
    "symbol_at",
    "typescript_boundary",
]

_EMPTY_SCAN = {"files": 0, "symbols": 0, "edges": 0, "elapsed_ms": 0, "skipped": False, "reused": 0}


def scan(root: str | os.PathLike[str], force: bool = False) -> dict[str, Any]:
    """Rebuild the structural graph. Skips the walk when the graph is already fresh."""
    try:
        return _graph.scan(_store.normalize_root(root), force=bool(force))
    except Exception:
        return dict(_EMPTY_SCAN)


def answer_record(
    root: str | os.PathLike[str],
    question: str,
    answer: str,
    evidence: list[dict[str, Any]],
) -> dict[str, Any]:
    """Store a verified answer with hashed evidence spans. Never raises."""
    try:
        return {"id": _answers.record(_store.normalize_root(root), question, answer, list(evidence or []))}
    except Exception:
        return {"id": ""}


def answer_lookup(root: str | os.PathLike[str], question: str) -> dict[str, Any]:
    """Best still-valid prior answer for ``question``, or a miss. Never raises."""
    try:
        return _answers.lookup(_store.normalize_root(root), question)
    except Exception:
        return {
            "hit": False,
            "id": "",
            "question": "",
            "answer": "",
            "age_days": 0.0,
            "similarity": 0.0,
            "evidence": [],
        }


def fact(root: str | os.PathLike[str], query: str, max_chars: int = 1200) -> dict[str, Any]:
    """Extract a direct answer for a single-fact question, or miss cleanly.

    A hit registers a read against the answering location so session
    weighting keeps learning even when no pack is retrieved.
    """
    try:
        root_path = _store.normalize_root(root)
        result = _facts.fact(root_path, query, max_chars=max(0, int(max_chars)))
        if result.get("facts"):
            try:
                agent = _weights._agent_id(None)
                events = [
                    (item["path"], item.get("symbol") or None, agent)
                    for item in result["facts"]
                    if isinstance(item.get("path"), str)
                ]
                _weights.register(root_path, events, "read")
            except Exception:
                pass
        return result
    except Exception:
        return {"facts": [], "confidence": "none", "candidates": []}


def status(root: str | os.PathLike[str]) -> dict[str, Any]:
    """Report on the persisted graph without building one."""
    try:
        loaded = _graph.load(_store.normalize_root(root))
        if loaded is None:
            return {"has_graph": False, "stale": True, "files": 0, "symbols": 0, "generated_at": None}
        return {
            "has_graph": True,
            "stale": loaded.stale(),
            "files": loaded.file_count,
            "symbols": loaded.symbol_count,
            "generated_at": loaded.generated_at,
        }
    except Exception:
        return {"has_graph": False, "stale": True, "files": 0, "symbols": 0, "generated_at": None}


def retrieve(
    root: str | os.PathLike[str],
    query: str,
    budget_chars: int = 18000,
    max_read_chars: int = 4000,
    limit: int = 12,
    adaptive: bool = True,
    skeleton: bool = True,
) -> dict[str, Any]:
    """Rank the repository against ``query`` and pack context up to ``budget_chars``.

    ``budget_chars`` is a ceiling, not a target: with ``adaptive`` on (the
    default), weak evidence shrinks the effective budget and packing stops
    once the strong candidates are in. ``adaptive=False`` restores plain
    fill-to-budget behaviour.
    """
    try:
        return _rank.retrieve(
            _store.normalize_root(root),
            query,
            budget_chars=max(0, int(budget_chars)),
            max_read_chars=max(0, int(max_read_chars)),
            limit=max(0, int(limit)),
            adaptive=bool(adaptive),
            skeleton=bool(skeleton),
        )
    except Exception:
        return {"items": [], "confidence": "low", "used_chars": 0, "stale": False, "budget_chars": 0}


def read(root: str | os.PathLike[str], ref: str, max_chars: int = 4000) -> dict[str, Any]:
    """Read a whole file or a single symbol addressed as ``path`` or ``path::symbol``."""
    root_path = _store.normalize_root(root)
    empty = {"path": "", "symbol": None, "lines": [0, 0], "text": "", "truncated": False}
    try:
        rel, symbol = _store.parse_ref(root_path, ref)
        if not rel:
            return empty
        limit = max(0, int(max_chars))
        loaded = _graph.ensure(root_path)
        lines = loaded.source(rel) if loaded is not None else None
        if lines is None:
            text = _graph.read_text(root_path.joinpath(*rel.split("/")))
            if text is None:
                return {**empty, "path": rel, "symbol": symbol}
            lines = text.split("\n")

        start, end = 1, len(lines)
        if symbol is not None and loaded is not None:
            entry = loaded.symbol(rel, symbol)
            if entry is not None:
                start = max(1, int(entry["line"]))
                end = min(len(lines), int(entry["end_line"]))
        body = "\n".join(lines[start - 1 : end])
        truncated = len(body) > limit
        body = body[:limit]
        return {
            "path": rel,
            "symbol": symbol,
            "lines": [start, start + body.count("\n")],
            "text": body,
            "truncated": truncated,
        }
    except Exception:
        return empty


def neighbors(root: str | os.PathLike[str], ref: str, depth: int = 1, limit: int = 20) -> dict[str, Any]:
    """Walk the import and containment edges around ``ref``."""
    try:
        root_path = _store.normalize_root(root)
        loaded = _graph.ensure(root_path)
        if loaded is None:
            return {"items": []}
        rel, symbol = _store.parse_ref(root_path, ref)
        if not rel:
            return {"items": []}
        cap = max(0, int(limit))
        hops = max(1, int(depth))
        items: list[dict[str, Any]] = []
        seen: set[tuple[str, str | None]] = {(rel, symbol)}

        def add(path: str, name: str | None, relation: str) -> None:
            key = (path, name)
            if key in seen or len(items) >= cap:
                return
            seen.add(key)
            items.append({"path": path, "symbol": name, "relation": relation})

        record = loaded.files.get(rel)
        if symbol is not None:
            add(rel, None, "contained_by")
            if record is not None:
                for entry in record["symbols"]:
                    if entry.get("name") != symbol:
                        add(rel, entry.get("name"), "sibling")
        elif record is not None:
            for entry in record["symbols"]:
                add(rel, entry.get("name"), "contains")

        frontier = {rel}
        visited = {rel}
        for hop in range(1, hops + 1):
            suffix = "" if hop == 1 else f"@{hop}"
            following: set[str] = set()
            for current in sorted(frontier):
                for target in sorted(loaded.imports.get(current, set())):
                    if target not in visited:
                        following.add(target)
                        add(target, None, "imports" + suffix)
                for source in sorted(loaded.imported_by.get(current, set())):
                    if source not in visited:
                        following.add(source)
                        add(source, None, "imported_by" + suffix)
                for target in sorted(loaded.calls.get(current, set())):
                    if target not in visited:
                        following.add(target)
                        add(target, None, "calls" + suffix)
                for source in sorted(loaded.called_by.get(current, set())):
                    if source not in visited:
                        following.add(source)
                        add(source, None, "called_by" + suffix)
            visited |= following
            frontier = following
            if not frontier or len(items) >= cap:
                break
        return {"items": items[:cap]}
    except Exception:
        return {"items": []}


def symbol_at(root: str | os.PathLike[str], path: str, line: int) -> str | None:
    """Name of the innermost symbol covering ``line``, or None."""
    try:
        root_path = _store.normalize_root(root)
        loaded = _graph.ensure(root_path)
        if loaded is None:
            return None
        rel = _store.to_rel(root_path, str(path))
        entry = loaded.symbol_at(rel, int(line))
        return entry.get("name") if entry else None
    except Exception:
        return None


def register_edits(root: str | os.PathLike[str], entries: list[dict[str, Any]]) -> dict[str, Any]:
    """Record edited locations. One batched turn counts as one event per location."""
    try:
        root_path = _store.normalize_root(root)
        loaded = _graph.load(root_path)
        events: list[tuple[str, str | None, str | None]] = []
        for entry in entries or []:
            if not isinstance(entry, dict):
                continue
            rel = _store.to_rel(root_path, str(entry.get("path") or ""))
            if not rel:
                continue
            symbol = entry.get("symbol")
            symbol = symbol if isinstance(symbol, str) and symbol else None
            if symbol is None and loaded is not None and entry.get("line") is not None:
                try:
                    found = loaded.symbol_at(rel, int(entry["line"]))
                except (TypeError, ValueError):
                    found = None
                if found:
                    symbol = found.get("name")
            events.append((rel, symbol, _weights._agent_id(entry.get("agent_id"))))
        return {"registered": _weights.register(root_path, events, "edit")}
    except Exception:
        return {"registered": 0}


def register_reads(root: str | os.PathLike[str], refs: list[str]) -> dict[str, Any]:
    """Record read locations addressed as ``path`` or ``path::symbol``."""
    try:
        root_path = _store.normalize_root(root)
        agent = _weights._agent_id(None)
        events: list[tuple[str, str | None, str | None]] = []
        for ref in refs or []:
            if not isinstance(ref, str):
                continue
            rel, symbol = _store.parse_ref(root_path, ref)
            if rel:
                events.append((rel, symbol, agent))
        return {"registered": _weights.register(root_path, events, "read")}
    except Exception:
        return {"registered": 0}


def remember(
    root: str | os.PathLike[str],
    type: str,  # noqa: A002 - part of the published contract
    content: str,
    tags: list[str] | None = None,
    files: list[str] | None = None,
) -> dict[str, Any]:
    """Append a durable note. Unknown types are stored as ``fact``."""
    try:
        return {
            "id": _notes.remember(
                _store.normalize_root(root),
                str(type),
                str(content),
                list(tags) if tags else None,
                list(files) if files else None,
            )
        }
    except Exception:
        return {"id": ""}


def prime(root: str | os.PathLike[str], limit: int = 12) -> dict[str, Any]:
    """The most recent notes from the last seven days, newest first."""
    try:
        return {"items": _notes.prime(_store.normalize_root(root), int(limit))}
    except Exception:
        return {"items": []}


def snapshot_capture(
    root: str | os.PathLike[str], source: str = "working_tree"
) -> dict[str, object]:
    """Capture an immutable Byte Repository Intelligence snapshot."""
    return _intelligence.snapshot_capture(root, source)


def claim_create(
    root: str | os.PathLike[str],
    snapshot_id: str,
    statement: str,
    status: str = "unresolved",
    scope: dict[str, object] | None = None,
    evidence: list[dict[str, object]] | None = None,
    dependencies: list[dict[str, object]] | None = None,
) -> dict[str, object]:
    """Create a claim with typed evidence and recheck dependencies."""
    return _intelligence.claim_create(
        root,
        snapshot_id,
        statement,
        status=status,
        scope=scope,
        evidence=evidence or (),
        dependencies=dependencies or (),
    )


def typescript_boundary(
    root: str | os.PathLike[str],
    snapshot_id: str,
    identifiers: list[str],
    max_results: int = 250,
) -> dict[str, object]:
    """Resolve TypeScript consumers against captured source without a live-workspace race."""
    return _intelligence.typescript_boundary(root, snapshot_id, identifiers, max_results)


def knowledge_record_answer(
    root: str | os.PathLike[str],
    snapshot_id: str,
    question: str,
    answer: str,
    evidence: list[dict[str, object]],
    boundary: dict[str, object] | None = None,
) -> dict[str, object]:
    """Persist an agent answer and the conditions under which it may be reused."""
    return _intelligence.knowledge_record_answer(
        root, snapshot_id, question, answer, evidence, boundary=boundary
    )


def knowledge_query(
    root: str | os.PathLike[str],
    question: str,
    snapshot_id: str | None = None,
    scope: dict[str, object] | None = None,
    budget: dict[str, object] | None = None,
) -> dict[str, object]:
    """Find and recheck related claims without executing repository code."""
    return _intelligence.knowledge_query(
        root, question, snapshot_id=snapshot_id, scope=scope, budget=budget
    )


def claim_check(
    root: str | os.PathLike[str],
    claim_ids: list[str],
    target_snapshot_id: str | None = None,
) -> dict[str, object]:
    """Recheck claims against a target snapshot or a new working-tree capture."""
    return _intelligence.claim_check(root, claim_ids, target_snapshot_id)


def change_analyze(
    root: str | os.PathLike[str],
    base_snapshot_id: str,
    target_snapshot_id: str,
    intent_ref: str = "",
) -> dict[str, object]:
    """Turn invalidated claim dependencies into scoped change obligations."""
    return _intelligence.change_analyze(
        root, base_snapshot_id, target_snapshot_id, intent_ref
    )


def knowledge_events(
    root: str | os.PathLike[str], after_cursor: int = 0, limit: int = 100
) -> dict[str, object]:
    """Read applicability, obligation, and probe events after a cursor."""
    return _intelligence.knowledge_events(root, after_cursor, limit)


def obligation_list(
    root: str | os.PathLike[str], status: str = "open", limit: int = 100
) -> dict[str, object]:
    """List durable repository-change obligations."""
    return _intelligence.obligation_list(root, status, limit)


def obligation_resolve(
    root: str | os.PathLike[str], obligation_id: str, resolution: str, rationale: str
) -> dict[str, object]:
    """Resolve an obligation with an explicit technical rationale."""
    return _intelligence.obligation_resolve(root, obligation_id, resolution, rationale)


def probe_plan(
    root: str | os.PathLike[str],
    snapshot_id: str,
    hypothesis: str,
    command: list[str],
    oracle_ref: dict[str, object],
    policy_id: str,
    claim_id: str | None = None,
    working_directory: str = ".",
    fixture: dict[str, object] | None = None,
    intervention: dict[str, object] | None = None,
) -> dict[str, object]:
    """Persist a reproducible probe plan without executing it."""
    return _intelligence.probe_plan(
        root,
        snapshot_id,
        hypothesis,
        command,
        oracle_ref,
        policy_id=policy_id,
        claim_id=claim_id,
        working_directory=working_directory,
        fixture=fixture,
        intervention=intervention,
    )


def probe_run(
    root: str | os.PathLike[str], probe_id: str, policy_id: str | None = None
) -> dict[str, object]:
    """Run a probe under its exact user-configured sandbox policy."""
    return _intelligence.probe_run(root, probe_id, policy_id)


def probe_get(root: str | os.PathLike[str], probe_id: str) -> dict[str, object]:
    """Read one immutable probe plan."""
    return _intelligence.probe_get(root, probe_id)
