"""BibTeX parser, renderer, and citation formatters (APA 7th, MLA 9th)."""

from __future__ import annotations

from typing import Any

from alice_cli.cite.models import CitationRecord

# ── Standard BibTeX fields mapped to CitationRecord attributes ────────────────

_BIBTEX_FIELD_MAP: dict[str, str] = {
    "title": "title",
    "author": "_authors_joined",  # special: joined with " and "
    "year": "year",
    "journal": "journal",
    "volume": "volume",
    "number": "number",
    "pages": "pages",
    "publisher": "publisher",
    "doi": "doi",
    "url": "url",
    "abstract": "abstract",
    "isbn": "isbn",
}

# Reverse map: CitationRecord attribute → BibTeX field name
_ATTR_TO_BIBTEX: dict[str, str] = {
    v: k for k, v in _BIBTEX_FIELD_MAP.items() if v != "_authors_joined"
}
_ATTR_TO_BIBTEX["authors"] = "author"

# Canonical field ordering for render_bibtex output
_FIELD_ORDER: list[str] = [
    "author",
    "title",
    "year",
    "journal",
    "volume",
    "number",
    "pages",
    "publisher",
    "doi",
    "url",
    "abstract",
    "isbn",
]

# ── BibTeX Parser ─────────────────────────────────────────────────────────────


class _BibTeXLexer:
    """Minimal lexer for BibTeX input."""

    def __init__(self, text: str) -> None:
        self.text = text
        self.pos = 0

    def _skip_whitespace(self) -> None:
        while self.pos < len(self.text) and self.text[self.pos] in " \t\n\r":
            self.pos += 1

    def peek(self) -> str | None:
        self._skip_whitespace()
        if self.pos >= len(self.text):
            return None
        return self.text[self.pos]

    def advance(self) -> str:
        self._skip_whitespace()
        ch = self.text[self.pos]
        self.pos += 1
        return ch

    def expect(self, ch: str) -> bool:
        self._skip_whitespace()
        if self.pos < len(self.text) and self.text[self.pos] == ch:
            self.pos += 1
            return True
        return False

    def read_braced_value(self) -> str:
        """Read a brace-delimited value: { ... }, handling nested braces."""
        if not self.expect("{"):
            return ""
        depth = 1
        start = self.pos
        while self.pos < len(self.text) and depth > 0:
            ch = self.text[self.pos]
            if ch == "{":
                depth += 1
            elif ch == "}":
                depth -= 1
            self.pos += 1
        # pos is now past the closing brace
        return self.text[start : self.pos - 1]

    def read_quoted_value(self) -> str:
        """Read a quote-delimited value: "..."."""
        if not self.expect('"'):
            return ""
        start = self.pos
        while self.pos < len(self.text) and self.text[self.pos] != '"':
            self.pos += 1
        value = self.text[start : self.pos]
        self.pos += 1  # skip closing quote
        return value

    def read_identifier(self) -> str:
        """Read an alphanumeric identifier (entry type, field name, or bare value)."""
        self._skip_whitespace()
        start = self.pos
        while self.pos < len(self.text) and (
            self.text[self.pos].isalnum() or self.text[self.pos] in "_-"
        ):
            self.pos += 1
        return self.text[start : self.pos]

    def read_citation_key(self) -> str:
        """Read a citation key (everything up to the first comma or closing brace)."""
        self._skip_whitespace()
        start = self.pos
        while self.pos < len(self.text) and self.text[self.pos] not in ",}":
            self.pos += 1
        return self.text[start : self.pos].strip()


