"""API v1 — suprafata gateway.

Endpointuri:
  - POST /v1/prezentari: enqueue cu idempotenta (dedup pe idempotency_key UNIQUE).
  - GET  /v1/prezentari, /v1/prezentari/{id}: monitorizare coada.
  - GET  /v1/nomenclator: cache local.
  - GET  /v1/mapari: listare mapari cont.
"""

from __future__ import annotations

import csv
import io
import json

from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field

from ...accounts import held_for_account
from ...auth import require_api_access, resolve_account_id
from ...crypto import encrypt_creds
from ...db import get_connection
from ...errors import eroare as err_eroare
from ...idempotency import build_key, canonicalize_row
from ...rar_env import MediuIndisponibil, rezolva_rar_env
from ...mapping import (
    _emite_text_rule_hits,
    account_or_default,
    account_scope_clause,
    classify_prezentare,
    load_mapping_meta,
    load_nomenclator_codes,
    load_text_rules,
    pending_unmapped,
    reresolve_account,
    save_mapping,
)
from ...models import (
    PrezentareRequest,
    PrezentariResponse,
    SubmissionResult,
    ValidarePrezentariRequest,
    ValidareResponse,
    ValidareResult,
)
from ...observ import log_event
from ...payload_view import prezentare_din_payload
from ...submissions_admin import (
    SubmissionNotFound,
    SubmissionStateConflict,
    delete_submission,
    requeue_submission,
)

router = APIRouter(prefix="/v1", tags=["v1"])


def _effective_on_unmapped_error(conn, acct: int, req_value: bool | None) -> bool:
    """Modul efectiv la cod necunoscut/nemapat (True => respinge cererea, False => needs_mapping).

    Precedenta: override per-cerere > default cont (on_unmapped_error_default) > False.
    """
    if req_value is not None:
        return req_value
    row = conn.execute("SELECT on_unmapped_error_default FROM accounts WHERE id=?", (acct,)).fetchone()
    return bool(row["on_unmapped_error_default"]) if row else False


def _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules=None) -> dict:
    """classify_prezentare + aplicarea modului on_unmapped_error.

    Cand exista coduri nemapate si error_mode=True, marcheaza outcome-ul ca respingere
    (blocked_error=True): rutele NU mai fac enqueue, ci intorc o eroare per-element.
    """
    cl = classify_prezentare(content, mapping, mapping_meta, valid_codes, text_rules)
    cl["blocked_error"] = bool(cl["unmapped"]) and error_mode
    return cl


def _erori_nemapate(unmapped: list[dict]) -> list[dict]:
    """Coduri nemapate imbogatite cu 3 niveluri (COD_NEMAPAT)."""
    return [
        {**u, **err_eroare("COD_NEMAPAT", cauza=f"cod {u.get('cod_op_service')} necunoscut/fara mapare RAR")}
        for u in unmapped
    ]


# PRD 5.19 US-010: mesaj uman pentru un rand tinut manual (queued AND held=1).
# Reutilizeaza vocabularul existent AUTO_SEND_OPRIT (errors.py) — nu inventam al treilea
# vocabular "auto_send". Onestitate: raspunsul NU trebuie sa arate un "queued" curat fals.
MOTIV_HELD = (
    "In asteptare — tinut pentru verificare; NU trimis la RAR (Auto OFF). "
    "Elibereaza manual (trimite-acum) sau activeaza Trimiterea automata."
)


def _motiv_held(status: str, held: bool) -> str | None:
    """Motiv uman pentru un rand tinut manual; None cand nu e cazul (status!=queued sau held=0)."""
    return MOTIV_HELD if (held and status == "queued") else None


def _motiv_clasificare(cl: dict) -> str | None:
    """Rezumat uman pe o linie pentru un rezultat de clasificare.

    None cand status='queued'. Acopera ramurile de blocaj: erori de continut
    (needs_data) si coduri fara mapare RAR (needs_mapping).
    Dupa US-001: needs_mapping apare EXCLUSIV cand unmapped e non-gol
    (ramura auto_send_oprit era inaccesibila si a fost eliminata).
    """
    if cl["status"] == "queued":
        return None
    if cl["errors"]:
        return "; ".join(
            (e.get("problema") or e.get("message") or "") for e in cl["errors"]
        ).strip("; ") or "Date incomplete (respinse de RAR)."
    if cl["unmapped"]:
        coduri = ", ".join((u.get("cod_op_service") or "") for u in cl["unmapped"])
        return f"Coduri fara mapare RAR: {coduri}"
    return None


