"""BibTeX validation checker.

Validates parsed BibTeX records for completeness (missing required fields),
duplicate citation keys, and broken DOI links via HTTP HEAD requests.
"""

from __future__ import annotations

import difflib
import re
from collections import Counter
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

import httpx

from alice_cli.cite.validator import REQUIRED_FIELDS, detect_citation_type

if TYPE_CHECKING:
    from alice_cli.cite.models import CitationRecord

_CROSSREF_WORKS = "https://api.crossref.org/works"
# DOI prefixes registered with DataCite (not Crossref) — skip metadata lookup
_DATACITE_PREFIXES = {"10.48550", "10.5281", "10.5061", "10.6084"}


def _normalize_title(title: str) -> str:
    title = title.lower()
    title = re.sub(r"[^\w\s]", " ", title)
    return re.sub(r"\s+", " ", title).strip()


def _title_similarity(a: str, b: str) -> float:
    return difflib.SequenceMatcher(None, _normalize_title(a), _normalize_title(b)).ratio()


def _doi_prefix(doi: str) -> str:
    return doi.split("/")[0] if "/" in doi else ""


@dataclass
class CheckError:
    """A single validation error or warning for a BibTeX entry."""

    citation_key: str
    level: str  # "error" or "warning"
    message: str
    suggestion: str | None = None  # actionable correction hint, if available


@dataclass
class ValidationReport:
    """Structured output from the BibTeX checker."""

    total_entries: int
    errors: list[CheckError] = field(default_factory=list)
    warnings: list[CheckError] = field(default_factory=list)

    @property
    def error_count(self) -> int:
        return len(self.errors)

    @property
    def warning_count(self) -> int:
        return len(self.warnings)


def _check_required_fields(record: CitationRecord) -> list[CheckError]:
    """Check a single record against REQUIRED_FIELDS for its detected type."""
    citation_type = detect_citation_type(record)
    required = REQUIRED_FIELDS.get(citation_type, [])
    errors: list[CheckError] = []
    for req_field in required:
        value = getattr(record, req_field, None)
        if (
            value is None
            or (isinstance(value, str) and not value.strip())
            or (isinstance(value, list) and len(value) == 0)
        ):
            errors.append(
                CheckError(
                    citation_key=record.citation_key,
                    level="error",
                    message=f"Missing required field '{req_field}' for {citation_type.value} entry",
                )
            )
    return errors


def _check_duplicate_keys(records: list[CitationRecord]) -> list[CheckError]:
    """Find duplicate citation keys across all records."""
    key_counts = Counter(r.citation_key for r in records)
    errors: list[CheckError] = []
    for record in records:
        if key_counts[record.citation_key] > 1:
            errors.append(
                CheckError(
                    citation_key=record.citation_key,
                    level="error",
                    message=f"Duplicate citation key '{record.citation_key}'",
                )
            )
    return errors


def _suggest_doi_correction(title: str, client: httpx.Client) -> str | None:
    """Search Crossref by title and return the best-matching DOI as a suggestion string.

    Returns a human-readable hint like:
        ``10.18653/v1/N19-1423 — "BERT: Pre-training…" (2019)``
    or ``None`` if no sufficiently similar result is found (threshold: 0.60).
    """
    if not title.strip():
        return None
    try:
        resp = client.get(
            "https://api.crossref.org/works",
            params={"query.title": title, "rows": 3},
            timeout=10.0,
        )
    except (httpx.HTTPError, httpx.TimeoutException):
        return None
    if not (200 <= resp.status_code < 300):
        return None
    try:
        items = resp.json().get("message", {}).get("items", [])
    except ValueError:
        return None

    best_sim = 0.0
    best_text: str | None = None
    for item in items:
        title_parts = item.get("title", [])
        crossref_title = title_parts[0] if title_parts else ""
        if not crossref_title:
            continue
        sim = _title_similarity(title, crossref_title)
        if sim > best_sim:
            best_sim = sim
            doi = item.get("DOI", "")
            date_parts = item.get("issued", {}).get("date-parts", [[]])
            year = str(date_parts[0][0]) if date_parts and date_parts[0] else ""
            year_str = f" ({year})" if year else ""
            best_text = f'{doi} — "{crossref_title[:60]}"{year_str}'

    return best_text if best_sim >= 0.60 else None


def _check_doi_links(
    records: list[CitationRecord],
    client: httpx.Client,
) -> list[CheckError]:
    """HEAD-request each DOI and classify the result.

    - 2xx: OK
    - 404: error — DOI definitively does not exist (hallucination indicator)
    - Other non-2xx (403, 418, …): warning — real papers blocked by bot-protection
    - Network failure: warning — could not be verified
    """
    issues: list[CheckError] = []
    for record in records:
        if not record.doi or not record.doi.strip():
            continue
        try:
            response = client.head(
                f"https://doi.org/{record.doi}",
                follow_redirects=True,
                timeout=10.0,
            )
            if 200 <= response.status_code < 300:
                continue
            if response.status_code == 404:
                suggestion = _suggest_doi_correction(record.title, client) if record.title else None
                issues.append(
                    CheckError(
                        citation_key=record.citation_key,
                        level="error",
                        message=f"DOI '{record.doi}' does not exist (HTTP 404)",
                        suggestion=suggestion,
                    )
                )
            else:
                issues.append(
                    CheckError(
                        citation_key=record.citation_key,
                        level="warning",
                        message=f"DOI '{record.doi}' returned HTTP {response.status_code}",
                    )
                )
        except (httpx.HTTPError, httpx.TimeoutException):
            issues.append(
                CheckError(
                    citation_key=record.citation_key,
                    level="warning",
                    message=f"DOI '{record.doi}' could not be reached",
                )
            )
    return issues