def _parse_entry(lexer: _BibTeXLexer) -> CitationRecord | None:
    """Parse a single BibTeX entry starting after the '@' character."""
    entry_type = lexer.read_identifier().lower()
    if not entry_type:
        return None

    if not lexer.expect("{"):
        return None

    citation_key = lexer.read_citation_key()
    if not citation_key:
        return None

    if not lexer.expect(","):
        # Entry with just a key and no fields — still valid
        lexer.expect("}")
        return CitationRecord(entry_type=entry_type, citation_key=citation_key)

    fields: dict[str, str] = {}
    while True:
        lexer._skip_whitespace()
        p = lexer.peek()
        if p is None or p == "}":
            lexer.expect("}")
            break

        field_name = lexer.read_identifier().lower()
        if not field_name:
            # Skip unexpected characters
            lexer.advance()
            continue

        lexer._skip_whitespace()
        if not lexer.expect("="):
            continue

        lexer._skip_whitespace()
        p = lexer.peek()
        if p == "{":
            value = lexer.read_braced_value()
        elif p == '"':
            value = lexer.read_quoted_value()
        else:
            # Bare value (e.g., a number or a macro)
            value = lexer.read_identifier()

        fields[field_name] = value

        # Consume optional comma between fields
        lexer.expect(",")

    return _fields_to_record(entry_type, citation_key, fields)


def _fields_to_record(entry_type: str, citation_key: str, fields: dict[str, str]) -> CitationRecord:
    """Map parsed BibTeX fields to a CitationRecord, putting unknown fields in extra_fields."""
    standard_fields = set(_BIBTEX_FIELD_MAP.keys())
    extra: dict[str, str] = {}
    kwargs: dict[str, Any] = {
        "entry_type": entry_type,
        "citation_key": citation_key,
    }

    for field_name, value in fields.items():
        if field_name in standard_fields:
            attr = _BIBTEX_FIELD_MAP[field_name]
            if attr == "_authors_joined":
                # Split "Last, First and Last2, First2" into list
                kwargs["authors"] = [a.strip() for a in value.split(" and ") if a.strip()]
            else:
                kwargs[attr] = value
        else:
            extra[field_name] = value

    kwargs["extra_fields"] = extra
    return CitationRecord(**kwargs)


def parse_bibtex(text: str) -> list[CitationRecord]:
    """Parse a BibTeX string into a list of CitationRecord objects.

    Uses a recursive-descent approach. Unparseable entries are skipped gracefully.
    """
    records: list[CitationRecord] = []
    lexer = _BibTeXLexer(text)

    while lexer.pos < len(lexer.text):
        lexer._skip_whitespace()
        if lexer.pos >= len(lexer.text):
            break

        if lexer.text[lexer.pos] == "@":
            lexer.pos += 1  # consume '@'
            save_pos = lexer.pos
            try:
                record = _parse_entry(lexer)
                if record is not None:
                    records.append(record)
            except Exception:
                # Skip unparseable entry — advance past the failed region
                lexer.pos = save_pos
                _skip_to_next_entry(lexer)
        else:
            # Skip non-entry text
            lexer.pos += 1

    return records


def _skip_to_next_entry(lexer: _BibTeXLexer) -> None:
    """Advance the lexer past the current (broken) entry to the next '@'."""
    depth = 0
    while lexer.pos < len(lexer.text):
        ch = lexer.text[lexer.pos]
        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth <= 0:
                lexer.pos += 1
                return
        elif ch == "@" and depth == 0:
            return
        lexer.pos += 1


# ── BibTeX Renderer ───────────────────────────────────────────────────────────


def render_bibtex(record: CitationRecord) -> str:
    """Render a CitationRecord as a BibTeX entry string.

    Uses consistent 2-space indentation and canonical field ordering.
    """
    lines: list[str] = []
    lines.append(f"@{record.entry_type}{{{record.citation_key},")

    # Collect all fields to render
    field_values: dict[str, str] = {}

    # Standard fields
    if record.authors:
        field_values["author"] = " and ".join(record.authors)
    if record.title:
        field_values["title"] = record.title
    if record.year:
        field_values["year"] = record.year
    if record.journal:
        field_values["journal"] = record.journal
    if record.volume:
        field_values["volume"] = record.volume
    if record.number:
        field_values["number"] = record.number
    if record.pages:
        field_values["pages"] = record.pages
    if record.publisher:
        field_values["publisher"] = record.publisher
    if record.doi:
        field_values["doi"] = record.doi
    if record.url:
        field_values["url"] = record.url
    if record.abstract:
        field_values["abstract"] = record.abstract
    if record.isbn:
        field_values["isbn"] = record.isbn

    # Extra fields (non-standard)
    for key, value in record.extra_fields.items():
        field_values[key] = value

    # Render in canonical order, then extras alphabetically
    ordered_keys: list[str] = []
    for key in _FIELD_ORDER:
        if key in field_values:
            ordered_keys.append(key)
    for key in sorted(field_values.keys()):
        if key not in ordered_keys:
            ordered_keys.append(key)

    for i, key in enumerate(ordered_keys):
        value = field_values[key]
        comma = "," if i < len(ordered_keys) - 1 else ""
        lines.append(f"  {key} = {{{value}}}{comma}")

    lines.append("}")
    return "\n".join(lines)