def _rezultat_enqueue(submission_id: int | None, cl: dict, held: int = 0, **extra) -> SubmissionResult:
    """SubmissionResult onest dintr-un rezultat de clasificare.

    Populeaza erori (validare continut), nemapate (coduri fara mapare) si motiv (uman)
    pentru orice status != 'queued'. Aditiv: pe 'queued' toate raman goale/None, EXCEPTAND
    randurile tinute manual (PRD 5.19 US-010: `held=1` -> held=true + motiv non-null).
    """
    tinut = bool(held)
    motiv = _motiv_clasificare(cl) or _motiv_held(cl["status"], tinut)
    return SubmissionResult(
        submission_id=submission_id,
        status=cl["status"],
        erori=list(cl["errors"]),
        nemapate=_erori_nemapate(cl["unmapped"]),
        motiv=motiv,
        held=tinut,
        **extra,
    )


def _rezultat_respins(submission_id: int | None, cl: dict, rar_env: str = "test") -> SubmissionResult:
    """Rezultat pentru on_unmapped_error=True: status='error', fara enqueue/reactivare.

    `erori` pastreaza COD_NEMAPAT (compat clienti vechi); `nemapate` + `motiv` adaugate.
    """
    nem = _erori_nemapate(cl["unmapped"])
    return SubmissionResult(
        submission_id=submission_id, status="error",
        erori=nem, nemapate=nem, motiv=_motiv_clasificare(cl),
        rar_env=rar_env,
    )


