"""Knowledge quality engine for GreyMatter Solo.

Detects contradictions, near-duplicates, and stale entries in the
knowledge graph. Provides trust weighting by source type and automated
quality scoring.

Usage:
    engine = QualityEngine(db)
    report = engine.full_check()
    duplicates = engine.find_duplicates()
    contradictions = engine.find_contradictions()
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

from .db import Database


# Source trust hierarchy: higher = more trustworthy
TRUST_WEIGHTS = {
    "correction": 1.0,    # User explicitly corrected behavior
    "gotcha": 0.9,        # Known pitfall, high signal
    "decision": 0.8,      # Deliberate choice
    "pattern": 0.7,       # Emerged from repetition
    "expertise": 0.7,     # Domain knowledge
    "reference": 0.5,     # Factual lookup
    "observation": 0.4,   # Auto-observed, needs validation
    "insight": 0.4,       # Needs confirmation
}

# Negation signals that suggest contradiction
_NEGATION_PAIRS = [
    ("always", "never"),
    ("must", "must not"),
    ("should", "should not"),
    ("do", "don't"),
    ("do", "do not"),
    ("use", "avoid"),
    ("enable", "disable"),
    ("true", "false"),
    ("yes", "no"),
]


@dataclass
class QualityIssue:
    """A detected quality issue."""
    issue_type: str       # "duplicate", "contradiction", "stale", "low_trust"
    severity: str         # "high", "medium", "low"
    entry_ids: list[str]
    description: str
    suggestion: str


@dataclass
class QualityReport:
    """Results of a quality check."""
    total_entries: int
    issues: list[QualityIssue] = field(default_factory=list)
    score: float = 1.0    # 0.0-1.0, higher = healthier

    @property
    def issue_count(self) -> int:
        return len(self.issues)

    def to_dict(self) -> dict[str, Any]:
        return {
            "total_entries": self.total_entries,
            "issue_count": self.issue_count,
            "score": round(self.score, 3),
            "issues": [
                {
                    "type": i.issue_type,
                    "severity": i.severity,
                    "entry_ids": i.entry_ids,
                    "description": i.description,
                    "suggestion": i.suggestion,
                }
                for i in self.issues
            ],
        }


class QualityEngine:
    """Analyzes knowledge graph quality."""

    def __init__(self, db: Database):
        self.db = db

    def full_check(self) -> QualityReport:
        """Run all quality checks and return a combined report."""
        entries = self._load_entries()
        report = QualityReport(total_entries=len(entries))

        report.issues.extend(self._find_duplicates(entries))
        report.issues.extend(self._find_contradictions(entries))
        report.issues.extend(self._find_stale(entries))

        # Score: deduct for issues
        if entries:
            deduction = sum(
                0.05 if i.severity == "low" else 0.10 if i.severity == "medium" else 0.15
                for i in report.issues
            )
            report.score = max(0.0, 1.0 - deduction)

        return report

    def find_duplicates(self, threshold: float = 0.7) -> list[QualityIssue]:
        """Find near-duplicate entries by title and content similarity."""
        return self._find_duplicates(self._load_entries(), threshold)

    def find_contradictions(self) -> list[QualityIssue]:
        """Find potentially contradicting entries in the same domain."""
        return self._find_contradictions(self._load_entries())

    def find_stale(self, days: int = 90) -> list[QualityIssue]:
        """Find entries not updated in N days."""
        return self._find_stale(self._load_entries(), days)

    def get_trust_weight(self, entry_type: str) -> float:
        """Return trust weight for an entry type."""
        return TRUST_WEIGHTS.get(entry_type, 0.5)

    def merge_duplicates(self, keep_id: str, remove_id: str) -> bool:
        """Merge two entries: keep one, prune the other."""
        keep = self.db.execute_one(
            "SELECT * FROM knowledge_entries WHERE id = ?", (keep_id,)
        )
        remove = self.db.execute_one(
            "SELECT * FROM knowledge_entries WHERE id = ?", (remove_id,)
        )
        if not keep or not remove:
            return False

        # Append remove's content to keep's if different
        if remove.get("content", "") not in keep.get("content", ""):
            merged = keep["content"] + "\n\n---\n\n" + remove["content"]
            self.db.execute(
                "UPDATE knowledge_entries SET content = ?, updated_at = datetime('now') WHERE id = ?",
                (merged, keep_id),
            )

        # Prune the duplicate
        self.db.execute(
            "UPDATE knowledge_entries SET pruned_at = datetime('now') WHERE id = ?",
            (remove_id,),
        )
        return True

    # --- Private ---

    def _load_entries(self) -> list[dict[str, Any]]:
        return self.db.execute(
            "SELECT * FROM knowledge_entries WHERE pruned_at IS NULL ORDER BY created_at"
        )

    def _find_duplicates(
        self, entries: list[dict], threshold: float = 0.7
    ) -> list[QualityIssue]:
        issues = []
        seen_pairs: set[tuple[str, str]] = set()

        for i, a in enumerate(entries):
            for b in entries[i + 1:]:
                pair = (a["id"], b["id"])
                if pair in seen_pairs:
                    continue

                sim = self._similarity(a, b)
                if sim >= threshold:
                    seen_pairs.add(pair)
                    trust_a = TRUST_WEIGHTS.get(a.get("entry_type", ""), 0.5)
                    trust_b = TRUST_WEIGHTS.get(b.get("entry_type", ""), 0.5)
                    keep = a["id"] if trust_a >= trust_b else b["id"]
                    remove = b["id"] if keep == a["id"] else a["id"]
                    issues.append(QualityIssue(
                        issue_type="duplicate",
                        severity="medium",
                        entry_ids=[a["id"], b["id"]],
                        description=f"Similar entries ({sim:.0%}): '{a['title'][:50]}' ↔ '{b['title'][:50]}'",
                        suggestion=f"Merge: keep {keep}, prune {remove}",
                    ))

        return issues

    def _find_contradictions(self, entries: list[dict]) -> list[QualityIssue]:
        issues = []
        seen_pairs: set[tuple[str, str]] = set()

        # Group by domain for efficiency
        by_domain: dict[str, list[dict]] = {}
        for e in entries:
            domain = e.get("domain") or "general"
            by_domain.setdefault(domain, []).append(e)

        for domain, group in by_domain.items():
            for i, a in enumerate(group):
                for b in group[i + 1:]:
                    pair = (a["id"], b["id"])
                    if pair in seen_pairs:
                        continue

                    if self._is_contradicting(a, b):
                        seen_pairs.add(pair)
                        issues.append(QualityIssue(
                            issue_type="contradiction",
                            severity="high",
                            entry_ids=[a["id"], b["id"]],
                            description=f"Potential contradiction in '{domain}': "
                                        f"'{a['title'][:40]}' vs '{b['title'][:40]}'",
                            suggestion="Review and resolve: prune the outdated entry",
                        ))

        return issues

    def _find_stale(self, entries: list[dict], days: int = 90) -> list[QualityIssue]:
        issues = []
        stale = self.db.execute(
            f"""SELECT id, title, entry_type, updated_at, created_at
                FROM knowledge_entries
                WHERE pruned_at IS NULL
                  AND COALESCE(updated_at, created_at) < datetime('now', '-{days} days')
                ORDER BY COALESCE(updated_at, created_at)
                LIMIT 20""",
        )
        for s in stale:
            issues.append(QualityIssue(
                issue_type="stale",
                severity="low",
                entry_ids=[s["id"]],
                description=f"Not updated in {days}+ days: '{s['title'][:60]}'",
                suggestion="Review: confirm if still accurate or prune",
            ))
        return issues

    @staticmethod
    def _similarity(a: dict, b: dict) -> float:
        """Compute similarity between two entries (Jaccard on title + content tokens)."""
        def tokenize(text: str) -> set[str]:
            return {w.lower() for w in text.split() if len(w) >= 3}

        title_a = tokenize(a.get("title", ""))
        title_b = tokenize(b.get("title", ""))
        content_a = tokenize(a.get("content", "")[:500])
        content_b = tokenize(b.get("content", "")[:500])

        tokens_a = title_a | content_a
        tokens_b = title_b | content_b

        if not tokens_a and not tokens_b:
            return 0.0
        union = tokens_a | tokens_b
        return len(tokens_a & tokens_b) / len(union) if union else 0.0

    @staticmethod
    def _is_contradicting(a: dict, b: dict) -> bool:
        """Check if two entries might be contradicting based on negation signals."""
        title_a = (a.get("title") or "").lower()
        title_b = (b.get("title") or "").lower()
        content_a = (a.get("content") or "").lower()[:300]
        content_b = (b.get("content") or "").lower()[:300]

        text_a = f"{title_a} {content_a}"
        text_b = f"{title_b} {content_b}"

        # Check for negation pairs
        for pos, neg in _NEGATION_PAIRS:
            if (pos in text_a and neg in text_b) or (neg in text_a and pos in text_b):
                # Only flag if they share significant topic overlap
                tokens_a = {w for w in text_a.split() if len(w) >= 4}
                tokens_b = {w for w in text_b.split() if len(w) >= 4}
                # Remove the negation words themselves
                for p, n in _NEGATION_PAIRS:
                    tokens_a.discard(p)
                    tokens_a.discard(n)
                    tokens_b.discard(p)
                    tokens_b.discard(n)
                if tokens_a and tokens_b:
                    overlap = len(tokens_a & tokens_b) / max(len(tokens_a), len(tokens_b), 1)
                    if overlap >= 0.3:
                        return True
        return False
