"""Quality gates for generated drafts — novelty, references, format, self-rating."""

from __future__ import annotations

import json
import re
from datetime import datetime, timezone

import numpy as np
from rich.console import Console

from ..config import Config
from ..db import Database

console = Console()


def _cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
    dot = np.dot(a, b)
    norm = np.linalg.norm(a) * np.linalg.norm(b)
    if norm == 0:
        return 0.0
    return float(dot / norm)


REQUIRED_SECTIONS = ["introduction", "security considerations", "iana considerations"]


class QualityGates:
    def __init__(self, config: Config, db: Database, analyzer):
        self.config = config
        self.db = db
        self.analyzer = analyzer

    def run_all(self, draft_id: int) -> dict:
        """Run all quality gates. Returns {gate_name: {passed: bool, score: float, details: str}}"""
        results = {}
        results["novelty"] = self.check_novelty(draft_id)
        results["references"] = self.check_references(draft_id)
        results["format"] = self.check_format(draft_id)
        results["self_rating"] = self.check_self_rating(draft_id)

        passed = sum(1 for r in results.values() if r["passed"])
        total = len(results)
        console.print(
            f"Quality gates: [{'green' if passed == total else 'yellow'}]"
            f"{passed}/{total} passed[/]"
        )
        for name, result in results.items():
            status = "[green]PASS[/]" if result["passed"] else "[red]FAIL[/]"
            console.print(f"  {status} {name}: {result['details']}")

        return results

    def check_novelty(self, draft_id: int) -> dict:
        """Embed generated abstract, compare against all existing drafts.
        Flag if max_similarity > 0.90."""
        gd = self.db.get_generated_draft(draft_id)
        if not gd:
            return {"passed": False, "score": 0.0, "details": "Draft not found"}

        abstract = gd.get("abstract", "")
        title = gd.get("title", "")
        text_to_embed = f"{title}\n\n{abstract}"

        if not text_to_embed.strip():
            return {"passed": False, "score": 0.0, "details": "No abstract to check"}

        # Embed via Ollama
        try:
            import ollama as ollama_lib
            client = ollama_lib.Client(host=self.config.ollama_url)
            resp = client.embed(
                model=self.config.ollama_embed_model,
                input=text_to_embed[:8000],
            )
            gen_vec = np.array(resp["embeddings"][0], dtype=np.float32)
        except Exception as e:
            return {"passed": True, "score": 0.0,
                    "details": f"Ollama unavailable, skipping novelty check: {e}"}

        all_embeddings = self.db.all_embeddings()
        if not all_embeddings:
            return {"passed": True, "score": 1.0, "details": "No existing embeddings to compare"}

        max_sim = 0.0
        most_similar = ""
        for name, vec in all_embeddings.items():
            sim = _cosine_similarity(gen_vec, vec)
            if sim > max_sim:
                max_sim = sim
                most_similar = name

        passed = max_sim < 0.90
        return {
            "passed": passed,
            "score": 1.0 - max_sim,
            "details": (
                f"Max similarity: {max_sim:.3f} with {most_similar}"
                + ("" if passed else " — too similar, needs differentiation")
            ),
        }

    def check_references(self, draft_id: int) -> dict:
        """Extract RFC/draft refs via regex, cross-check against draft_refs table."""
        gd = self.db.get_generated_draft(draft_id)
        if not gd:
            return {"passed": False, "score": 0.0, "details": "Draft not found"}

        full_text = gd.get("full_text", "")
        if not full_text:
            return {"passed": False, "score": 0.0, "details": "No full text"}

        # Extract references from generated text
        rfc_pattern = re.compile(r'\[?RFC\s*(\d{3,5})\]?', re.IGNORECASE)
        draft_pattern = re.compile(r'(draft-[a-z0-9-]+)', re.IGNORECASE)

        found_rfcs = set(rfc_pattern.findall(full_text))
        found_drafts = set(draft_pattern.findall(full_text))

        total_refs = len(found_rfcs) + len(found_drafts)

        # Cross-check: how many of these RFCs are actually in our DB?
        known_rfcs = set()
        for ref_id in found_rfcs:
            drafts = self.db.drafts_referencing("rfc", ref_id)
            if drafts:
                known_rfcs.add(ref_id)

        # Cross-check: how many referenced drafts exist in our DB?
        known_drafts = set()
        for dname in found_drafts:
            if self.db.get_draft(dname):
                known_drafts.add(dname)

        verified = len(known_rfcs) + len(known_drafts)
        score = verified / total_refs if total_refs > 0 else 0.0

        passed = total_refs >= 3 and score >= 0.3
        return {
            "passed": passed,
            "score": score,
            "details": (
                f"{total_refs} refs found ({len(found_rfcs)} RFCs, {len(found_drafts)} drafts), "
                f"{verified} verified in DB ({score:.0%})"
            ),
        }

    def check_format(self, draft_id: int) -> dict:
        """Check line length <= 72, required sections present, no markdown leaked."""
        gd = self.db.get_generated_draft(draft_id)
        if not gd:
            return {"passed": False, "score": 0.0, "details": "Draft not found"}

        full_text = gd.get("full_text", "")
        if not full_text:
            return {"passed": False, "score": 0.0, "details": "No full text"}

        issues = []

        # Check line length
        lines = full_text.split("\n")
        long_lines = [i + 1 for i, line in enumerate(lines) if len(line) > 72]
        if long_lines:
            issues.append(f"{len(long_lines)} lines exceed 72 chars")

        # Check required sections
        text_lower = full_text.lower()
        for section in REQUIRED_SECTIONS:
            if section not in text_lower:
                issues.append(f"Missing required section: {section}")

        # Check for leaked markdown
        markdown_patterns = [
            (r'^#{1,3}\s', "markdown headers (# )"),
            (r'\*\*[^*]+\*\*', "bold markdown (**text**)"),
            (r'```', "code fences (```)"),
            (r'\[([^\]]+)\]\(http', "markdown links"),
        ]
        for pattern, desc in markdown_patterns:
            if re.search(pattern, full_text, re.MULTILINE):
                issues.append(f"Leaked markdown: {desc}")

        if not issues:
            return {"passed": True, "score": 1.0, "details": "All format checks pass"}

        score = max(0.0, 1.0 - len(issues) * 0.25)
        return {
            "passed": len(issues) <= 1,  # Allow one minor issue
            "score": score,
            "details": "; ".join(issues),
        }

    def check_self_rating(self, draft_id: int) -> dict:
        """Feed through existing rate_draft() pipeline. Score on same 1-5 scale."""
        gd = self.db.get_generated_draft(draft_id)
        if not gd:
            return {"passed": False, "score": 0.0, "details": "Draft not found"}

        # Create a temporary prompt matching the analyzer's rating format
        title = gd.get("title", "")
        abstract = gd.get("abstract", "")
        draft_name = gd.get("draft_name", "")

        from ..analyzer import RATE_PROMPT_COMPACT, CATEGORIES_SHORT, _prompt_hash

        prompt = RATE_PROMPT_COMPACT.format(
            name=draft_name,
            title=title,
            time=datetime.now(timezone.utc).strftime("%Y-%m-%d"),
            pages="?",
            abstract=abstract[:2000],
            categories=", ".join(CATEGORIES_SHORT),
        )

        phash = _prompt_hash("self-rate-" + prompt)
        cache_key = f"_selfrate_{draft_id}_"

        # Check cache
        cached = self.db.get_cached_response(cache_key, phash)
        if cached:
            try:
                data = json.loads(cached)
                return self._parse_self_rating(data, draft_id)
            except (json.JSONDecodeError, KeyError):
                pass

        try:
            text, in_tok, out_tok = self.analyzer._call_claude(prompt, max_tokens=512, cheap=True)
            text = self.analyzer._extract_json(text)
            data = json.loads(text)

            self.db.cache_response(
                cache_key, phash,
                self.config.claude_model_cheap,
                prompt, text, in_tok, out_tok,
            )

            return self._parse_self_rating(data, draft_id)
        except Exception as e:
            return {"passed": False, "score": 0.0,
                    "details": f"Self-rating failed: {e}"}

    def _parse_self_rating(self, data: dict, draft_id: int) -> dict:
        """Parse self-rating result and update the generated draft."""
        novelty = int(data.get("n", data.get("novelty", 3)))
        maturity = int(data.get("m", data.get("maturity", 3)))
        relevance = int(data.get("r", data.get("relevance", 3)))
        overlap = int(data.get("o", data.get("overlap", 3)))
        momentum = int(data.get("mo", data.get("momentum", 3)))

        composite = (
            novelty * 0.30
            + relevance * 0.25
            + maturity * 0.20
            + momentum * 0.15
            + (6 - overlap) * 0.10
        )

        # Store rating on the generated draft
        gd = self.db.get_generated_draft(draft_id)
        if gd:
            self.db.conn.execute(
                "UPDATE generated_drafts SET rating_json = ?, quality_score = ? WHERE id = ?",
                (json.dumps(data), composite, draft_id),
            )
            self.db.conn.commit()

        passed = composite >= 2.5
        return {
            "passed": passed,
            "score": composite / 5.0,
            "details": (
                f"Composite: {composite:.1f}/5 "
                f"(N:{novelty} M:{maturity} O:{overlap} Mo:{momentum} R:{relevance})"
            ),
        }
