from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Dict, Iterable, List, Optional, Tuple

from .extract_ids import extract_ids
from .normalize import author_family_names
from .scoring import ScoreConfig, score_candidate
from .types import Candidate, EntryResult, ReferenceInput
from .util import collapse_whitespace


YEAR_RE = re.compile(r"\b(18|19|20)\d{2}\b")
TITLE_QUOTE_RE = re.compile(r"[\"“”](.+?)[\"“”]")


@dataclass
class MatchConfig:
    verified_threshold: float = 0.85
    likely_threshold: float = 0.7
    overwrite_threshold: float = 0.9
    score_config: ScoreConfig = field(default_factory=ScoreConfig)


def verify_references(
    refs: Iterable[ReferenceInput],
    providers: Iterable,
    config: MatchConfig,
) -> List[EntryResult]:
    results: List[EntryResult] = []
    for ref in refs:
        results.append(_verify_reference(ref, providers, config))
    return results


def _verify_reference(ref: ReferenceInput, providers: Iterable, config: MatchConfig) -> EntryResult:
    if ref.fields.get("error"):
        return EntryResult(
            ref_id=ref.ref_id,
            verdict="ERROR",
            score=0.0,
            reasons=[ref.fields.get("error")],
            attempts=[],
            top_candidates=[],
            patch={},
        )

    ref_fields = _augment_ref_fields(ref)
    ids = _extract_ref_ids(ref_fields, ref.raw)

    attempts = []
    candidates: List[Candidate] = []
    for provider in providers:
        provider_hits: List[Candidate] = []
        for id_type, identifier in ids.items():
            try:
                hits = provider.lookup_by_id(identifier, id_type)
            except Exception as exc:  # pragma: no cover - provider errors handled as runtime
                attempts.append({"provider": provider.provider_name, "action": "lookup", "error": str(exc)})
                continue
            attempts.append({"provider": provider.provider_name, "action": "lookup", "id_type": id_type})
            provider_hits.extend(hits)
        candidates.extend(provider_hits)
        query = _build_query(ref_fields)
        if query and not provider_hits:
            try:
                hits = provider.search(query)
            except Exception as exc:  # pragma: no cover
                attempts.append({"provider": provider.provider_name, "action": "search", "error": str(exc)})
                continue
            attempts.append({"provider": provider.provider_name, "action": "search", "query": query})
            candidates.extend(hits)

    if not candidates:
        return EntryResult(
            ref_id=ref.ref_id,
            verdict="UNVERIFIED",
            score=0.0,
            reasons=["No candidates found."],
            attempts=attempts,
            top_candidates=[],
            patch={},
        )

    scored = []
    for candidate in candidates:
        score, breakdown = score_candidate(
            ref_fields,
            {
                "title": candidate.title,
                "authors": candidate.authors,
                "year": candidate.year,
                "venue": candidate.venue,
            },
            config.score_config,
        )
        scored.append((score, breakdown, candidate))

    scored.sort(key=lambda item: item[0], reverse=True)
    best_score, best_breakdown, best_candidate = scored[0]

    hard_match = _hard_match(ids, best_candidate.ids)
    if hard_match:
        verdict = "VERIFIED"
        reason = f"Exact {hard_match} match."
    elif best_score >= config.verified_threshold:
        verdict = "VERIFIED"
        reason = "Metadata similarity above verified threshold."
    elif best_score >= config.likely_threshold:
        verdict = "LIKELY"
        reason = "Metadata similarity above likely threshold."
    else:
        verdict = "UNVERIFIED"
        reason = "No candidates above likely threshold."

    if _has_conflict(scored, ref_fields, config.likely_threshold):
        verdict = "CONFLICT"
        reason = "Multiple high-scoring candidates disagree on key fields."

    patch = _build_patch(ref_fields, best_candidate, verdict, config)

    return EntryResult(
        ref_id=ref.ref_id,
        verdict=verdict,
        score=best_score,
        reasons=[reason],
        attempts=attempts,
        top_candidates=[best_candidate],
        patch=patch,
    )


