"""Archivo 1: serie diaria del euro del Data Download Program de la H.10.

Convierte el CSV congelado en barras (fecha, P). Los días «ND» y los valores no numéricos no son
barras: se listan, no se rellenan (protocolo §3, «Integridad»). Una fecha duplicada detiene el
código con una excepción. El archivo nunca se edita: el recorte por fecha se hace en memoria.
"""

from __future__ import annotations

import csv
import io
import re
from dataclasses import dataclass
from datetime import date
from pathlib import Path

EURO_ID = "RXI$US_N.B.EU"
HEADER_ROWS = 6  # anexo: el archivo 1 trae seis filas de cabecera (la 6, «Time Period»)
FIRST_DATE = date(1999, 1, 4)  # protocolo §3
LAST_DATE = date(2026, 9, 30)  # protocolo §3
ND = "ND"

ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
NUMERIC = re.compile(r"^[0-9]+(\.[0-9]+)?$")


class DuplicateDateError(ValueError):
    """El archivo trae la misma fecha en más de una fila (protocolo §3: el código se detiene)."""


class UnexpectedFormatError(ValueError):
    """Una fila tiene un formato que el protocolo no cubre."""


@dataclass(frozen=True)
class Bars:
    """Barras numeradas en orden: la barra t es dates[t - 1] con precio prices[t - 1]."""

    dates: tuple[date, ...]
    prices: tuple[float, ...]
    skipped: tuple[tuple[date, str], ...]  # (fecha, valor crudo) de ND y no numéricos


def parse_h10(text: str, first_date: date = FIRST_DATE, last_date: date = LAST_DATE) -> Bars:
    """Parsea el CSV de la H.10 y devuelve las barras con fecha entre first_date y last_date."""
    rows = list(csv.reader(io.StringIO(text, newline="")))
    if len(rows) <= HEADER_ROWS:
        raise UnexpectedFormatError(f"el archivo tiene {len(rows)} filas y faltan las de datos")
    header = rows[HEADER_ROWS - 1]
    found = [i for i, value in enumerate(header) if value == EURO_ID]
    if len(found) != 1:
        raise UnexpectedFormatError(f"{EURO_ID!r} aparece {len(found)} veces en la fila 6")
    col, width = found[0], len(header)

    rows_by_date: dict[date, int] = {}
    previous: date | None = None
    dates: list[date] = []
    prices: list[float] = []
    skipped: list[tuple[date, str]] = []
    for number, row in enumerate(rows[HEADER_ROWS:], HEADER_ROWS + 1):
        if len(row) != width:
            raise UnexpectedFormatError(f"fila {number}: {len(row)} campos en lugar de {width}")
        if not ISO_DATE.match(row[0]):
            raise UnexpectedFormatError(f"fila {number}: la fecha {row[0]!r} no es AAAA-MM-DD")
        try:
            current = date.fromisoformat(row[0])
        except ValueError as error:
            raise UnexpectedFormatError(f"fila {number}: fecha inexistente {row[0]!r}") from error
        if current in rows_by_date:
            raise DuplicateDateError(
                f"fecha duplicada {current}: filas {rows_by_date[current]} y {number}"
            )
        if previous is not None and current < previous:
            raise UnexpectedFormatError(f"fila {number}: la fecha {current} va fuera de orden")
        rows_by_date[current] = number
        previous = current
        if current < first_date or current > last_date:
            continue
        raw = row[col]
        if NUMERIC.match(raw):
            dates.append(current)
            prices.append(float(raw))
        else:
            skipped.append((current, raw))
    return Bars(tuple(dates), tuple(prices), tuple(skipped))


def load_bars(path: Path, first_date: date = FIRST_DATE, last_date: date = LAST_DATE) -> Bars:
    """Lee el archivo 1 tal cual (UTF-8 estricto) y lo parsea."""
    return parse_h10(path.read_bytes().decode("utf-8"), first_date, last_date)