@router.post("/prezentari", response_model=PrezentariResponse)
def create_prezentari(
    req: PrezentareRequest,
    account_id: int = Depends(require_api_access),
) -> PrezentariResponse:
    """Enqueue una/mai multe prezentari. Idempotent: continut identic -> acelasi submission.

    Validarea de continut (app.validation) ruleaza inainte de enqueue: esecurile NU
    resping cererea, ci enqueue-aza cu status `needs_data` + motiv. JSON malformat ->
    422 din Pydantic (validare de shape).
    account_id vine din cheia API (resolve_account_id): cont real cu cheie,
    implicit id=1 in dev fara cheie, 401 fara cheie valida in prod.
    Cand rar_credentials lipseste, submission-ul intra fara creds efemere: worker-ul
    cade pe creds-urile durabile ale contului (per-env: `accounts.rar_creds_{env}_enc`).
    """
    acct = account_or_default(account_id)
    # Creds RAR efemere: criptate si lipite de fiecare submission nou pana la
    # primul login reusit pentru cont (worker le sterge atunci). Zero-storage at
    # rest — niciodata in clar in DB/loguri. Optional: cand lipsesc,
    # creds_enc=NULL si worker-ul foloseste creds-urile durabile ale contului.
    creds_enc = encrypt_creds(req.rar_credentials.model_dump()) if req.rar_credentials else None
    conn = get_connection()
    results: list[SubmissionResult] = []
    try:
        # load_mapping_meta incarca maparea op->cod RAR; dupa US-001, auto_send nu mai tine randuri.
        mapping_meta = load_mapping_meta(conn, acct)
        mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
        # Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat.
        # valid_codes gol (nomenclator nepopulat) -> None (nu validam, ca sa nu blocam tot).
        valid_codes = load_nomenclator_codes(conn) or None
        # Reguli text incarcate o data per cerere (seam partajat cu dry-run).
        text_rules = load_text_rules(conn, acct)
        error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)

        # US-005: rezolva mediul RAR tinta (cerut > default cont > ancora globala).
        # MediuIndisponibil -> 422 inainte de orice enqueue (respinge tot lotul).
        try:
            env = rezolva_rar_env(conn, acct, req.rar_env)
        except MediuIndisponibil as e:
            # US-012: audit blocare mediu indisponibil (tip='rar_env_blocat').
            log_event(
                "rar_env_blocat",
                nivel="WARNING",
                account_id=acct,
                context={"env": e.env},
                conn=conn,
            )
            raise HTTPException(
                status_code=422,
                detail=err_eroare(
                    "RAR_MEDIU_INDISPONIBIL",
                    cauza=(
                        f"mediu cerut: {e.env}; disponibile: "
                        f"{', '.join(e.disponibile) or 'niciunul'}"
                    ),
                ),
            )
        except ValueError:
            # Pydantic Literal prinde valorile invalide inainte sa ajunga aici;
            # ramura e defensiva pentru apeluri directe fara model Pydantic.
            raise HTTPException(
                status_code=422,
                detail=err_eroare("RAR_MEDIU_INDISPONIBIL", cauza="valoare invalida pentru rar_env"),
            )

        # T3 (PRD 5.17): enforce volum plan — INAINTE de build_key/enqueue (invariant idempotenta).
        # Decizie #21: respingere TOTALA a lotului (nu enqueue partial tacut).
        from ...config import get_settings as _get_settings
        from ...plans import PLANS, effective_tier, monthly_usage
        _settings = _get_settings()
        if _settings.enforce_plans:
            _acct_row = conn.execute(
                "SELECT tier, trial_until FROM accounts WHERE id=?", (acct,)
            ).fetchone()
            _now = datetime.now(timezone.utc)
            _et = effective_tier(_acct_row, _now)
            _plan_limit = PLANS[_et].get("monthly_limit")
            if _plan_limit is not None:
                _usage = monthly_usage(conn, acct, _now)
                _nr_cerut = len(req.prezentari)
                if _usage + _nr_cerut > _plan_limit:
                    _remaining = max(0, _plan_limit - _usage)
                    log_event(
                        "plan_limita_lunara_atinsa",
                        account_id=acct,
                        nivel="WARNING",
                        mesaj=f"Lot de {_nr_cerut} respins (usage={_usage}, limita={_plan_limit})",
                        context={
                            "nr_cerut": _nr_cerut, "usage": _usage,
                            "plan_limit": _plan_limit, "tier": _et,
                        },
                        conn=conn,
                    )
                    raise HTTPException(
                        status_code=422,
                        detail=err_eroare(
                            "PLAN_LIMITA_LUNARA",
                            cauza=(
                                f"Ai trimis {_usage}/{_plan_limit} prezentari luna aceasta;"
                                f" mai poti trimite {_remaining}."
                            ),
                        ),
                    )

        # PRD 5.19 US-002: snapshot `held` din comutatorul contului (chokepoint unic
        # held_for_account). 0 daca Auto ON, 1 daca Auto OFF (tine randul). Depinde de
        # cont, nu de rand -> calculat o data. NU intra in payload/idempotenta.
        held_val = held_for_account(conn, acct)

        for prez in req.prezentari:
            content = prez.model_dump()
            # canonicalize_row inaintea build_key (odometru strip ".0", VIN upper).
            # build_key aplica account_or_default(account_id) inainte de hash:
            # None si 1 colapseaza la aceeasi cheie (canal API + canal import).
            canon = canonicalize_row(content)
            key = build_key(account_id, canon, env)
            # Aplica normalizarea si in content (odometru canonicalizat inainte de validare)
            content.update({
                "vin": canon["vin"],
                "nr_inmatriculare": canon["nr_inmatriculare"],
                "odometru_final": canon["odometru_final"],
            })
            existing = conn.execute(
                "SELECT id, status, id_prezentare, held FROM submissions WHERE idempotency_key=?",
                (key,),
            ).fetchone()
            if existing:
                # Un rand `error` (ex. creds RAR gresite) NU mai blocheaza tacit
                # retrimiterea aceluiasi continut. Il RE-ACTIVAM (re-clasificam + actualizam
                # creds + reset), printr-un UPDATE compare-and-swap pe status='error'.
                if existing["status"] == "error":
                    cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
                    if cl["blocked_error"]:
                        # on_unmapped_error=True: nu reactivam; randul ramane 'error'.
                        results.append(_rezultat_respins(existing["id"], cl, rar_env=env))
                        continue
                    # PRD 5.19 US-002 (Eng Finding A — bug de bypass): la reactivare
                    # re-snapshot-am `held` din comutatorul contului. Fara asta randul
                    # pastra held VECHI si se auto-trimitea desi contul e Auto OFF.
                    cur = conn.execute(
                        "UPDATE submissions SET status=?, payload_json=?, rar_error=?, "
                        "rar_creds_enc=COALESCE(?, rar_creds_enc), retry_count=0, "
                        "next_attempt_at=NULL, sending_since=NULL, purge_after=NULL, "
                        "held=?, rar_env=?, updated_at=datetime('now') WHERE id=? AND status='error'",
                        (cl["status"], json.dumps(cl["content"], ensure_ascii=False),
                         cl["rar_error"], creds_enc, held_val, env, existing["id"]),
                    )
                    if cur.rowcount == 1:
                        # Creds noi se propaga si in slotul durabil per-env al contului
                        # — ambele canale converg pe parola corectata (US-013, env-aware).
                        if req.rar_credentials is not None:
                            conn.execute(
                                f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?",
                                (encrypt_creds(req.rar_credentials.model_dump()), acct),
                            )
                        _emite_text_rule_hits(conn, acct, existing["id"], cl["resolved"])
                        # Raspuns onest si la reactivare: daca re-clasificarea cade pe
                        # needs_data/needs_mapping, expune motivul (nu doar status).
                        results.append(_rezultat_enqueue(existing["id"], cl, held=held_val, reactivated=True, rar_env=env))
                        continue
                    # Cursa: alt POST/requeue a schimbat starea intre SELECT si UPDATE
                    # (rowcount==0) -> raspuns dedup pe starea CURENTA.
                    existing = conn.execute(
                        "SELECT id, status, id_prezentare, held FROM submissions WHERE id=?",
                        (existing["id"],),
                    ).fetchone()
                # Echo onest pe dedup (PRD 5.19 US-002/US-010): propaga `held` al randului
                # existent — un rand tinut NU trebuie sa apara ca un "queued" curat fals.
                dedup_held = bool(existing["held"])
                results.append(
                    SubmissionResult(
                        submission_id=existing["id"],
                        status=existing["status"],
                        id_prezentare=existing["id_prezentare"],
                        deduped=True,
                        held=dedup_held,
                        motiv=_motiv_held(existing["status"], dedup_held),
                        rar_env=env,
                    )
                )
                continue

            # Helper pur partajat cu dry-run: reproduce EXACT clasificarea
            # (canonicalize + mapare op->cod + validare; auto_send gate eliminat dupa US-001).
            cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
            if cl["blocked_error"]:
                # on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat).
                results.append(_rezultat_respins(None, cl, rar_env=env))
                continue
            # PRD 5.19 US-002: `held` = snapshot comutator cont (chokepoint held_for_account).
            cur = conn.execute(
                "INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, rar_creds_enc, rar_env, held) "
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
                (key, acct, cl["status"], json.dumps(cl["content"], ensure_ascii=False), cl["rar_error"], creds_enc, env, held_val),
            )
            sub_id = int(cur.lastrowid)
            _emite_text_rule_hits(conn, acct, sub_id, cl["resolved"])
            # Raspuns onest: pe needs_data/needs_mapping expune erori/nemapate/motiv;
            # pe queued tinut manual (held=1) expune held=true + motiv.
            results.append(_rezultat_enqueue(sub_id, cl, held=held_val, rar_env=env))

        # Audit cerere API per cont. Doar metadate (count + distributie status),
        # NICIUN camp de payload PII integral. Reuse conn (fara contentie WAL).
        dist: dict[str, int] = {}
        for r in results:
            if r.reactivated:
                cheie = "reactivated"
            elif r.deduped:
                cheie = "deduped"
            else:
                cheie = r.status
            dist[cheie] = dist.get(cheie, 0) + 1
        log_event(
            "api_prezentari",
            account_id=acct,
            mesaj=f"{len(results)} prezentari procesate",
            context={"count": len(results), "distributie": dist},
            conn=conn,
        )
    finally:
        conn.close()
    return PrezentariResponse(results=results)