# ── APA 7th Edition Formatter ────────────────────────────────────────────────


def _format_apa_authors(authors: list[str]) -> str:
    """Format author list for APA style.

    APA 7th: Last, F. M., Last2, F. M., & Last3, F. M.
    """
    if not authors:
        return ""

    formatted: list[str] = []
    for author in authors:
        parts = [p.strip() for p in author.split(",", 1)]
        if len(parts) == 2:
            last = parts[0]
            firsts = parts[1].strip().split()
            initials = " ".join(f"{f[0]}." for f in firsts if f)
            formatted.append(f"{last}, {initials}" if initials else last)
        else:
            formatted.append(parts[0])

    if len(formatted) == 1:
        return formatted[0]
    if len(formatted) == 2:
        return f"{formatted[0]}, & {formatted[1]}"
    return ", ".join(formatted[:-1]) + ", & " + formatted[-1]


def format_apa(record: CitationRecord) -> str:
    """Format a CitationRecord in APA 7th edition style.

    Pattern: Author (Year). Title. Journal, Volume(Number), Pages.
    """
    parts: list[str] = []

    # Author
    author_str = _format_apa_authors(record.authors)
    if author_str:
        parts.append(author_str)

    # Year
    if record.year:
        parts.append(f"({record.year}).")
    else:
        parts.append("(n.d.).")

    # Title
    if record.title:
        title = record.title
        # APA: only first word capitalized for article titles (sentence case)
        # but we preserve the original casing from the record
        parts.append(f"{title}.")

    # Source: Journal, Volume(Number), Pages.
    source_parts: list[str] = []
    if record.journal:
        source_parts.append(record.journal)
    if record.volume:
        vol = record.volume
        if record.number:
            vol += f"({record.number})"
        source_parts.append(vol)
    if record.pages:
        source_parts.append(record.pages)

    if source_parts:
        parts.append(", ".join(source_parts) + ".")

    # DOI
    if record.doi:
        parts.append(f"https://doi.org/{record.doi}")

    return " ".join(parts)


# ── MLA 9th Edition Formatter ────────────────────────────────────────────────


def _format_mla_authors(authors: list[str]) -> str:
    """Format author list for MLA style.

    MLA 9th: Last, First. (one author)
             Last, First, and First Last. (two authors)
             Last, First, et al. (three or more)
    """
    if not authors:
        return ""

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

    if len(authors) == 2:
        # First author: Last, First; second author: First Last
        first = authors[0]
        parts = [p.strip() for p in authors[1].split(",", 1)]
        second = f"{parts[1].strip()} {parts[0]}" if len(parts) == 2 else parts[0]
        return f"{first}, and {second}"

    # Three or more: first author et al.
    return f"{authors[0]}, et al."


def format_mla(record: CitationRecord) -> str:
    """Format a CitationRecord in MLA 9th edition style.

    Pattern: Author. "Title." Container, Version, Publisher, Date.
    """
    parts: list[str] = []

    # Author
    author_str = _format_mla_authors(record.authors)
    if author_str:
        # Ensure author ends with period
        if not author_str.endswith("."):
            author_str += "."
        parts.append(author_str)

    # Title in quotes
    if record.title:
        title = record.title.rstrip(".")
        parts.append(f'"{title}."')

    # Container (journal) in italics (using markdown-style asterisks)
    container_parts: list[str] = []
    if record.journal:
        container_parts.append(f"*{record.journal}*")
    if record.volume:
        vol = f"vol. {record.volume}"
        if record.number:
            vol += f", no. {record.number}"
        container_parts.append(vol)
    if record.pages:
        container_parts.append(f"pp. {record.pages}")
    if record.publisher:
        container_parts.append(record.publisher)
    if record.year:
        container_parts.append(record.year)

    if container_parts:
        parts.append(", ".join(container_parts) + ".")

    # DOI
    if record.doi:
        parts.append(f"https://doi.org/{record.doi}")

    return " ".join(parts)


