"""Reference extraction: section detection, entry segmentation, field parsing."""
from __future__ import annotations

import re
from ..types import Reference, TextLine, DocumentBlock, BlockType, BBox
from ..utils.geometry import merge_bboxes


_REF_HEADINGS = {"references", "bibliography", "works cited", "literature cited"}

# Sentinel to protect middle initials from period-splitting
_INIT_SENTINEL = "\x00"


def _split_on_sentence_periods(text: str) -> list[str]:
    """Split text on sentence-ending periods, preserving middle initials.

    "Quoc V. Le" stays intact; "Neural networks. In Proc." splits correctly.
    """
    # Protect middle initials: single uppercase letter followed by period
    protected = re.sub(r"(?<=[A-Z])\.\s", _INIT_SENTINEL, text)
    parts = [p.strip() for p in protected.split(".") if p.strip()]
    # Restore initials
    return [p.replace(_INIT_SENTINEL, ". ") for p in parts]


def is_references_heading(text: str) -> bool:
    """Check if text is a References section heading."""
    t = text.strip().lower()
    # Strip leading numbering (e.g., "7 References", "VII. References")
    t = re.sub(r'^(\d+[\s.]*|[ivx]+[\s.]+)', '', t).strip()
    if t in _REF_HEADINGS:
        return True
    # Match "References and Notes", "References and Further Reading", etc.
    if t.startswith("references"):
        return True
    return False


def find_references_section(blocks: list[DocumentBlock]) -> int | None:
    """Find the index where the References section starts.

    First checks heading blocks, then falls back to scanning body blocks
    for bold "References" text.
    """
    # Primary: look in heading blocks
    for i, block in enumerate(blocks):
        if block.block_type in (BlockType.HEADING, BlockType.TITLE, BlockType.SUBHEADING):
            if is_references_heading(block.text):
                return i

    # Fallback: scan body blocks for bold "References" line
    for i, block in enumerate(blocks):
        if block.block_type != BlockType.BODY:
            continue
        for line in block.lines:
            text = line.text.strip()
            if is_references_heading(text):
                # Check if any span is bold
                has_bold = any(s.is_bold for s in line.spans if s.text.strip())
                if has_bold:
                    return i

    return None


# Matches: [1], [23], [123], [ADG+16], [BBDIW20], [AI19], [RWC+19a]
# Also matches bare [N] at end of line (when key is on its own line)
_CITE_KEY_PATTERN = re.compile(
    r"^\[(\d{1,4}|[A-Za-z][A-Za-z0-9+*]{0,10}\d{2,4}[a-z]?)\](?:[\s.,]|$)"
)

# Also matches simple numbered: "1. " or "1) "
_NUM_PATTERN = re.compile(r"^\d{1,3}[.)]\s")