@router.post("/prezentari/valideaza", response_model=ValidareResponse)
def valideaza_prezentari(
    req: ValidarePrezentariRequest,
    account_id: int = Depends(resolve_account_id),
) -> ValidareResponse:
    """Dry-run: valideaza payload exact ca POST /prezentari, fara enqueue si fara efecte secundare.

    Intoarce pentru fiecare prezentare: verdictul (status_estimat), erorile de
    continut si codurile nemapate — exact ce ar obtine trimiterea reala pe acelasi
    payload + aceeasi mapare de cont. rar_credentials ignorat complet.
    """
    acct = account_or_default(account_id)
    conn = get_connection()
    results: list[ValidareResult] = []
    try:
        mapping_meta = load_mapping_meta(conn, acct)
        mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
        valid_codes = load_nomenclator_codes(conn) or None
        # Acelasi seam ca trimiterea reala: dry-run trebuie sa vada aceleasi reguli text.
        text_rules = load_text_rules(conn, acct)
        error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)

        # US-005 (DX F5): rezolva env identic ca trimiterea reala si ecou-ieste in raspuns.
        try:
            env = rezolva_rar_env(conn, acct, req.rar_env)
        except MediuIndisponibil as e:
            # US-012: audit blocare mediu indisponibil pe dry-run (tip='rar_env_blocat').
            log_event(
                "rar_env_blocat",
                nivel="WARNING",
                account_id=acct,
                context={"env": e.env},
                conn=conn,
            )
            raise HTTPException(
                status_code=422,
                detail=err_eroare(
                    "RAR_MEDIU_INDISPONIBIL",
                    cauza=(
                        f"mediu cerut: {e.env}; disponibile: "
                        f"{', '.join(e.disponibile) or 'niciunul'}"
                    ),
                ),
            )
        except ValueError:
            raise HTTPException(
                status_code=422,
                detail=err_eroare("RAR_MEDIU_INDISPONIBIL", cauza="valoare invalida pentru rar_env"),
            )

        for i, prez in enumerate(req.prezentari):
            content = prez.model_dump()
            res = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
            if res["blocked_error"]:
                res = {**res, "status": "error"}
            # Imbogatim fiecare element nemapat cu 3 niveluri COD_NEMAPAT
            nemapate = [
                {**u, **err_eroare("COD_NEMAPAT", cauza=f"cod {u.get('cod_op_service')} fara mapare RAR")}
                for u in res["unmapped"]
            ]
            results.append(ValidareResult(
                index=i,
                valid=(res["status"] == "queued"),
                status_estimat=res["status"],
                rar_env=env,
                erori=res["errors"],
                nemapate=nemapate,
                prestatii_rezolvate=res["resolved"],
            ))
    finally:
        conn.close()
    return ValidareResponse(results=results)


