"""
Lectura de CSV con detección de delimitador (compartida por layouts_csv).
Si se pasa fieldnames, no se usa la primera fila como cabecera y se toma como dato (CSV sin cabeceras).
Si se pasa headerless_first_cell_values, se detecta si la primera fila es cabecera o dato por el valor de la primera celda.
"""
import csv
import io
from typing import Iterator, Tuple, Dict, Any, Optional, List, Set, Sequence


def _normalize_empty_headers(headers: List[str]) -> List[str]:
    """Sustituye cabeceras vacías por _COL_0_, _COL_1_, ... para que DictReader no colapse columnas."""
    result: List[str] = []
    empty_idx = 0
    for h in headers:
        if (h or "").strip() == "":
            result.append(f"_COL_{empty_idx}_")
            empty_idx += 1
        else:
            result.append(h)
    return result


_ENCODING_FALLBACKS: Sequence[str] = ("utf-8-sig", "utf-8", "cp1252", "latin-1")


def _detect_text_encoding(
    file_path: str,
    encodings: Sequence[str] = _ENCODING_FALLBACKS,
    sample_bytes: int = 8192,
) -> str:
    """
    Detecta encoding por prueba de decodificación en un sample en binario.
    Nota: latin-1 decodifica cualquier byte; por eso debe ir al final.
    """
    with open(file_path, "rb") as f:
        raw = f.read(sample_bytes)
    last_err: Optional[Exception] = None
    for enc in encodings:
        try:
            raw.decode(enc)
            return enc
        except Exception as e:
            last_err = e
    if last_err:
        raise last_err
    return "utf-8-sig"


def iter_csv_rows(
    file_path: str,
    fieldnames: Optional[List[str]] = None,
    headerless_first_cell_values: Optional[Set[str]] = None,
    headerless_second_cell_key_pattern: Optional[str] = None,
) -> Iterator[Tuple[int, Dict[str, Any]]]:
    """
    Abre el CSV, detecta dialecto y devuelve (line_num, row_dict) por cada fila.
    line_num empieza en 1 (primera fila de datos).
    Si fieldnames y headerless_first_cell_values se pasan: si la primera celda de la primera fila
    (quitando BOM, strip, upper) está en headerless_first_cell_values, se trata como dato y se usan fieldnames.
    headerless_second_cell_key_pattern se ignora si no se usa (reservado para otros layouts).
    """
    encoding = _detect_text_encoding(file_path)
    with open(file_path, "r", encoding=encoding) as f:
        sample = f.read(2048)
        f.seek(0)
        try:
            dialect = csv.Sniffer().sniff(sample, delimiters=",;\t")
        except Exception:
            dialect = "excel"
        if fieldnames and headerless_first_cell_values is not None:
            first_line = f.readline()
            if not first_line:
                return
            row_reader = csv.reader(io.StringIO(first_line), dialect=dialect)
            first_cells = next(row_reader, None)
            if not first_cells:
                return
            first_cell_clean = (first_cells[0] or "").lstrip("\ufeff").strip().upper()
            use_headerless = first_cell_clean in headerless_first_cell_values
            if use_headerless:
                pad = len(fieldnames) - len(first_cells)
                cells = first_cells[: len(fieldnames)] + ([""] * pad if pad > 0 else [])
                yield 1, dict(zip(fieldnames, cells))
                reader = csv.DictReader(f, fieldnames=fieldnames, dialect=dialect, restval="")
                for i, row in enumerate(reader, start=2):
                    yield i, dict(row)
                return
            f.seek(0)
            first_line = f.readline()
            if not first_line:
                return
            row_reader = csv.reader(io.StringIO(first_line), dialect=dialect)
            raw_headers = next(row_reader, None)
            if not raw_headers:
                return
            normalized = _normalize_empty_headers(raw_headers)
            reader = csv.DictReader(f, fieldnames=normalized, dialect=dialect, restval="")
            for i, row in enumerate(reader, start=1):
                yield i, row
        elif fieldnames:
            reader = csv.DictReader(f, fieldnames=fieldnames, dialect=dialect)
            for i, row in enumerate(reader, start=1):
                yield i, dict(row)
        else:
            first_line = f.readline()
            if not first_line:
                return
            row_reader = csv.reader(io.StringIO(first_line), dialect=dialect)
            raw_headers = next(row_reader, None)
            if not raw_headers:
                return
            normalized = _normalize_empty_headers(raw_headers)
            reader = csv.DictReader(f, fieldnames=normalized, dialect=dialect, restval="")
            for i, row in enumerate(reader, start=1):
                yield i, row


def count_csv_rows(file_path: str, has_header: bool = True) -> int:
    """Cuenta filas del CSV. Si has_header=True (por defecto), no cuenta la cabecera."""
    encoding = _detect_text_encoding(file_path)
    with open(file_path, "r", encoding=encoding) as f:
        total_lines = sum(1 for _ in f)
    return total_lines if not has_header else max(0, total_lines - 1)