def extract_reference_entries(lines: list[TextLine]) -> list[str]:
    """Segment lines into individual reference entries.

    Detection strategy (in order of preference):
    1. Bracketed citation keys: [1], [ADG+16], etc.
    2. Hanging indent: new entries at left margin, continuations indented
    3. Extra vertical space between entries
    """
    if not lines:
        return []

    # Filter empty lines
    valid_lines = [l for l in lines if l.text.strip()]
    if not valid_lines:
        return []

    # Detect reference style from first few lines
    keyed_count = sum(1 for l in valid_lines[:20] if _CITE_KEY_PATTERN.match(l.text.strip()))
    use_keys = keyed_count >= 2

    # Cluster x0 positions to find entry-start vs continuation indent
    x0_values = sorted(set(round(l.bbox.x0, 1) for l in valid_lines))
    left_margin = x0_values[0] if x0_values else 0

    # Find the most common continuation indent (second cluster of x0)
    continuation_x0 = None
    if len(x0_values) >= 2:
        # Group x0 values by proximity
        groups = []
        current_group = [x0_values[0]]
        for x in x0_values[1:]:
            if x - current_group[-1] < 5:
                current_group.append(x)
            else:
                groups.append(current_group)
                current_group = [x]
        groups.append(current_group)

        if len(groups) >= 2:
            # Count lines at each group
            group_counts = []
            for g in groups:
                g_min, g_max = min(g), max(g)
                count = sum(1 for l in valid_lines if g_min - 2 <= l.bbox.x0 <= g_max + 2)
                group_counts.append((count, g))
            group_counts.sort(reverse=True)

            # Entry margin = leftmost group, continuation = second group
            entry_group = min(groups, key=lambda g: min(g))
            left_margin = min(entry_group)
            if len(groups) >= 2:
                cont_groups = [g for g in groups if min(g) > left_margin + 3]
                if cont_groups:
                    continuation_x0 = min(min(g) for g in cont_groups)

    # Determine normal line spacing for gap detection
    gaps = []
    for i in range(1, len(valid_lines)):
        g = valid_lines[i].bbox.y0 - valid_lines[i - 1].bbox.y1
        # Skip cross-page gaps (negative or huge)
        if 0 < g < 50:
            gaps.append(g)

    if gaps:
        gaps_sorted = sorted(gaps)
        normal_gap = gaps_sorted[len(gaps_sorted) // 4]  # 25th percentile
        entry_gap_threshold = normal_gap * 1.8
    else:
        entry_gap_threshold = 15.0

    entries: list[str] = []
    current: list[str] = []

    for i, line in enumerate(valid_lines):
        text = line.text.strip()
        is_new_entry = False

        if use_keys:
            if _CITE_KEY_PATTERN.match(text):
                is_new_entry = True
        else:
            # Hanging indent detection
            if current:
                curr_x0 = line.bbox.x0
                at_left = abs(curr_x0 - left_margin) < 5

                if at_left:
                    # Check if previous line was indented (continuation)
                    if i > 0:
                        prev_x0 = valid_lines[i - 1].bbox.x0
                        if continuation_x0 and prev_x0 > left_margin + 3:
                            is_new_entry = True
                        elif prev_x0 > curr_x0 + 3:
                            is_new_entry = True

                # Extra vertical space (but not cross-page)
                if i > 0 and not is_new_entry:
                    gap = line.bbox.y0 - valid_lines[i - 1].bbox.y1
                    if 0 < gap > entry_gap_threshold:
                        is_new_entry = True
                    # Cross-page: large gap or negative gap = new page
                    if gap < -100 and at_left:
                        is_new_entry = True

        if is_new_entry and current:
            entries.append(_join_ref_lines(current))
            current = [text]
        else:
            current.append(text)

    if current:
        entries.append(_join_ref_lines(current))

    # Filter out garbage entries (appendix headings, stray characters)
    entries = [e for e in entries if len(e) > 5]

    return entries


def _join_ref_lines(lines: list[str]) -> str:
    """Join reference entry lines with dehyphenation."""
    if not lines:
        return ""
    result = lines[0]
    for line in lines[1:]:
        if result.endswith("-") and line and line[0].islower():
            result = result[:-1] + line
        else:
            result = result + " " + line
    return result


def parse_reference(raw: str) -> Reference:
    """Parse a raw reference string into structured fields."""
    ref = Reference(raw=raw)

    # Extract bracketed key
    key_match = re.match(r"^\[([^\]]+)\]\s*", raw)
    if key_match:
        key = key_match.group(1)
        if key.isdigit():
            ref.number = int(key)
        raw = raw[key_match.end():].strip()

    # Try: Authors (Year). Title. Venue.
    year_match = re.search(r"\((\d{4}[a-z]?)\)", raw)
    if year_match:
        ref.year = year_match.group(1)
        ref.authors = raw[:year_match.start()].strip().rstrip(",").strip()
        rest = raw[year_match.end():].strip().lstrip(".").strip()
        parts = _split_on_sentence_periods(rest)
        if parts:
            ref.title = parts[0]
            if len(parts) > 1:
                ref.venue = ". ".join(parts[1:])
        return ref

    # Try: Authors. Year. Title. Venue. (ACL/NLP style: bare year after author)
    bare_year_match = re.search(r"[.]\s+((?:19|20)\d{2}[a-z]?)\s*[.]", raw)
    if bare_year_match:
        ref.year = bare_year_match.group(1)
        ref.authors = raw[:bare_year_match.start()].strip().rstrip(".").strip()
        rest = raw[bare_year_match.end():].strip()
        parts = _split_on_sentence_periods(rest)
        if parts:
            ref.title = parts[0]
            if len(parts) > 1:
                ref.venue = ". ".join(parts[1:])
        return ref

    # Try: Authors. Title. Venue, Year.  OR  Title, Year. Page
    year_match2 = re.search(r",?\s*((?:19|20)\d{2}[a-z]?)\s*[.,]?\s*$", raw)
    if year_match2:
        ref.year = year_match2.group(1)
        rest = raw[:year_match2.start()].strip()
        parts = _split_on_sentence_periods(rest)
        if len(parts) >= 2:
            ref.authors = parts[0]
            ref.title = parts[1]
            if len(parts) > 2:
                ref.venue = ". ".join(parts[2:])
        elif parts:
            ref.title = parts[0]
        return ref

    # Try: general year search — find any 4-digit year in the text
    year_general = re.search(r"[,.\s]((?:19|20)\d{2}[a-z]?)[.,\s]", raw)
    if year_general:
        ref.year = year_general.group(1)
        rest = raw[:year_general.start()].strip()
        parts = _split_on_sentence_periods(rest)
        if len(parts) >= 2:
            ref.authors = parts[0]
            ref.title = parts[1]
            if len(parts) > 2:
                ref.venue = ". ".join(parts[2:])
        elif parts:
            ref.title = parts[0]
        return ref

    # Fallback
    parts = _split_on_sentence_periods(raw)
    if len(parts) >= 3:
        ref.authors = parts[0]
        ref.title = parts[1]
        ref.venue = ". ".join(parts[2:])
    elif len(parts) == 2:
        ref.authors = parts[0]
        ref.title = parts[1]
    elif parts:
        ref.title = parts[0]

    return ref


def references_to_markdown(refs: list[Reference]) -> str:
    """Render references as Markdown list."""
    lines = []
    for ref in refs:
        if ref.number:
            prefix = f"[{ref.number}] "
        else:
            prefix = "- "

        parts = []
        if ref.authors:
            parts.append(ref.authors)
        if ref.year:
            parts.append(f"({ref.year})")
        if ref.title:
            parts.append(f"*{ref.title}*")
        if ref.venue:
            parts.append(ref.venue)

        if parts:
            lines.append(prefix + ". ".join(parts) + ".")
        else:
            lines.append(prefix + ref.raw)
    return "\n".join(lines)
