"""Two-pass universal answer quality pipeline (pre_gen + post_gen)."""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Any

from iuve_connect_agents.shared.answer_quality.answer_level import (
    classify_answer_level,
    detect_level_mismatch,
)
from iuve_connect_agents.shared.answer_quality.contract import StructuredAnswerContract
from iuve_connect_agents.shared.answer_quality.normalizer import (
    normalize_answer,
    remove_duplicate_first_sentence,
    remove_duplicate_headers,
)
from iuve_connect_agents.shared.answer_quality.prompt_audit import audit_prompt_context
from iuve_connect_agents.shared.answer_quality.renderer import strip_duplicate_renderer_title
from iuve_connect_agents.shared.answer_quality.semantic_dedup import deduplicate_blocks, deduplicate_chunks
from iuve_connect_agents.shared.answer_quality.source_validator import is_valid_source_line, strip_fake_sources
from iuve_connect_agents.shared.answer_quality.telemetry import (
    ensure_quality_meta,
    new_correlation_id,
    record_event,
    set_correlation_id,
)


@dataclass
class AnswerQualityContext:
    question: str = ""
    channel: str = "server_chat"
    correlation_id: str = ""
    prompt_chunks: list[str] = field(default_factory=list)
    prompt_titles: list[str] = field(default_factory=list)
    known_sources: list[str] = field(default_factory=list)
    developer_mode: bool = False
    quality_meta: dict[str, Any] = field(default_factory=dict)