# ── Chicago 17th Edition Notes-Bibliography Formatter ─────────────────────────


def _format_chicago_authors(authors: list[str]) -> str:
    """Format author list for Chicago Notes-Bibliography style.

    Chicago: Last, First. (one author)
             Last, First, and First Last. (two authors)
             Last, First, First Last, and First Last. (three or more)
    """
    if not authors:
        return ""

    formatted: list[str] = []
    for i, author in enumerate(authors):
        parts = [p.strip() for p in author.split(",", 1)]
        if i == 0:
            # First author: Last, First
            formatted.append(author)
        else:
            # Subsequent authors: First Last
            if len(parts) == 2:
                formatted.append(f"{parts[1].strip()} {parts[0]}")
            else:
                formatted.append(parts[0])

    if len(formatted) == 1:
        return formatted[0]
    if len(formatted) == 2:
        return f"{formatted[0]}, and {formatted[1]}"
    return ", ".join(formatted[:-1]) + ", and " + formatted[-1]


def format_chicago(record: CitationRecord) -> str:
    """Format a CitationRecord in Chicago 17th edition Notes-Bibliography style.

    Pattern: Last, First. "Title." Journal Volume, no. Number (Year): Pages.
    """
    parts: list[str] = []

    # Author
    author_str = _format_chicago_authors(record.authors)
    if author_str:
        if not author_str.endswith("."):
            author_str += "."
        parts.append(author_str)

    # Title in quotes
    if record.title:
        title = record.title.rstrip(".")
        parts.append(f'"{title}."')

    # Journal, Volume, no. Number (Year): Pages.
    source_parts: list[str] = []
    if record.journal:
        journal_str = record.journal
        if record.volume:
            journal_str += f" {record.volume}"
        if record.number:
            journal_str += f", no. {record.number}"
        if record.year:
            journal_str += f" ({record.year})"
        if record.pages:
            journal_str += f": {record.pages}"
        source_parts.append(journal_str)
    elif record.year:
        # No journal but has year
        source_parts.append(record.year)

    if source_parts:
        parts.append(". ".join(source_parts) + ".")

    # DOI
    if record.doi:
        parts.append(f"https://doi.org/{record.doi}")

    return " ".join(parts)


# ── IEEE Citation Style Formatter ─────────────────────────────────────────────


def _format_ieee_authors(authors: list[str]) -> str:
    """Format author list for IEEE style.

    IEEE: F. Last (one author)
          F. Last and F. Last (two authors)
          F. Last, F. Last, and F. Last (three or more)
    """
    if not authors:
        return ""

    formatted: list[str] = []
    for author in authors:
        parts = [p.strip() for p in author.split(",", 1)]
        if len(parts) == 2:
            last = parts[0]
            firsts = parts[1].strip().split()
            initials = " ".join(f"{f[0]}." for f in firsts if f)
            formatted.append(f"{initials} {last}" if initials else last)
        else:
            formatted.append(parts[0])

    if len(formatted) == 1:
        return formatted[0]
    if len(formatted) == 2:
        return f"{formatted[0]} and {formatted[1]}"
    return ", ".join(formatted[:-1]) + ", and " + formatted[-1]


def format_ieee(record: CitationRecord) -> str:
    """Format a CitationRecord in IEEE citation style.

    Pattern: [N] F. Last, "Title," Journal, vol. V, no. N, pp. P, Year.
    Includes bracketed reference number (defaults to [1]).
    """
    parts: list[str] = []

    # Bracketed reference number
    parts.append("[1]")

    # Author
    author_str = _format_ieee_authors(record.authors)
    if author_str:
        parts.append(f"{author_str},")

    # Title in quotes with trailing comma inside
    if record.title:
        title = record.title.rstrip(".")
        parts.append(f'"{title},"')

    # Journal, vol. V, no. N, pp. P, Year.
    source_parts: list[str] = []
    if record.journal:
        source_parts.append(record.journal)
    if record.volume:
        source_parts.append(f"vol. {record.volume}")
    if record.number:
        source_parts.append(f"no. {record.number}")
    if record.pages:
        source_parts.append(f"pp. {record.pages}")
    if record.year:
        source_parts.append(record.year)

    if source_parts:
        parts.append(", ".join(source_parts) + ".")

    return " ".join(parts)


