"""Filing metadata: what a document is, derived and displayed.

A document carries filing_type, year, period and version as columns. Nothing
displayable is stored beyond them: the label a card, the workspace header or
an export filename shows is derived here at read time, so grooming the
metadata corrects every label at once.

`infer` pre-fills the metadata from a filename. It is a convenience and never
authoritative — the upload form shows what it guessed and any field the user
touches is pinned. The same rules live in `ui/src/metadata.js` for the form;
`tests/playground/test_metadata.py` runs one case list through both so the
two implementations cannot drift apart silently.

The identity tuple (folder, filing_type, year, period, version) is what the
overwrite flow keys on. It only exists when filing_type, year and period are
all present, and it compares case-insensitively; `tuple_key` is that rule in
one place. `overlap_key` is the same tuple without the version — two versions
of one filing share it, which is exactly what the library's possible-duplicate
flag reports.
"""

from __future__ import annotations

import re
from typing import Any, Dict, Iterable, Optional, Tuple

#: The normalized identity tuple: (folder, filing_type, year, period, version).
TupleKey = Tuple[str, str, int, str, Optional[int]]

PERIODS = ("Q1", "Q2", "Q3", "Q4", "H1", "H2", "FY")

# (pattern, groups) pairs tried in order; the first hit decides the period.
# Compact quarter-year compounds come first so "q126" reads as Q1 2026 rather
# than as a bare Q1 with the year lost.
_PERIOD_PATTERNS: list[tuple[re.Pattern[str], str]] = [
    (re.compile(r"(?<!\d)([1-4])\s*q\s*(20\d{2})(?!\d)", re.I), "q-year"),
    (re.compile(r"(?<!\d)(20\d{2})[-_. ]?q([1-4])(?!\d)", re.I), "year-q"),
    (re.compile(r"(?<![a-z0-9])q([1-4])[-_. ]?(\d{2})(?!\d)", re.I), "q-yy"),
    (re.compile(r"(?<![a-z0-9])q([1-4])(?![0-9])", re.I), "q"),
    (re.compile(r"(?<![a-z0-9])h([12])(?![0-9])", re.I), "h"),
    (re.compile(r"(first|second)[-_ ]half", re.I), "half"),
    (re.compile(r"(?<![a-z0-9])(fy|full[-_ ]?year|annual)(?![a-z0-9])", re.I), "fy"),
]

_YEAR_RE = re.compile(r"(?<!\d)(20\d{2})(?!\d)")

# Explicit form tokens first, then the press-release word, then the deck
# vocabulary the design names for 99-2.
_TYPE_PATTERNS: list[tuple[re.Pattern[str], str]] = [
    (re.compile(r"(?<![a-z0-9])10[-_ ]?k(?![a-z0-9])", re.I), "10-K"),
    (re.compile(r"(?<![a-z0-9])10[-_ ]?q(?![a-z0-9])", re.I), "10-Q"),
    (re.compile(r"(?<![a-z0-9])99[-_. ]?1(?![0-9])", re.I), "99-1"),
    (re.compile(r"(?<![a-z0-9])99[-_. ]?2(?![0-9])", re.I), "99-2"),
    (re.compile(r"press[-_ ]?release", re.I), "99-1"),
    (re.compile(r"supplemental|presentation|deck|slides|earnings", re.I), "99-2"),
]


def infer(filename: str) -> Dict[str, Any]:
    """Filing metadata guessed from a filename: {filing_type, year, period},
    each None when nothing in the name says. A 10-K with no explicit period
    defaults to FY — an annual report is annual."""
    stem = filename.rsplit("/", 1)[-1]
    stem = re.sub(r"\.[^.]+$", "", stem)

    filing_type = next((label for rx, label in _TYPE_PATTERNS if rx.search(stem)), None)

    period: Optional[str] = None
    year: Optional[int] = None
    for rx, kind in _PERIOD_PATTERNS:
        m = rx.search(stem)
        if not m:
            continue
        if kind == "q-year":
            period, year = f"Q{m.group(1)}", int(m.group(2))
        elif kind == "year-q":
            period, year = f"Q{m.group(2)}", int(m.group(1))
        elif kind == "q-yy":
            period, year = f"Q{m.group(1)}", 2000 + int(m.group(2))
        elif kind == "q":
            period = f"Q{m.group(1)}"
        elif kind == "h":
            period = f"H{m.group(1)}"
        elif kind == "half":
            period = "H1" if m.group(1).lower() == "first" else "H2"
        else:
            period = "FY"
        break

    if year is None:
        m = _YEAR_RE.search(stem)
        if m:
            year = int(m.group(1))
    if period is None and filing_type == "10-K":
        period = "FY"
    return {"filing_type": filing_type, "year": year, "period": period}


def filing_label(
    filing_type: Optional[str],
    year: Optional[int],
    period: Optional[str],
    version: Optional[int],
) -> str:
    """The display label: `99-2 · Q1 2026 · v2` with missing parts omitted.
    Empty when the metadata is entirely absent — the title then stands alone."""
    parts: list[str] = []
    if filing_type:
        parts.append(filing_type)
    py = " ".join(str(p) for p in (period, year) if p)
    if py:
        parts.append(py)
    if version:
        parts.append(f"v{version}")
    return " · ".join(parts)


def export_stem(*candidates: Optional[str]) -> str:
    """A filename-safe stem from the first non-empty candidate — label, then
    title, then original filename, per the label precedence."""
    for c in candidates:
        if c and c.strip():
            stem = re.sub(r"[^a-z0-9]+", "-", c.lower()).strip("-")[:60]
            if stem:
                return stem
    return "document"


def _norm(s: Optional[str]) -> str:
    return (s or "").strip().lower()


def tuple_key(
    folder: Optional[str],
    filing_type: Optional[str],
    year: Optional[int],
    period: Optional[str],
    version: Optional[int],
) -> Optional[TupleKey]:
    """The identity tuple, normalized for comparison — or None when it does
    not exist because filing_type, year or period is missing. Documents
    without a tuple never collide and never replace."""
    if not (filing_type and year and period):
        return None
    return (_norm(folder), _norm(filing_type), int(year), _norm(period), version)


def overlap_key(
    folder: Optional[str],
    filing_type: Optional[str],
    year: Optional[int],
    period: Optional[str],
) -> Optional[Tuple[str, str, int, str]]:
    """The tuple without its version: what v1/v2 siblings share, and what the
    possible-duplicate flag groups by."""
    key = tuple_key(folder, filing_type, year, period, version=None)
    return key[:4] if key else None


def annotate_overlaps(docs: Iterable[Dict[str, Any]]) -> None:
    """Set `overlap` on each document dict: True when another document shares
    its versionless tuple. Documents with incomplete metadata never flag."""
    rows = list(docs)
    counts: Dict[Tuple[str, str, int, str], int] = {}
    for d in rows:
        key = overlap_key(d.get("folder"), d.get("filing_type"), d.get("year"), d.get("period"))
        if key is not None:
            counts[key] = counts.get(key, 0) + 1
    for d in rows:
        key = overlap_key(d.get("folder"), d.get("filing_type"), d.get("year"), d.get("period"))
        d["overlap"] = bool(key is not None and counts[key] > 1)