@router.get("/prezentari")
def list_prezentari(
    status: str | None = None,
    limit: int = 100,
    account_id: int = Depends(resolve_account_id),
) -> dict:
    conn = get_connection()
    try:
        scope_sql, scope_params = account_scope_clause(account_id)
        # payload_json e plaintext (vezi submissions.payload_json); il citim doar ca
        # sa derivam campurile afisabile prin helper-ul partajat, nu il expunem.
        # rar_env inclus (US-005): badge mediu in lista.
        cols = (
            "id, status, id_prezentare, rar_status_code, retry_count, "
            "created_at, updated_at, payload_json, rar_env, held"
        )
        if status:
            rows = conn.execute(
                f"SELECT {cols} FROM submissions WHERE {scope_sql} AND status=? "
                f"ORDER BY id DESC LIMIT ?",
                scope_params + [status, limit],
            ).fetchall()
        else:
            rows = conn.execute(
                f"SELECT {cols} FROM submissions WHERE {scope_sql} ORDER BY id DESC LIMIT ?",
                scope_params + [limit],
            ).fetchall()
        out = []
        for r in rows:
            d = dict(r)
            # Campuri afisabile derivate din payload (acelasi helper ca dashboardul web);
            # payload_json brut nu se intoarce in raspuns.
            d["prezentare"] = prezentare_din_payload(d.pop("payload_json", None))
            out.append(d)
        return {"submissions": out}
    finally:
        conn.close()


# Campuri expuse de GET /v1/prezentari/{id} — allowlist explicita.
# Exclude: rar_creds_enc, payload_json, idempotency_key, sending_since.
_PREZENTARE_FIELDS = frozenset({
    "id", "status", "id_prezentare", "rar_status_code", "retry_count",
    "next_attempt_at", "created_at", "updated_at", "account_id",
    "batch_id", "row_index", "purge_after",
    # rar_error e SIGUR de expus — contine doar coduri/mesaje de validare RAR si
    # erori din catalog (niciodata creds, ex. RAR_CREDS_INVALIDE poarta doar cauza
    # "credentiale RAR invalide", fara parola). Face recovery-ul observabil prin API.
    "rar_error",
    # US-005: mediul RAR tinta (Test/Productie) — necesar pentru badge + ecou API.
    "rar_env",
    # PRD 5.19 US-010: flag `held` (tinut manual) — onestitate GET (dev vede ca NU a plecat).
    "held",
})