def _check_doi_metadata(
    records: list[CitationRecord],
    client: httpx.Client,
    skip_dois: set[str],
) -> list[CheckError]:
    """Fetch Crossref metadata for resolved DOIs and compare against BibTeX fields.

    Catches hallucinated entries where a real DOI is paired with fabricated metadata
    (wrong title, wrong year).  DataCite-registered DOIs are skipped because they
    are not in the Crossref registry.

    - Title similarity < 0.60: error (DOI points to a completely different paper)
    - Title similarity 0.60–0.79: warning (significant title mismatch)
    - Year difference > 2: warning (possible preprint/published year skew)
    """
    issues: list[CheckError] = []
    for record in records:
        doi = (record.doi or "").strip()
        if not doi or doi in skip_dois:
            continue
        if _doi_prefix(doi) in _DATACITE_PREFIXES:
            continue
        try:
            resp = client.get(f"{_CROSSREF_WORKS}/{doi}", timeout=10.0)
        except (httpx.HTTPError, httpx.TimeoutException):
            continue
        if resp.status_code == 404:
            # Crossref doesn't know this DOI — likely DataCite or another registry.
            # doi.org HEAD already confirmed it resolves, so skip silently.
            continue
        if not (200 <= resp.status_code < 300):
            continue
        try:
            msg = resp.json().get("message", {})
        except ValueError:
            continue

        # ── Title ──────────────────────────────────────────────────────
        crossref_titles = msg.get("title", [])
        crossref_title = crossref_titles[0] if crossref_titles else ""
        if record.title and crossref_title:
            sim = _title_similarity(record.title, crossref_title)
            if sim < 0.60:
                issues.append(
                    CheckError(
                        citation_key=record.citation_key,
                        level="error",
                        message=(
                            f"DOI '{doi}' resolves to a different paper — "
                            f"BibTeX title: '{record.title[:55]}'; "
                            f"Crossref title: '{crossref_title[:55]}' "
                            f"({sim:.0%} match)"
                        ),
                    )
                )
                continue  # title mismatch is conclusive; skip year check
            if sim < 0.80:
                issues.append(
                    CheckError(
                        citation_key=record.citation_key,
                        level="warning",
                        message=(
                            f"DOI '{doi}' title mismatch — "
                            f"BibTeX: '{record.title[:55]}'; "
                            f"Crossref: '{crossref_title[:55]}' "
                            f"({sim:.0%} match)"
                        ),
                    )
                )

        # ── Year ───────────────────────────────────────────────────────
        date_parts = msg.get("issued", {}).get("date-parts", [[]])
        crossref_year = str(date_parts[0][0]) if date_parts and date_parts[0] else ""
        if record.year and crossref_year:
            try:
                if abs(int(record.year) - int(crossref_year)) > 2:
                    issues.append(
                        CheckError(
                            citation_key=record.citation_key,
                            level="warning",
                            message=(
                                f"DOI '{doi}' year mismatch — "
                                f"BibTeX: {record.year}, Crossref: {crossref_year}"
                            ),
                        )
                    )
            except ValueError:
                pass

    return issues


def check_bibtex(
    records: list[CitationRecord],
    client: httpx.Client | None = None,
    no_network: bool = False,
) -> ValidationReport:
    """Validate parsed BibTeX records.

    Checks performed (in order):
    1. Required-field completeness per citation type
    2. Duplicate citation keys
    3. DOI existence via HEAD to doi.org (404 → error; other non-2xx → warning)
    4. Crossref metadata agreement for resolved DOIs (title/year mismatch → error/warning)
    """
    all_errors: list[CheckError] = []
    all_warnings: list[CheckError] = []

    # Check required fields for each record
    for record in records:
        all_errors.extend(_check_required_fields(record))

    # Check for duplicate citation keys
    all_errors.extend(_check_duplicate_keys(records))

    # Optionally check DOI existence and metadata
    if not no_network and client is not None:
        non_existent_dois: set[str] = set()
        for issue in _check_doi_links(records, client):
            if issue.level == "error":
                all_errors.append(issue)
                # Track so metadata check skips these
                for r in records:
                    if r.citation_key == issue.citation_key and r.doi:
                        non_existent_dois.add(r.doi)
            else:
                all_warnings.append(issue)

        for issue in _check_doi_metadata(records, client, non_existent_dois):
            if issue.level == "error":
                all_errors.append(issue)
            else:
                all_warnings.append(issue)

    return ValidationReport(
        total_entries=len(records),
        errors=all_errors,
        warnings=all_warnings,
    )
