"""Validare de domeniu pentru prezentari (T3).

Replica regulile RAR (docs/api-rar-contract.md sect. "Reguli de validare") ÎNAINTE
de enqueue, ca sa nu primim 4xx de la RAR. Spre deosebire de validarea de SHAPE
(Pydantic, da 422 la JSON malformat), aceasta e validare de CONTINUT: esecurile NU
resping cererea, ci marcheaza submission-ul `needs_data` (plan.md sect. 3 — masina de
stari + failure registry). Asa apar in dashboard cu motiv, corectabile.

Functiile sunt pure (dict -> listă erori), unit-testabile fara DB/HTTP.
Erorile au forma {field, message} — aceeasi ca raspunsul de eroare RAR.
"""

from __future__ import annotations

import base64
import re
from datetime import date
from zoneinfo import ZoneInfo

from app.errors import eroare as _eroare

# VIN: 17 caractere, majuscule, fara O/I/Q (plan §2 + contract).
VIN_RE = re.compile(r"^[A-HJ-NPR-Z0-9]{17}$")
# Numar inmatriculare: max 10, litere + cifre majuscule.
NRINM_RE = re.compile(r"^[A-Z0-9]{1,10}$")
# Coduri care fac odometruInitial obligatoriu.
ODOMETER_CODES = {"R-ODO", "I-ODO"}
# Interval dataPrestatie.
MIN_DATA_PRESTATIE = date(2024, 12, 1)
TZ_BUCURESTI = ZoneInfo("Europe/Bucharest")


def _norm(value: object) -> str:
    return str(value or "").strip().upper()


def _codes(prestatii: list | None) -> list[str]:
    out: list[str] = []
    for p in prestatii or []:
        cod = p.get("cod_prestatie") if isinstance(p, dict) else getattr(p, "cod_prestatie", None)
        if cod:
            out.append(str(cod).strip().upper())
    return out


def _parse_int(value: object) -> int | None:
    s = str(value or "").strip()
    if s.isdigit():
        return int(s)
    return None


def today_bucuresti() -> date:
    from datetime import datetime

    return datetime.now(TZ_BUCURESTI).date()


def validate_prezentare(content: dict) -> list[dict]:
    """Intoarce lista erorilor de continut [{field, message}]. Goala = valid.

    `content` = PrezentareIn.model_dump() (campuri snake_case interne).
    """
    errors: list[dict] = []

    # --- VIN ---
    vin = _norm(content.get("vin"))
    if not VIN_RE.match(vin):
        errors.append(_eroare(
            "VIN_FORMAT",
            field="vin",
            cauza="VIN trebuie sa aiba exact 17 caractere majuscule, fara spatii/caractere speciale si fara O, I, Q.",
        ))

    # --- nrInmatriculare ---
    nrinm = _norm(content.get("nr_inmatriculare"))
    if not NRINM_RE.match(nrinm):
        errors.append(_eroare(
            "NR_INMATRICULARE_FORMAT",
            field="nr_inmatriculare",
            cauza="Numarul de inmatriculare trebuie sa aiba max 10 caractere, doar litere si cifre majuscule.",
        ))

    # --- dataPrestatie ∈ [2024-12-01, azi] TZ Bucuresti ---
    raw_data = str(content.get("data_prestatie") or "").strip()
    try:
        d = date.fromisoformat(raw_data)
    except ValueError:
        errors.append(_eroare(
            "DATA_FORMAT",
            field="data_prestatie",
            cauza="Format data invalid; foloseste YYYY-MM-DD.",
        ))
        d = None
    if d is not None:
        if d < MIN_DATA_PRESTATIE:
            errors.append(_eroare(
                "DATA_PREA_VECHE",
                field="data_prestatie",
                cauza="Data prestatiei nu poate fi anterioara datei de 01.12.2024.",
            ))
        elif d > today_bucuresti():
            errors.append(_eroare(
                "DATA_VIITOR",
                field="data_prestatie",
                cauza="Data prestatiei nu poate fi in viitor.",
            ))

    # --- odometruFinal (string numeric) ---
    odo_final = _parse_int(content.get("odometru_final"))
    if odo_final is None:
        errors.append(_eroare(
            "ODOMETRU_FINAL_FORMAT",
            field="odometru_final",
            cauza="odometruFinal trebuie sa fie un numar intreg (ca string).",
        ))

    # --- odometruInitial: obligatoriu daca prestatii ∋ R-ODO/I-ODO; <= odometruFinal ---
    codes = _codes(content.get("prestatii"))
    needs_initial = bool(set(codes) & ODOMETER_CODES)
    raw_initial = content.get("odometru_initial")
    has_initial = str(raw_initial or "").strip() != ""
    if needs_initial and not has_initial:
        errors.append(_eroare(
            "ODOMETRU_INITIAL_LIPSA",
            field="odometru_initial",
            cauza="odometruInitial este obligatoriu cand prestatiile contin R-ODO sau I-ODO.",
        ))
    if has_initial:
        odo_initial = _parse_int(raw_initial)
        if odo_initial is None:
            errors.append(_eroare(
                "ODOMETRU_INITIAL_FORMAT",
                field="odometru_initial",
                cauza="odometruInitial trebuie sa fie un numar intreg.",
            ))
        elif odo_final is not None and odo_initial > odo_final:
            errors.append(_eroare(
                "ODOMETRU_INITIAL_ORDINE",
                field="odometru_initial",
                cauza="odometruInitial trebuie sa fie <= odometruFinal.",
            ))

    # --- prestatii nevide ---
    if not codes:
        errors.append(_eroare(
            "PRESTATII_GOALE",
            field="prestatii",
            cauza="Lista de prestatii nu poate fi goala.",
        ))

    # --- b64Image: optional, dar daca e prezent trebuie base64 valid ---
    b64 = content.get("b64_image")
    if b64:
        if not _is_valid_base64(str(b64)):
            errors.append(_eroare(
                "B64_INVALID",
                field="b64_image",
                cauza="b64Image nu este base64 valid.",
            ))

    return errors


def _is_valid_base64(value: str) -> bool:
    s = value.strip()
    if not s:
        return False
    try:
        base64.b64decode(s, validate=True)
        return True
    except (ValueError, base64.binascii.Error):  # type: ignore[attr-defined]
        return False
