"""Conteos de solo lectura para el anexo de datos del Estudio 01.

Lee los archivos congelados de data/ y cuenta filas, fechas y valores de la columna del euro. No
calcula medias, señales ni métricas de ningún tipo, y no usa la red. Se detiene con código 2 si
encuentra una condición que el protocolo trata como fallo de integridad: fechas duplicadas, filas
fuera de orden o filas con un formato que el protocolo no cubre. Las filas no numéricas distintas
de ND no detienen el script: se listan.

Uso, desde la raíz del repositorio:

    .venv/bin/python studies/estudio-01/annex_counts.py          # texto
    .venv/bin/python studies/estudio-01/annex_counts.py --json   # JSON
"""

from __future__ import annotations

import argparse
import csv
import json
import re
import sys
from collections import Counter
from datetime import date
from decimal import Decimal
from html.parser import HTMLParser
from itertools import pairwise
from pathlib import Path
from typing import Any

DATA_DIR = Path(__file__).resolve().parent / "data"
FILE1 = DATA_DIR / "01-h10-daily-rates.csv"
FILE2 = DATA_DIR / "02-h10-hist-euro.htm"

EURO_ID = "RXI$US_N.B.EU"
HEADER_ROWS = 6
CUTOFF = date(2026, 9, 30)
CONTROL_START = date(2000, 1, 3)
FINAL_SEGMENT_START = date(2024, 10, 1)
WARMUP_BARS = 1_000
UNIT_PHRASE = "Rates in U.S. dollars per EU euro"
ABSENT = "(sin fila)"
ND = "ND"

EXIT_INTEGRITY = 2

ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
HIST_DATE = re.compile(r"^(\d{1,2})-([A-Z]{3})-(\d{2})$")
NUMERIC = re.compile(r"^[0-9]+(\.[0-9]+)?$")
MONTHS = {
    name: number
    for number, name in enumerate(
        ("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"), 1
    )
}
HIST_YEAR_BASE = 2000  # la página histórica cubre desde 2000-01-03: AA = 00 es 2000


def parse_numeric(raw: str | None) -> Decimal | None:
    if raw is not None and NUMERIC.match(raw):
        return Decimal(raw)
    return None


def _iso(value: date | None) -> str | None:
    return value.isoformat() if value is not None else None


def read_file1() -> dict[str, Any]:
    """Archivo 1: cabeceras, columna del euro localizada por identificador y filas de datos."""
    with open(FILE1, newline="", encoding="utf-8") as handle:
        rows = list(csv.reader(handle))
    ids = [i for i, value in enumerate(rows[HEADER_ROWS - 1]) if value == EURO_ID]
    if len(ids) != 1:
        raise SystemExit(f"{EURO_ID!r} aparece {len(ids)} veces en la fila {HEADER_ROWS}")
    col = ids[0]
    width = len(rows[HEADER_ROWS - 1])
    data: list[tuple[date, str]] = []
    unexpected: list[list[Any]] = []
    for number, row in enumerate(rows[HEADER_ROWS:], HEADER_ROWS + 1):
        if len(row) != width:
            unexpected.append([number, f"{len(row)} campos en lugar de {width}"])
        elif not ISO_DATE.match(row[0]):
            unexpected.append([number, f"fecha {row[0]!r} sin formato AAAA-MM-DD"])
        else:
            try:
                data.append((date.fromisoformat(row[0]), row[col]))
            except ValueError:
                unexpected.append([number, f"fecha {row[0]!r} inexistente"])
    return {
        "rows": rows,
        "col": col,
        "width": width,
        "data": data,
        "unexpected": unexpected,
    }


class _HistTable(HTMLParser):
    """Extrae (fecha, valor) de cada fila con una celda th y una celda td."""

    def __init__(self) -> None:
        super().__init__()
        self.rows: list[tuple[str, str]] = []
        self.skipped: list[list[str]] = []
        self._cells: list[tuple[str, str]] | None = None
        self._tag: str | None = None
        self._text: list[str] = []

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        if tag == "tr":
            self._cells = []
        elif tag in ("th", "td") and self._cells is not None:
            self._tag, self._text = tag, []

    def handle_data(self, data: str) -> None:
        if self._tag is not None:
            self._text.append(data)

    def handle_endtag(self, tag: str) -> None:
        if tag == self._tag and self._cells is not None:
            self._cells.append((tag, " ".join("".join(self._text).split())))
            self._tag = None
        elif tag == "tr" and self._cells is not None:
            if len(self._cells) == 2 and [c[0] for c in self._cells] == ["th", "td"]:
                self.rows.append((self._cells[0][1], self._cells[1][1]))
            else:
                self.skipped.append([c[1] for c in self._cells])
            self._cells = None


def read_file2() -> dict[str, Any]:
    """Archivo 2: filas (fecha D-MMM-AA, valor) de la tabla histórica del euro."""
    text = FILE2.read_text(encoding="utf-8")
    parser = _HistTable()
    parser.feed(text)
    data: list[tuple[date, str]] = []
    unexpected: list[list[Any]] = [["fila sin th/td", cells] for cells in parser.skipped]
    for raw_date, raw_value in parser.rows:
        match = HIST_DATE.match(raw_date)
        try:
            if match is None or match.group(2) not in MONTHS:
                raise ValueError(raw_date)
            day, month, year = int(match.group(1)), MONTHS[match.group(2)], int(match.group(3))
            data.append((date(HIST_YEAR_BASE + year, month, day), raw_value))
        except ValueError:
            unexpected.append(["fecha sin formato D-MMM-AA", raw_date])
    return {"has_unit_phrase": UNIT_PHRASE in text, "data": data, "unexpected": unexpected}