@router.get("/prezentari/{submission_id}")
def get_prezentare(
    submission_id: int,
    account_id: int = Depends(resolve_account_id),
) -> dict:
    conn = get_connection()
    try:
        scope_sql, scope_params = account_scope_clause(account_id)
        row = conn.execute(
            f"SELECT * FROM submissions WHERE id=? AND {scope_sql}",
            [submission_id] + scope_params,
        ).fetchone()
        if not row:
            # Acelasi mesaj indiferent daca randul exista dar apartine altui cont
            # sau nu exista deloc — nu confirmam existenta.
            raise HTTPException(status_code=404, detail="submission inexistent")
        row_dict = dict(row)
        return {k: v for k, v in row_dict.items() if k in _PREZENTARE_FIELDS}
    finally:
        conn.close()


@router.delete("/prezentari/{submission_id}")
def delete_prezentare(
    submission_id: int,
    account_id: int = Depends(resolve_account_id),
) -> dict:
    """Sterge o trimitere blocata a contului cheii API.

    Raspuns 200 + body JSON (NU 204 — clienti VFP fac string-parse). Scope evaluat
    INAINTEA starii: cross-account / inexistent -> 404 (acelasi mesaj);
    own-account `sent`/`sending` -> 409 (conflict de stare).
    """
    conn = get_connection()
    try:
        try:
            res = delete_submission(conn, account_id, submission_id)
        except SubmissionNotFound:
            raise HTTPException(status_code=404, detail="submission inexistent")
        except SubmissionStateConflict as exc:
            raise HTTPException(
                status_code=409,
                detail=f"trimiterea nu se poate sterge in starea '{exc.status}'",
            )
        return {"ok": True, **res}
    finally:
        conn.close()


@router.post("/prezentari/{submission_id}/repune")
def repune_prezentare(
    submission_id: int,
    account_id: int = Depends(resolve_account_id),
) -> dict:
    """Re-pune in coada o trimitere blocata a contului cheii API.

    `error -> queued`, re-ruleaza classify. Acelasi oracol de scope/stare ca DELETE
    (404 cross-account/inexistent, 409 sent/sending).
    """
    conn = get_connection()
    try:
        try:
            res = requeue_submission(conn, account_id, submission_id)
        except SubmissionNotFound:
            raise HTTPException(status_code=404, detail="submission inexistent")
        except SubmissionStateConflict as exc:
            raise HTTPException(
                status_code=409,
                detail=f"trimiterea nu se poate re-pune in starea '{exc.status}'",
            )
        return {"ok": True, **res}
    finally:
        conn.close()


@router.post("/prezentari/{submission_id}/trimite-acum")
def trimite_acum_prezentare(
    submission_id: int,
    account_id: int = Depends(resolve_account_id),
) -> dict:
    """Elibereaza manual un rand tinut (PRD 5.19 US-010, paritate API cu /repune).

    Scoped strict pe contul cheii API (account_id din sesiune/cheie, NICIODATA din body).
    404-before-leak pe id strain/inexistent. `held: 1 -> 0` DOAR daca randul e
    `queued AND held=1` (no-op sigur altfel: un rand deja `sending`/`sent` ramane neatins).
    Worker-ul preia randul la urmatorul poll (trimitere asincrona).
    """
    conn = get_connection()
    try:
        scope_sql, scope_params = account_scope_clause(account_id)
        row = conn.execute(
            f"SELECT id FROM submissions WHERE id=? AND {scope_sql}",
            [submission_id] + scope_params,
        ).fetchone()
        if not row:
            raise HTTPException(status_code=404, detail="submission inexistent")
        cur = conn.execute(
            f"UPDATE submissions SET held=0, updated_at=datetime('now') "
            f"WHERE id=? AND {scope_sql} AND status='queued' AND held=1",
            [submission_id] + scope_params,
        )
        conn.commit()
        return {"ok": True, "eliberat": cur.rowcount == 1}
    finally:
        conn.close()


@router.get("/nomenclator")
def get_nomenclator() -> dict:
    conn = get_connection()
    try:
        rows = conn.execute(
            "SELECT cod_prestatie, nume_prestatie, updated_at FROM nomenclator_rar ORDER BY cod_prestatie"
        ).fetchall()
        return {"nomenclator": [dict(r) for r in rows]}
    finally:
        conn.close()


AUDIT_COLUMNS = [
    "submission_id",
    "status",
    "id_prezentare",
    "account_id",
    "vin",
    "nr_inmatriculare",
    "data_prestatie",
    "odometru_final",
    "prestatii",
    "rar_status_code",
    # US-010 (PRD 5.20): mediul RAR tinta per trimitere.
    "rar_env",
    "created_at",
    "updated_at",
    "purge_after",
]