class AnswerQualityPipeline:
    """Shared pre/post generation quality pipeline for all answer channels."""

    def __init__(self, *, chunk_threshold: float = 0.82, block_threshold: float = 0.82) -> None:
        self.chunk_threshold = chunk_threshold
        self.block_threshold = block_threshold

    def _with_correlation(self, ctx: AnswerQualityContext) -> AnswerQualityContext:
        correlation_id = str(ctx.correlation_id or "").strip() or new_correlation_id()
        meta = set_correlation_id(ensure_quality_meta(ctx.quality_meta), correlation_id)
        return AnswerQualityContext(
            question=ctx.question,
            channel=ctx.channel,
            correlation_id=correlation_id,
            prompt_chunks=list(ctx.prompt_chunks),
            prompt_titles=list(ctx.prompt_titles),
            known_sources=list(ctx.known_sources),
            developer_mode=ctx.developer_mode,
            quality_meta=meta,
        )

    def pre_gen(self, ctx: AnswerQualityContext) -> AnswerQualityContext:
        ctx = self._with_correlation(ctx)
        meta = dict(ctx.quality_meta)

        audited = audit_prompt_context(ctx.prompt_chunks, titles=ctx.prompt_titles)
        removed_prompt = int(audited.get("prompt_duplicates_found") or 0)
        if removed_prompt:
            record_event(meta, "prompt_duplicates_found", count=removed_prompt)
            record_event(meta, "chunks_deduplicated", count=int(audited.get("removed_chunks") or 0))
            record_event(meta, "prompt_titles_deduplicated", count=int(audited.get("removed_titles") or 0))

        deduped_sources = deduplicate_chunks(
            [str(item) for item in ctx.known_sources if str(item or "").strip()],
            threshold=self.chunk_threshold,
        )
        removed_sources = max(0, len(ctx.known_sources) - len(deduped_sources))
        if removed_sources:
            record_event(meta, "sources_deduplicated", count=removed_sources)

        return AnswerQualityContext(
            question=ctx.question,
            channel=ctx.channel,
            correlation_id=ctx.correlation_id,
            prompt_chunks=list(audited.get("chunks") or []),
            prompt_titles=list(audited.get("titles") or []),
            known_sources=deduped_sources,
            developer_mode=ctx.developer_mode,
            quality_meta=meta,
        )

    def _extract_title(self, text: str) -> str:
        body = str(text or "").strip()
        if not body:
            return ""
        first_line = body.splitlines()[0].strip()
        header_match = re.match(r"^#{1,6}\s+(.+?)\s*$", first_line)
        if header_match:
            return str(header_match.group(1) or "").strip()
        bold_match = re.match(r"^\*\*(.+?)\*\*\s*$", first_line)
        if bold_match:
            return str(bold_match.group(1) or "").strip()
        return ""

    def _collect_sources(self, text: str, known: list[str]) -> list[str]:
        sources = [str(item).strip() for item in known if str(item or "").strip()]
        for line in str(text or "").splitlines():
            if is_valid_source_line(line):
                sources.append(line.strip())
        return deduplicate_chunks(sources, threshold=0.95)

    def post_gen(
        self,
        contract: StructuredAnswerContract,
        ctx: AnswerQualityContext,
    ) -> StructuredAnswerContract:
        ctx = self._with_correlation(ctx)
        meta = ensure_quality_meta(dict(contract.quality_meta or ctx.quality_meta))
        set_correlation_id(meta, ctx.correlation_id)

        body = str(contract.body_md or "").strip()
        title = str(contract.title or "").strip() or self._extract_title(body)

        before_headers = body
        body = remove_duplicate_headers(body)
        if body != before_headers:
            record_event(meta, "duplicate_title_removed")

        before_sentence = body
        body = remove_duplicate_first_sentence(body)
        if body != before_sentence:
            record_event(meta, "duplicate_first_sentence_removed")

        before_sources = body
        body = strip_fake_sources(body)
        if body != before_sources:
            record_event(meta, "fake_source_stripped")

        before_blocks = body
        body = deduplicate_blocks(body, threshold=self.block_threshold)
        if body != before_blocks:
            record_event(meta, "semantic_blocks_merged")

        body = normalize_answer(body)

        level = str(contract.level or "").strip() or classify_answer_level(ctx.question)
        if detect_level_mismatch(ctx.question, body, level):
            record_event(meta, "level_mismatch_detected", detail=level)

        sources = self._collect_sources(body, list(contract.sources or ctx.known_sources))

        return StructuredAnswerContract(
            title=title,
            body_md=body,
            sources=sources,
            level=level,
            quality_meta=meta,
        )

    def process(
        self,
        answer: str,
        *,
        question: str = "",
        channel: str = "server_chat",
        correlation_id: str = "",
        title: str = "",
        sources: list[str] | None = None,
        level: str = "",
        quality_meta: dict[str, Any] | None = None,
    ) -> StructuredAnswerContract:
        ctx = AnswerQualityContext(
            question=str(question or ""),
            channel=str(channel or "server_chat"),
            correlation_id=str(correlation_id or ""),
            known_sources=list(sources or []),
            quality_meta=dict(quality_meta or {}),
        )
        contract = StructuredAnswerContract.from_text(
            answer,
            title=title,
            sources=list(sources or []),
            level=level,
            quality_meta=dict(quality_meta or {}),
        )
        return self.post_gen(contract, ctx)

    def process_rendered(
        self,
        rendered: str,
        contract: StructuredAnswerContract,
        *,
        channel: str = "desktop_agent",
    ) -> tuple[str, StructuredAnswerContract]:
        cleaned, stripped = strip_duplicate_renderer_title(
            rendered,
            contract_title=contract.title,
            body_md=contract.body_md,
        )
        meta = dict(contract.quality_meta or {})
        if stripped:
            record_event(meta, "renderer_title_stripped")
        updated = StructuredAnswerContract(
            title=contract.title,
            body_md=contract.body_md,
            sources=list(contract.sources),
            level=contract.level,
            quality_meta=meta,
        )
        return cleaned, updated

    def deduplicate_retrieval_chunks(self, chunks: list[str]) -> tuple[list[str], int]:
        original = [str(item) for item in chunks if str(item or "").strip()]
        deduped = deduplicate_chunks(original, threshold=self.chunk_threshold)
        return deduped, max(0, len(original) - len(deduped))
