"""Deterministic BibEntry merge + coherence check.

Merges multiple ``BibEntry`` objects representing the same paper
from different sources into a single entry with the best fields.

Merge strategy (field priority):
    - DOI ← CrossRef (highest confidence)
    - eprint ← arXiv (native)
    - venue ← DBLP (curated)
    - abstract ← longest available
    - key ← first entry
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from difflib import SequenceMatcher

from axm_bib.exceptions import MergeConflict
from axm_bib.models import BibEntry

__all__ = ["check_coherence", "merge_entries"]

logger = logging.getLogger(__name__)


# ── Internal merge state ─────────────────────────────────────────────────


@dataclass
class _MergeState:
    """Mutable accumulator used by ``merge_entries``."""

    doi: str | None
    title: str | None
    authors: list[str]
    year: int | None
    raw_parts: list[str] = field(default_factory=list)


def _accumulate(state: _MergeState, entry: BibEntry) -> None:
    """Merge one entry's fields into the accumulator."""
    if state.doi is None and entry.doi is not None:
        state.doi = entry.doi

    if entry.title is not None:
        if state.title is None or len(entry.title) > len(state.title):
            state.title = entry.title

    if len(entry.authors) > len(state.authors):
        state.authors = list(entry.authors)

    if state.year is None and entry.year is not None:
        state.year = entry.year

    state.raw_parts.append(entry.raw)


# Entry type priority: higher index = better (published > preprint)
_ENTRY_TYPE_PRIORITY: dict[str, int] = {
    "misc": 0,
    "techreport": 1,
    "incollection": 2,
    "inproceedings": 3,
    "article": 4,
}

# Regex to extract BibTeX field values: `fieldname = {value}`
_FIELD_RE = re.compile(r"""(?:,|\n)\s*(\w+)\s*=\s*\{([^}]*)\}""")


def _extract_fields(raw: str) -> dict[str, str]:
    """Extract field→value mapping from a raw BibTeX string."""
    return {m.group(1).lower(): m.group(2) for m in _FIELD_RE.finditer(raw)}


def _best_entry_type(entries: list[BibEntry]) -> str:
    """Return the highest-priority entry type among entries."""
    best_type = entries[0].entry_type
    best_rank = _ENTRY_TYPE_PRIORITY.get(best_type, -1)
    for entry in entries[1:]:
        rank = _ENTRY_TYPE_PRIORITY.get(entry.entry_type, -1)
        if rank > best_rank:
            best_type = entry.entry_type
            best_rank = rank
    return best_type


def _build_merged_raw(base: BibEntry, entries: list[BibEntry]) -> str:
    """Build a single merged raw BibTeX string from multiple entries.

    Uses the base entry's raw as template, then injects missing fields
    from other entries.  Promotes the entry type to the best available.

    Args:
        base: The primary entry (provides key and structure).
        entries: All entries including base.

    Returns:
        Single raw BibTeX string with all fields merged.
    """
    # Collect all fields from all entries (base fields take priority)
    base_fields = _extract_fields(base.raw)
    extra_fields: dict[str, str] = {}
    for entry in entries[1:]:
        for fname, value in _extract_fields(entry.raw).items():
            if fname not in base_fields and fname not in extra_fields:
                extra_fields[fname] = value

    # Find the closing brace in base raw
    last_brace = base.raw.rfind("}")
    if last_brace == -1:
        # Malformed raw — graceful fallback to concatenation
        logger.warning("Malformed BibTeX (no closing brace), falling back")
        return "\n\n".join(e.raw for e in entries)

    # Inject extra fields before closing brace
    injected = base.raw[:last_brace]
    for fname, value in extra_fields.items():
        injected += f"  {fname} = {{{value}}},\n"
    injected += base.raw[last_brace:]

    # Promote entry type
    best_type = _best_entry_type(entries)
    if best_type != base.entry_type:
        injected = re.sub(
            rf"@{re.escape(base.entry_type)}\{{",
            f"@{best_type}{{",
            injected,
            count=1,
            flags=re.IGNORECASE,
        )

    return injected


# ── Public API ───────────────────────────────────────────────────────────


def merge_entries(entries: list[BibEntry]) -> BibEntry:
    """Merge N BibEntry objects into a single deterministic entry.

    The first entry provides the base (key, entry_type, raw).
    Subsequent entries fill in missing fields using a priority
    strategy: DOI from any, title from longest, authors from
    longest list, year from any.

    Args:
        entries: One or more BibEntry objects to merge.

    Returns:
        Merged ``BibEntry``.

    Raises:
        MergeConflict: If entries list is empty.
    """
    if not entries:
        msg = "Cannot merge empty list of entries"
        raise MergeConflict(msg)

    if len(entries) == 1:
        return entries[0]

    base = entries[0]
    state = _MergeState(
        doi=base.doi,
        title=base.title,
        authors=list(base.authors),
        year=base.year,
        raw_parts=[base.raw],
    )

    for entry in entries[1:]:
        _accumulate(state, entry)

    return BibEntry(
        key=base.key,
        entry_type=_best_entry_type(entries),
        raw=_build_merged_raw(base, entries),
        doi=state.doi,
        title=state.title,
        authors=state.authors,
        year=state.year,
    )


def check_coherence(entries: list[BibEntry]) -> bool:
    """Check whether multiple BibEntries refer to the same paper.

    Coherence requires:
        1. Title similarity ≥ 0.8 (normalized, case-insensitive, no punctuation)
        2. First author surname match (case-insensitive)

    Args:
        entries: Two or more entries to check.

    Returns:
        ``True`` if all entries are coherent, ``False`` otherwise.
    """
    if len(entries) < 2:  # noqa: PLR2004
        return True

    ref = entries[0]
    ref_title = _normalize_title(ref.title or "")
    ref_surname = _extract_surname(ref.authors[0] if ref.authors else "")

    for entry in entries[1:]:
        entry_title = _normalize_title(entry.title or "")
        entry_surname = _extract_surname(entry.authors[0] if entry.authors else "")

        # Title similarity check
        ratio = SequenceMatcher(None, ref_title, entry_title).ratio()
        if ratio < 0.8:  # noqa: PLR2004
            return False

        # First author surname check
        if ref_surname.lower() != entry_surname.lower():
            return False

    return True


# ── Private helpers ──────────────────────────────────────────────────────


def _normalize_title(title: str) -> str:
    """Normalize title for comparison: lowercase, strip punctuation."""
    return re.sub(r"[^\w\s]", "", title.lower().strip())


def _extract_surname(author: str) -> str:
    """Extract surname from author string.

    Handles formats:
        - ``Last, First`` → ``Last``
        - ``First Last`` → ``Last``
        - ``First Middle Last`` → ``Last``
    """
    if not author:
        return ""

    # Handle "Last, First" format
    if "," in author:
        return author.split(",")[0].strip()

    # Handle "First Last" or "First Middle Last" format
    parts = author.strip().split()
    return parts[-1] if parts else ""