def _audit_rows(conn, date_from: str | None, date_to: str | None, status: str, account_id: int):
    """Randuri audit filtrate pe cont + data(updated_at) in [from, to].

    account_id = contul cheii API (scope obligatoriu — PII in CSV). Randuri cu
    account_id IS NULL apartin contului 1. b64_image NU intra in CSV.
    """
    scope_sql, scope_params = account_scope_clause(account_id)
    sql = (
        "SELECT id, status, id_prezentare, account_id, payload_json, rar_status_code, "
        "rar_env, created_at, updated_at, purge_after FROM submissions"
    )
    where = [scope_sql]
    params: list = list(scope_params)
    if status != "all":
        where.append("status=?")
        params.append(status)
    if date_from:
        where.append("date(updated_at) >= date(?)")
        params.append(date_from)
    if date_to:
        where.append("date(updated_at) <= date(?)")
        params.append(date_to)
    sql += " WHERE " + " AND ".join(where)
    sql += " ORDER BY id"

    for r in conn.execute(sql, params).fetchall():
        try:
            p = json.loads(r["payload_json"]) if r["payload_json"] else {}
        except (ValueError, TypeError):
            p = {}
        codes = ",".join(
            (it.get("cod_prestatie") or it.get("cod_op_service") or "")
            for it in (p.get("prestatii") or [])
            if isinstance(it, dict)
        )
        yield {
            "submission_id": r["id"],
            "status": r["status"],
            "id_prezentare": r["id_prezentare"] or "",
            # NULL→cont 1: coloana reflecta invariantul de scope, nu "" ambiguu.
            "account_id": account_or_default(r["account_id"]),
            "vin": p.get("vin") or "",
            "nr_inmatriculare": p.get("nr_inmatriculare") or "",
            "data_prestatie": p.get("data_prestatie") or "",
            "odometru_final": p.get("odometru_final") or "",
            "prestatii": codes,
            "rar_status_code": r["rar_status_code"] or "",
            # US-010 (PRD 5.20): mediul RAR tinta — coloana audit.
            "rar_env": r["rar_env"] or "",
            "created_at": r["created_at"],
            "updated_at": r["updated_at"],
            "purge_after": r["purge_after"] or "",
        }


@router.get("/audit/export")
def audit_export(
    date_from: str | None = None,
    date_to: str | None = None,
    status: str = "sent",
    account_id: int = Depends(resolve_account_id),
) -> StreamingResponse:
    """CSV audit scoped pe contul cheii API. Filtre optionale `date_from`/`date_to` (YYYY-MM-DD)

    pe data(updated_at). `status` implicit `sent` (ce a ajuns efectiv la RAR);
    `status=all` exporta toata coada contului. Leaga retinerea 90 zile prin coloana
    `purge_after`. b64_image nu se exporta.
    """
    conn = get_connection()
    try:
        buf = io.StringIO()
        writer = csv.DictWriter(buf, fieldnames=AUDIT_COLUMNS)
        writer.writeheader()
        for row in _audit_rows(conn, date_from, date_to, status, account_id):
            writer.writerow(row)
        data = buf.getvalue()
    finally:
        conn.close()

    fname = f"audit_{status}_{date_from or 'inceput'}_{date_to or 'azi'}.csv"
    return StreamingResponse(
        iter([data]),
        media_type="text/csv",
        headers={"Content-Disposition": f'attachment; filename="{fname}"'},
    )


@router.get("/mapari")
def get_mapari(
    key_account: int = Depends(resolve_account_id),
    account_id: int | None = None,
) -> dict:
    """Maparile operatie->cod ale contului curent.

    Parametrul `account_id` din query e pastrat pentru compatibilitate, dar contul
    efectiv vine MEREU din cheia API. Daca e prezent si difera -> 400.
    """
    if account_id is not None and account_id != key_account:
        raise HTTPException(
            status_code=400,
            detail="account_id din query nu corespunde contului cheii API",
        )
    conn = get_connection()
    try:
        rows = conn.execute(
            "SELECT * FROM operations_mapping WHERE account_id=? ORDER BY cod_op_service",
            (key_account,),
        ).fetchall()
        return {"mapari": [dict(r) for r in rows]}
    finally:
        conn.close()


