from __future__ import annotations

from app.modules.agent.runtime.docs_qa_pipeline.doc_identity import DocsCanonicalDocIdResolver
from app.modules.agent.runtime.docs_qa_pipeline.models import DocsDiagnostics, DocsEvidenceBundle, OpenAPIResult


class DocsDiagnosticsBuilder:
    def __init__(self, resolver: DocsCanonicalDocIdResolver | None = None) -> None:
        self._resolver = resolver or DocsCanonicalDocIdResolver()

    def build(
        self,
        *,
        intent: str,
        sub_intent: str,
        planned_layers: list[str],
        executed_layers: list[str],
        non_empty_layers: list[str],
        layer_diagnostics: dict[str, object],
        evidence_bundle: DocsEvidenceBundle,
        openapi_result: OpenAPIResult | None,
        prompt_used: str,
        llm_mode: str,
        answer_mode: str,
        output_valid: bool,
        matched_intent_source: str,
        matched_anchor_type: str,
        matched_anchor_value: str | None,
        exact_anchor_match: bool,
        query_entity_candidates: list[str],
        resolved_entity_candidates: list[str],
        query_anchor_candidates: list[str],
        resolved_anchor_candidates: list[str],
        anchor_candidates: list[str],
        selected_anchor: str | None,
        anchor_selection_reason: str,
        anchor_match_type: str,
        docs_layers_with_hits: list[str],
        gate_decision: str,
        gate_decision_reason: str,
        gate_missing_requirements: list[str],
        gate_satisfied_requirements: list[str],
        requested_fragment_type: str | None,
        fragment_evidence_found: list[str],
        fragment_missing_requirements: list[str],
        prompt: dict[str, object],
        degraded_reason: str | None,
        fallback_used: bool,
        code_intents_stubbed: bool,
    ) -> DocsDiagnostics:
        missing = list((openapi_result.diagnostics if openapi_result else {}).get("missing_required_fields") or [])
        openapi_fields = 0
        if openapi_result is not None:
            openapi_fields += len((openapi_result.request_schema or {}).get("properties") or {})
            openapi_fields += len((openapi_result.response_schema or {}).get("properties") or {})
        return DocsDiagnostics(
            intent=intent,
            sub_intent=sub_intent,
            layers_used=list(planned_layers),
            documents_found=len(evidence_bundle.documents),
            facts_found=len(evidence_bundle.facts),
            relations_found=len(evidence_bundle.relations),
            openapi_fields_extracted=openapi_fields,
            missing_required_fields=missing,
            openapi_status=self._openapi_status(openapi_result),
            prompt_used=prompt_used,
            llm_mode=llm_mode,
            output_valid=output_valid,
            matched_intent_source=matched_intent_source,
            matched_anchor_type=matched_anchor_type,
            matched_anchor_value=matched_anchor_value,
            exact_anchor_match=exact_anchor_match,
            docs_layers_requested=list(planned_layers),
            docs_layers_with_hits=list(docs_layers_with_hits),
            planned_layers=list(planned_layers),
            executed_layers=list(executed_layers),
            non_empty_layers=list(non_empty_layers),
            layer_diagnostics=dict(layer_diagnostics),
            query_entity_candidates=list(query_entity_candidates),
            resolved_entity_candidates=list(resolved_entity_candidates),
            query_anchor_candidates=list(query_anchor_candidates),
            resolved_anchor_candidates=list(resolved_anchor_candidates),
            anchor_candidates=list(anchor_candidates),
            selected_anchor=selected_anchor,
            anchor_selection_reason=anchor_selection_reason,
            anchor_match_type=anchor_match_type,
            doc_ids=self._doc_ids(evidence_bundle, selected_anchor),
            doc_paths=self._doc_paths(evidence_bundle),
            doc_titles=self._doc_titles(evidence_bundle),
            relation_hits_count=len(evidence_bundle.relations),
            relation_targets=self._relation_targets(evidence_bundle),
            selected_doc_ids=self._selected_doc_ids(evidence_bundle),
            selected_fact_ids=self._selected_ids(evidence_bundle.facts, ("fact_id", "doc_id", "document_id"), fallback="path"),
            selected_relation_ids=self._selected_ids(
                evidence_bundle.relations,
                ("relation_id", "target_doc_id", "target_document_id", "doc_id", "document_id"),
                fallback="path",
            ),
            selected_chunk_ids=self._selected_ids(evidence_bundle.chunks, ("chunk_id", "doc_id", "document_id"), fallback="path"),
            selected_entity_ids=self._selected_ids(evidence_bundle.entities, ("entity", "doc_id", "document_id"), fallback="title"),
            selected_workflow_ids=self._selected_ids(evidence_bundle.workflows, ("workflow_id", "doc_id", "document_id"), fallback="path"),
            fallback_doc_hits_count=len(evidence_bundle.documents) + len(evidence_bundle.chunks),
            fallback_used=fallback_used,
            fact_hits=len(evidence_bundle.facts),
            entity_hits=self._entity_hits(evidence_bundle),
            evidence_summary=self._evidence_summary(evidence_bundle, openapi_result),
            gate_decision=gate_decision,
            gate_decision_reason=gate_decision_reason,
            gate_missing_requirements=list(gate_missing_requirements),
            gate_satisfied_requirements=list(gate_satisfied_requirements),
            openapi_evidence=dict(self._openapi_evidence(openapi_result)),
            requested_fragment_type=requested_fragment_type,
            fragment_evidence_found=list(fragment_evidence_found),
            fragment_missing_requirements=list(fragment_missing_requirements),
            prompt=dict(prompt),
            answer_mode=answer_mode,
            degrade_reason=degraded_reason,
            degraded_reason=degraded_reason,
            code_intents_stubbed=code_intents_stubbed,
        )

    def _openapi_status(self, openapi_result: OpenAPIResult | None) -> dict[str, bool]:
        diagnostics = openapi_result.diagnostics if openapi_result else {}
        return {
            "has_path": bool(diagnostics.get("has_path")),
            "has_method": bool(diagnostics.get("has_method")),
            "has_request": bool(diagnostics.get("has_request")),
            "has_response": bool(diagnostics.get("has_response")),
        }

    def _doc_ids(self, evidence_bundle: DocsEvidenceBundle, selected_anchor: str | None) -> list[str]:
        values = self._collect_doc_ids(evidence_bundle)
        for item in evidence_bundle.relations:
            metadata = dict(item.get("metadata") or {})
            target = metadata.get("target_doc_id") or metadata.get("target_document_id")
            if target:
                values.append(str(target))
        result = self._dedupe(values)
        if selected_anchor and selected_anchor in result:
            return [selected_anchor, *[item for item in result if item != selected_anchor]]
        if selected_anchor:
            return [selected_anchor, *result]
        return result

    def _selected_doc_ids(self, evidence_bundle: DocsEvidenceBundle) -> list[str]:
        values = self._selected_ids(evidence_bundle.documents, ("doc_id", "document_id"), fallback="path")
        values.extend(self._selected_ids(evidence_bundle.entities, ("doc_id", "document_id"), fallback="path"))
        values.extend(self._selected_ids(evidence_bundle.workflows, ("doc_id", "document_id"), fallback="path"))
        return self._dedupe(values)

    def _doc_paths(self, evidence_bundle: DocsEvidenceBundle) -> list[str]:
        return self._collect_distinct(evidence_bundle, "path")

    def _doc_titles(self, evidence_bundle: DocsEvidenceBundle) -> list[str]:
        return self._collect_distinct(evidence_bundle, "title")

    def _relation_targets(self, evidence_bundle: DocsEvidenceBundle) -> list[str]:
        values: list[str] = []
        for item in evidence_bundle.relations:
            metadata = dict(item.get("metadata") or {})
            target = (
                metadata.get("target_doc_id")
                or metadata.get("target_document_id")
                or metadata.get("related_to")
                or metadata.get("document_id")
                or metadata.get("doc_id")
                or item.get("title")
                or item.get("path")
            )
            if target:
                values.append(str(target))
        return self._dedupe(values)

    def _selected_ids(self, items: list[dict], metadata_keys: tuple[str, ...], *, fallback: str) -> list[str]:
        values: list[str] = []
        for item in items:
            metadata = dict(item.get("metadata") or {})
            candidate = None
            for key in metadata_keys:
                candidate = metadata.get(key)
                if candidate:
                    break
            if not candidate:
                candidate = item.get(fallback)
            if candidate:
                values.append(str(candidate))
        return self._dedupe(values)

    def _collect_doc_ids(self, evidence_bundle: DocsEvidenceBundle) -> list[str]:
        values: list[str] = []
        for item in evidence_bundle.raw_rows:
            values.extend(self._resolver.candidates(item))
        return self._dedupe(values)

    def _collect_distinct(self, evidence_bundle: DocsEvidenceBundle, key: str, *, fallback_key: str | None = None) -> list[str]:
        values: list[str] = []
        for item in evidence_bundle.raw_rows:
            metadata = dict(item.get("metadata") or {})
            candidate = metadata.get(key) if key == "doc_id" else item.get(key)
            if not candidate and fallback_key:
                candidate = item.get(fallback_key)
            if candidate:
                values.append(str(candidate))
        return self._dedupe(values)

    def _dedupe(self, values: list[str]) -> list[str]:
        result: list[str] = []
        seen: set[str] = set()
        for value in values:
            normalized = value.strip()
            if not normalized:
                continue
            if normalized in seen:
                continue
            seen.add(normalized)
            result.append(normalized)
        return result

    def _entity_hits(self, evidence_bundle: DocsEvidenceBundle) -> int:
        values = self._collect_distinct(evidence_bundle, "entity")
        return len(values)

    def _evidence_summary(self, evidence_bundle: DocsEvidenceBundle, openapi_result: OpenAPIResult | None) -> dict[str, object]:
        return {
            "documents": len(evidence_bundle.documents),
            "facts": len(evidence_bundle.facts),
            "entities": len(evidence_bundle.entities),
            "workflows": len(evidence_bundle.workflows),
            "relations": len(evidence_bundle.relations),
            "chunks": len(evidence_bundle.chunks),
            "selected_doc_ids": self._selected_doc_ids(evidence_bundle),
            "selected_fact_ids": self._selected_ids(evidence_bundle.facts, ("fact_id", "doc_id", "document_id"), fallback="path"),
            "selected_relation_ids": self._selected_ids(
                evidence_bundle.relations,
                ("relation_id", "target_doc_id", "target_document_id", "doc_id", "document_id"),
                fallback="path",
            ),
            "selected_chunk_ids": self._selected_ids(evidence_bundle.chunks, ("chunk_id", "doc_id", "document_id"), fallback="path"),
            "entity_hits": self._entity_hits(evidence_bundle),
            "openapi_signals": self._openapi_evidence(openapi_result),
        }

    def _openapi_evidence(self, openapi_result: OpenAPIResult | None) -> dict[str, bool]:
        diagnostics = openapi_result.diagnostics if openapi_result else {}
        return {
            "path_found": bool(diagnostics.get("has_path")),
            "method_found": bool(diagnostics.get("has_method")),
            "operation_semantics_found": bool(diagnostics.get("operation_semantics_found")),
            "request_payload_found": bool(diagnostics.get("request_payload_found")),
            "request_schema": bool(diagnostics.get("has_request")),
            "request_fields_found": bool(diagnostics.get("request_fields_found")),
            "response_payload_found": bool(diagnostics.get("response_payload_found")),
            "response_schema": bool(diagnostics.get("has_response")),
            "response_fields_found": bool(diagnostics.get("response_fields_found")),
            "status_codes": bool(diagnostics.get("status_codes_found")),
            "content_type_found": bool(diagnostics.get("content_type_found")),
            "examples_found": bool(diagnostics.get("examples_found")),
            "payload_description": bool(diagnostics.get("payload_description_found")),
        }