def _augment_ref_fields(ref: ReferenceInput) -> Dict[str, object]:
    fields = dict(ref.fields)
    raw = ref.raw or ""
    if "title" not in fields:
        title = _infer_title(raw)
        if title:
            fields["title"] = title
    if "year" not in fields:
        year = _infer_year(raw)
        if year:
            fields["year"] = year
    if "authors" not in fields and fields.get("author"):
        fields["authors"] = _split_authors(str(fields["author"]))
    if "venue" not in fields:
        fields["venue"] = fields.get("journal") or fields.get("booktitle") or fields.get("publisher")
    return fields


def _split_authors(author_field: str) -> list[str]:
    return [part.strip() for part in author_field.split(" and ") if part.strip()]


def _infer_year(raw: str) -> Optional[int]:
    match = YEAR_RE.search(raw)
    if not match:
        return None
    return int(match.group(0))


def _infer_title(raw: str) -> Optional[str]:
    match = TITLE_QUOTE_RE.search(raw)
    if match:
        return collapse_whitespace(match.group(1))
    year_match = re.search(r"\(\s*(\d{4})\s*\)", raw)
    if year_match:
        start = year_match.end()
        remainder = raw[start:]
        parts = remainder.split(".")
        if parts:
            return collapse_whitespace(parts[0])
    return None


def _extract_ref_ids(fields: Dict[str, object], raw: str) -> Dict[str, str]:
    ids = {}
    for key in ("doi", "arxiv", "isbn"):
        value = fields.get(key)
        if isinstance(value, str) and value:
            ids[key] = value
    if not ids:
        ids.update(extract_ids(raw))
    return ids


def _hard_match(ref_ids: Dict[str, str], candidate_ids: Dict[str, str]) -> Optional[str]:
    for key, value in ref_ids.items():
        candidate_value = candidate_ids.get(key)
        if not candidate_value:
            continue
        if key == "doi":
            if candidate_value.lower() == value.lower():
                return "doi"
        else:
            if candidate_value == value:
                return key
    return None


def _has_conflict(scored: list[tuple[float, dict, Candidate]], ref_fields: Dict[str, object], threshold: float) -> bool:
    contenders = [item for item in scored if item[0] >= threshold]
    if len(contenders) < 2:
        return False
    (score_a, _, cand_a), (score_b, _, cand_b) = contenders[:2]
    if abs((cand_a.year or 0) - (cand_b.year or 0)) > 1:
        return True
    fam_a = author_family_names(cand_a.authors)
    fam_b = author_family_names(cand_b.authors)
    if fam_a and fam_b and fam_a[0] != fam_b[0]:
        return True
    ref_doi = ref_fields.get("doi")
    if ref_doi and cand_a.ids.get("doi") and cand_b.ids.get("doi"):
        if cand_a.ids.get("doi") != cand_b.ids.get("doi"):
            return True
    return False


def _build_query(fields: Dict[str, object]) -> str:
    title = fields.get("title")
    authors = fields.get("authors") or []
    year = fields.get("year")
    parts = []
    if title:
        parts.append(str(title))
    if authors:
        parts.append(str(authors[0]))
    if year:
        parts.append(str(year))
    return collapse_whitespace(" ".join(parts))


def _build_patch(fields: Dict[str, object], candidate: Candidate, verdict: str, config: MatchConfig) -> Dict[str, object]:
    if verdict not in {"VERIFIED", "LIKELY"}:
        return {}
    patch = {}
    if "doi" not in fields and candidate.ids.get("doi"):
        patch["doi"] = candidate.ids.get("doi")
    if "url" not in fields and candidate.url:
        patch["url"] = candidate.url
    if "isbn" not in fields and candidate.ids.get("isbn"):
        patch["isbn"] = candidate.ids.get("isbn")
    if "publisher" not in fields and candidate.venue:
        patch["publisher"] = candidate.venue
    return patch