@router.get("/mapari/pending")
def get_mapari_pending(
    account_id: int = Depends(resolve_account_id),
) -> dict:
    """Operatii ROAAUTO nemapate (din submission-uri needs_mapping) + sugestii fuzzy.

    Filtrate pe contul cheii API. Fiecare intrare: {account_id, cod_op_service,
    denumire, blocked, suggestions:[{cod_prestatie, nume_prestatie, score}]}.
    """
    conn = get_connection()
    try:
        return {"pending": pending_unmapped(conn, account_id=account_id)}
    finally:
        conn.close()


class MapareIn(BaseModel):
    cod_op_service: str = Field(..., min_length=1)
    cod_prestatie: str = Field(..., min_length=1)
    auto_send: bool = True


@router.post("/mapari")
def create_mapare(
    req: MapareIn,
    account_id: int = Depends(resolve_account_id),
) -> dict:
    """Salveaza/actualizeaza o mapare op->cod si re-rezolva submission-urile blocate.

    Contul vine din cheia API (NU din body) — un cont nu poate edita maparile
    altuia. Verifica intai ca `cod_prestatie` exista in nomenclator (nu lasam
    mapari catre coduri inexistente). Apoi upsert + re-rezolvare `needs_mapping`.
    """
    conn = get_connection()
    try:
        cod = req.cod_prestatie.strip().upper()
        exists = conn.execute(
            "SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod,)
        ).fetchone()
        if not exists:
            raise HTTPException(status_code=422, detail=f"cod_prestatie '{cod}' nu exista in nomenclator")
        save_mapping(conn, account_id, req.cod_op_service, cod, req.auto_send)
        stats = reresolve_account(conn, account_id)
        return {"saved": {"cod_op_service": req.cod_op_service.strip(), "cod_prestatie": cod}, "reresolve": stats}
    finally:
        conn.close()


class RarCredsIn(BaseModel):
    """Creds RAR durabile per-cont, stocate criptat (Fernet) in slotul per-mediu.

    `rar_target` selecteaza mediul: 'test' | 'prod'. Absent -> mediul ancorei globale
    (AUTOPASS_RAR_ENV), implicit 'test'. Schimbare aditiva — clientii vechi care nu trimit
    `rar_target` continua sa functioneze (comportament consistent cu ancora globala).
    """

    email: str = Field(..., min_length=1)
    password: str = Field(..., min_length=1, repr=False)
    rar_target: str | None = None  # 'test' | 'prod' | None -> ancora globala


@router.post("/conturi/rar-creds")
def set_rar_creds(
    req: RarCredsIn,
    account_id: int = Depends(resolve_account_id),
) -> dict:
    """Seteaza creds RAR durabile per-cont, in slotul per-mediu (US-013, env-aware).

    Slotul tinta: `req.rar_target` ('test'/'prod') sau ancora globala (AUTOPASS_RAR_ENV).
    Activeaza mediul selectat (`rar_{env}_enabled=1`). Worker-ul le foloseste ca
    fallback cand submission-ul nu mai are creds efemere. Contul vine din cheia API.
    """
    from ...config import get_settings as _gs
    _s = _gs()
    env = req.rar_target if req.rar_target in ("test", "prod") else (
        _s.rar_env if _s.rar_env in ("test", "prod") else "test"
    )
    acct = account_or_default(account_id)
    enc = encrypt_creds({"email": req.email, "password": req.password})
    conn = get_connection()
    try:
        conn.execute(
            f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?",
            (enc, acct),
        )
        return {"ok": True, "account_id": acct, "rar_env": env}
    finally:
        conn.close()


@router.delete("/conturi/rar-creds")
def delete_rar_creds(
    account_id: int = Depends(resolve_account_id),
    env: str | None = None,
) -> dict:
    """Sterge creds RAR durabile per-cont (revenire la modelul efemer Treapta 1).

    `env` (query param): 'test' | 'prod' -> sterge DOAR slotul acelui mediu + dezactiveaza-l.
    Absent -> sterge AMBELE sloturi (revenire completa). Schimbare aditiva (back-compat).
    """
    acct = account_or_default(account_id)
    conn = get_connection()
    try:
        if env in ("test", "prod"):
            conn.execute(
                f"UPDATE accounts SET rar_creds_{env}_enc=NULL, rar_{env}_enabled=0 WHERE id=?",
                (acct,),
            )
        else:
            conn.execute(
                "UPDATE accounts SET rar_creds_test_enc=NULL, rar_test_enabled=0, "
                "rar_creds_prod_enc=NULL, rar_prod_enabled=0 WHERE id=?",
                (acct,),
            )
        return {"ok": True, "account_id": acct}
    finally:
        conn.close()
