"""Tier 2 job execution, DB-backed — the deeper command set and stronger
model, with Tier 1's transcript in view. Signals are parsed in code from
both transcripts; tier_agreement (PRD §4.5's Tier 2-only check) is an exact
comparison between the module Tier 1's `!analyze -v` named and the module
Tier 2's narrative concludes is at fault. Also grounds the model in prior
similar resolved cases from the internal knowledge base, if any exist —
see triage/kb_lookup.py.
"""
import logging
import time
import uuid

from sqlalchemy.ext.asyncio import AsyncSession

from ..config import get_settings
from ..models import TierReport
from ..telemetry import record_tier_usage, token_split, tracer
from ..triage.decision_engine import evaluate
from ..triage.kb_lookup import find_similar_cases, format_kb_context
from ..triage.llm import LLMRefused, structured_report
from ..triage.mcp_client import WinDbgMCPClient
from ..triage.schemas import DecisionResult, DumpType, Tier2Findings, Tier2Narrative
from ..triage.transcript_signals import normalize_module, parse_signals
from ..triage.transcripts import capped, load_transcript, save_transcript
from ..workers.base import WorkerHandle
from .tier1_runner import REFUSAL_SUMMARY, collect_transcript

logger = logging.getLogger("triage.tier2")

TIER2_SYSTEM_PROMPT = """\
You are a second-line Windows crash triage engineer reviewing a dump that \
Tier 1 could not confidently resolve. You are given Tier 1's debugger \
transcript followed by the output of a deeper set of WinDbg/CDB/KD commands.

Write the root-cause hypothesis to the standard a competent human first-line \
engineer would use: cite the specific transcript evidence backing each claim \
in `evidence`, and list every place the evidence is inconclusive in \
`open_questions` rather than papering over it with a confident-sounding \
guess. A third-party driver having no public symbols is normal, not \
evidence of guilt by itself — only name it as the cause when the transcript \
backs that up (its code on the faulting frame, a disassembly-level \
explanation, a known bug pattern), and when the stack only points at it \
without that kind of proof, say so as your most-likely-culprit judgment in \
`open_questions` rather than asserting it as fact. Set `faulting_module` to \
the module or driver image you conclude is at fault, or null if the \
evidence doesn't single one out — do not simply repeat Tier 1's module if \
the deeper output points elsewhere. You are not asked to \
rate your confidence or decide whether this report is delivered.\
"""


async def run_tier2(
    db_session: AsyncSession,
    *,
    job_id: uuid.UUID,
    dump_id: uuid.UUID,
    dump_type: DumpType,
    remote_path: str,
    worker: WorkerHandle,
    tier1_faulting_module: str | None,
    tier1_transcript_ref: str | None = None,
    symbol_dir: str | None = None,
) -> tuple[TierReport, DecisionResult]:
    with tracer.start_as_current_span(
        "tier2.run", attributes={"dump.id": str(dump_id), "job.id": str(job_id)}
    ):
        start = time.monotonic()
        async with WinDbgMCPClient.connect(worker.mcp_url) as client:
            transcript, tool_error = await collect_transcript(
                db_session,
                client,
                actor="tier2-worker",
                dump_id=dump_id,
                dump_type=dump_type,
                remote_path=remote_path,
                tier=2,
                symbol_dir=symbol_dir,
            )
        compute_seconds = time.monotonic() - start

        # The base facts (bucket, code, module, stack) live in Tier 1's
        # `!analyze -v`; Tier 2's own commands add depth, not those. Parse
        # the two together so Tier 2's signals rest on the same evidence,
        # plus any tool error from either pass.
        try:
            tier1_transcript = await load_transcript(tier1_transcript_ref) or []
        except Exception:  # noqa: BLE001 — corrupt/expired/missing object: degrade, don't fail every Tier 2 retry
            logger.exception("could not load Tier 1 transcript %s; Tier 2 signals will fail safe", tier1_transcript_ref)
            tier1_transcript = []
        parsed = parse_signals(dump_type, [*tier1_transcript, *transcript], tool_error_occurred=tool_error)
        transcript_ref = await save_transcript(dump_id, 2, transcript)

        # Best-effort grounding from prior resolved cases (V1.1 KB PRD §7) —
        # a lookup failure or empty corpus just means no extra context, same
        # as bugcheck_reference.py's own fail-safe shape.
        kb_matches = await find_similar_cases(
            db_session,
            dump_type=dump_type,
            bugcheck_code=parsed.bugcheck_code,
            exception_code=parsed.exception_code,
            faulting_module=normalize_module(parsed.faulting_module),
            failure_bucket_id=parsed.failure_bucket_id,
        )
        kb_context = format_kb_context(kb_matches)

        settings = get_settings()
        usage_sink: list[dict] = []
        refused = False
        with tracer.start_as_current_span("tier2.llm_call", attributes={"llm.model": settings.tier2_model}):
            try:
                narrative: Tier2Narrative = await structured_report(
                    model=settings.tier2_model,
                    system_prompt=TIER2_SYSTEM_PROMPT,
                    transcript=capped([*tier1_transcript, *transcript]),
                    schema=Tier2Narrative,
                    context=kb_context,
                    usage_sink=usage_sink,
                )
            except LLMRefused:
                refused = True
                narrative = Tier2Narrative(root_cause_hypothesis=REFUSAL_SUMMARY)

        # Exact, in code. The Tier 1 side is what the debugger said (parsed,
        # or the stored Tier 1 finding for reports that predate stored
        # transcripts); only the Tier 2 side is the model's conclusion.
        tier1_modules = parsed.module_aliases if tier1_transcript else {normalize_module(tier1_faulting_module)} - {None}
        tier2_module = normalize_module(narrative.faulting_module)
        parsed.signals.tier_agreement = bool(tier2_module and tier2_module in tier1_modules)

        findings = Tier2Findings(
            root_cause_hypothesis=narrative.root_cause_hypothesis,
            faulting_module=narrative.faulting_module,
            evidence=narrative.evidence,
            open_questions=narrative.open_questions,
            confidence_signals=parsed.signals,
            hard_escalate_flags=parsed.flags,
        )

        decision_result = evaluate(findings.confidence_signals, findings.hard_escalate_flags)
        if refused:
            decision_result = DecisionResult(
                action="escalate", score=decision_result.score, reasons=["model declined to analyse the transcript"]
            )
        tokens_used = record_tier_usage("2", settings.tier2_model, usage_sink, compute_seconds)
        input_tokens, output_tokens = token_split(usage_sink)

        report = TierReport(
            job_id=job_id,
            tier=2,
            model_used=settings.tier2_model,
            findings=findings.model_dump(mode="json"),
            confidence_signals=findings.confidence_signals.model_dump(mode="json"),
            transcript_ref=transcript_ref,
            tokens_used=tokens_used,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            compute_seconds=compute_seconds,
        )
        db_session.add(report)
        await db_session.commit()
        await db_session.refresh(report)
        return report, decision_result