# ── Vancouver (ICMJE) Citation Style Formatter ───────────────────────────────


def _format_vancouver_authors(authors: list[str]) -> str:
    """Format author list for Vancouver style.

    Vancouver: Last FM. (initials without periods/spaces)
               Last FM, Last FM. (multiple authors, comma-separated)
    """
    if not authors:
        return ""

    formatted: list[str] = []
    for author in authors:
        parts = [p.strip() for p in author.split(",", 1)]
        if len(parts) == 2:
            last = parts[0]
            firsts = parts[1].strip().split()
            # Vancouver uses initials without periods or spaces
            initials = "".join(f[0].upper() for f in firsts if f)
            formatted.append(f"{last} {initials}" if initials else last)
        else:
            formatted.append(parts[0])

    return ", ".join(formatted)


def format_vancouver(record: CitationRecord) -> str:
    """Format a CitationRecord in Vancouver (ICMJE) citation style.

    Pattern: Last FM. Title. Journal. Year;Volume(Number):Pages.
    """
    parts: list[str] = []

    # Author
    author_str = _format_vancouver_authors(record.authors)
    if author_str:
        if not author_str.endswith("."):
            author_str += "."
        parts.append(author_str)

    # Title (no quotes, ends with period)
    if record.title:
        title = record.title.rstrip(".")
        parts.append(f"{title}.")

    # Journal. Year;Volume(Number):Pages.
    if record.journal:
        journal_str = record.journal
        journal_str += "."

        # Year;Volume(Number):Pages
        loc_parts: list[str] = []
        if record.year:
            loc = record.year
            if record.volume:
                loc += f";{record.volume}"
                if record.number:
                    loc += f"({record.number})"
            if record.pages:
                loc += f":{record.pages}"
            loc_parts.append(loc)

        if loc_parts:
            journal_str += " " + "".join(loc_parts)

        parts.append(journal_str + ".")
    elif record.year:
        parts.append(f"{record.year}.")

    # DOI
    if record.doi:
        parts.append(f"doi:{record.doi}")

    return " ".join(parts)


# ── Format Dispatcher ─────────────────────────────────────────────────────────

_FORMAT_REGISTRY: dict[str, type[object] | None] = {}


def format_citation(record: CitationRecord, output_format: str, *, annotate: bool = False) -> str:
    """Render a single CitationRecord in the specified format.

    Supported formats: "bibtex", "apa", "mla", "chicago", "ieee", "vancouver".
    When annotate=True, includes abstract as annotation (annote field in BibTeX,
    appended paragraph in other formats). Omits annotation silently when abstract is empty.
    Raises ValueError for unknown formats.
    """
    fmt = output_format.lower()

    _fmt_fn = {
        "apa": format_apa,
        "mla": format_mla,
        "chicago": format_chicago,
        "ieee": format_ieee,
        "vancouver": format_vancouver,
    }

    if fmt != "bibtex" and fmt not in _fmt_fn:
        raise ValueError(
            f"Unknown output format: {output_format!r}. "
            f"Expected 'bibtex', 'apa', 'mla', 'chicago', 'ieee', or 'vancouver'."
        )

    if fmt == "bibtex":
        if annotate and record.abstract:
            # Build a copy with annote in extra_fields for render_bibtex
            extra = dict(record.extra_fields)
            extra["annote"] = record.abstract
            annotated_record = record.model_copy(update={"extra_fields": extra})
            return render_bibtex(annotated_record)
        return render_bibtex(record)

    # Non-BibTeX format
    result = _fmt_fn[fmt](record)

    if annotate and record.abstract:
        result += "\n\n  " + record.abstract

    return result