def _series_summary(data: list[tuple[date, str]]) -> dict[str, Any]:
    dates = [d for d, _ in data]
    duplicated = sorted(d for d, times in Counter(dates).items() if times > 1)
    out_of_order = sum(1 for before, after in pairwise(dates) if after < before)
    values = [v for _, v in data]
    return {
        "date_rows": len(data),
        "first_date": _iso(dates[0]) if dates else None,
        "last_date": _iso(dates[-1]) if dates else None,
        "nd_rows": sum(1 for v in values if v == ND),
        "numeric_rows": sum(1 for v in values if parse_numeric(v) is not None),
        "non_numeric_rows": [
            [d.isoformat(), v] for d, v in data if v != ND and parse_numeric(v) is None
        ],
        "duplicate_dates": [d.isoformat() for d in duplicated],
        "out_of_order_rows": out_of_order,
    }


def count_file1(parsed: dict[str, Any]) -> dict[str, Any]:
    rows, col, data = parsed["rows"], parsed["col"], parsed["data"]
    summary = _series_summary(data)
    bars = [d for d, v in data if parse_numeric(v) is not None and d <= CUTOFF]

    def bar(number: int) -> str | None:
        return _iso(bars[number - 1]) if len(bars) >= number else None

    summary.update(
        {
            "columns": parsed["width"],
            "euro_column_index_base0": col,
            "euro_row1": rows[0][col],
            "euro_row5": rows[4][col],
            "euro_row6": rows[5][col],
            "rows_after_cutoff": sum(1 for d, _ in data if d > CUTOFF),
            "nd_rows_to_cutoff": sum(1 for d, v in data if v == ND and d <= CUTOFF),
            "bars": len(bars),
            "bar_1": bar(1),
            "bar_1000": bar(WARMUP_BARS),
            "bar_1001": bar(WARMUP_BARS + 1),
            "bar_L": _iso(bars[-1]) if bars else None,
            "bar_L_number": len(bars),
            "returns_T": len(bars) - WARMUP_BARS,
            "final_segment_bars": sum(1 for d in bars if d >= FINAL_SEGMENT_START),
            "unexpected_rows": parsed["unexpected"],
        }
    )
    return summary


def count_file2(parsed: dict[str, Any]) -> dict[str, Any]:
    summary = _series_summary(parsed["data"])
    summary["has_unit_phrase"] = parsed["has_unit_phrase"]
    summary["unexpected_rows"] = parsed["unexpected"]
    return summary


def equality_control(
    data1: list[tuple[date, str]], data2: list[tuple[date, str]], last_bar: date
) -> dict[str, Any]:
    """Compara los valores numéricos de los dos archivos entre 2000-01-03 y la fecha de L.

    Se compara una fecha si al menos uno de los dos archivos trae en ella un valor numérico; ND y
    fecha ausente cuentan como sin dato. La igualdad es numérica (1.0155 y 1.01550 son iguales).
    """
    by1, by2 = dict(data1), dict(data2)
    dates = sorted(d for d in {*by1, *by2} if CONTROL_START <= d <= last_bar)
    compared = 0
    differences: list[list[str]] = []
    for d in dates:
        raw1, raw2 = by1.get(d), by2.get(d)
        v1, v2 = parse_numeric(raw1), parse_numeric(raw2)
        if v1 is None and v2 is None:
            continue
        compared += 1
        if v1 != v2:
            differences.append(
                [
                    d.isoformat(),
                    ABSENT if raw1 is None else raw1,
                    ABSENT if raw2 is None else raw2,
                ]
            )
    return {
        "range_start": CONTROL_START.isoformat(),
        "range_end": last_bar.isoformat(),
        "dates_compared": compared,
        "differences_count": len(differences),
        "differences": differences,
    }


def build() -> dict[str, Any]:
    parsed1, parsed2 = read_file1(), read_file2()
    file1, file2 = count_file1(parsed1), count_file2(parsed2)
    control = equality_control(parsed1["data"], parsed2["data"], date.fromisoformat(file1["bar_L"]))
    return {"file1": file1, "file2": file2, "control": control}


def integrity_problems(report: dict[str, Any]) -> list[str]:
    problems = []
    for key, label in (("file1", "archivo 1"), ("file2", "archivo 2")):
        section = report[key]
        if section["duplicate_dates"]:
            problems.append(f"{label}: fechas duplicadas {section['duplicate_dates']}")
        if section["out_of_order_rows"]:
            problems.append(f"{label}: {section['out_of_order_rows']} filas fuera de orden")
        if section["unexpected_rows"]:
            problems.append(f"{label}: filas con formato no cubierto {section['unexpected_rows']}")
    return problems


def render_text(report: dict[str, Any]) -> str:
    lines: list[str] = []
    for key in ("file1", "file2", "control"):
        lines.append(f"[{key}]")
        for name, value in report[key].items():
            if isinstance(value, list) and value and isinstance(value[0], list):
                lines.append(f"  {name}: {len(value)}")
                lines.extend(f"    {item}" for item in value)
            else:
                lines.append(f"  {name}: {value}")
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--json", action="store_true", help="salida en JSON")
    args = parser.parse_args()
    report = build()
    problems = integrity_problems(report)
    report["integrity_problems"] = problems
    if args.json:
        print(json.dumps(report, indent=2, ensure_ascii=False))
    else:
        print(render_text(report))
        print(f"[integridad] problemas: {len(problems)}")
        for problem in problems:
            print(f"  {problem}")
    return EXIT_INTEGRITY if problems else 0


if __name__ == "__main__":
    sys.exit(main())
