"""Cœur métier du module Financement Bancaire : apport initial + gate check.

Fonctions PURES (aucune I/O, aucun horodatage) → testables et déterministes.
Elles matérialisent l'exigence Michel (DIRECTIVE + amendement 2026-08-03) :

  AUCUN dossier n'est transmis à la banque tant que les 4 conditions ne sont pas
  remplies :
    1. apport_initial_complet — dépôt initial (20 % résident RD · 30 % étranger)
       intégralement versé et vérifié ;
    2. documents_exiges       — tous les documents `is_required` de la banque
       choisie au statut « Validé WAG » ou « Envoyé banque » ;
    3. autorisations_signees  — toutes les autorisations signées (date non nulle) ;
    4. validation_wag         — un conseiller WAG a validé (`wag_validated_by`).

Anti-invention (#6) : les taux d'apport (20/30) et la liste des documents/
autorisations ne sont JAMAIS codés en dur ici — ils proviennent du contrat
`financement_spec.json` via `GateConfig`. Changer le contrat change le gate,
sans toucher ce code.
"""

from __future__ import annotations

from typing import Any

# Ordre canonique des 4 clés de condition du gate (= ordre du contrat / bannière).
CONDITION_KEYS = (
    "apport_initial_complet",
    "documents_exiges",
    "autorisations_signees",
    "validation_wag",
)


class GateConfig:
    """Vue en lecture du contrat, indexée pour le calcul du gate."""

    def __init__(self, spec: dict) -> None:
        self.taux_apport_pct: dict[str, float] = {
            rt["code"]: float(rt["taux_apport_pct"])
            for rt in spec["apport"]["residence_types"]
        }
        self.documents: list[dict] = list(spec["documents"])
        self.autorisations: list[dict] = list(spec["autorisations"])
        self.statuts_recevables: set[str] = set(spec["document_statuses_recevables"])
        self.banques: dict[str, dict] = {b["id"]: b for b in spec["banques"]}

    @classmethod
    def from_spec(cls, spec: dict) -> "GateConfig":
        return cls(spec)


def _round1(x: float) -> float:
    """Arrondi à 1 décimale, stable pour un artefact diffable."""
    return round(x + 0.0, 1)


def apport_requis_usd(prix_usd: float, residence_type: str, cfg: GateConfig) -> float:
    """Apport requis = prix × taux (20 % résident RD, 30 % étranger)."""
    if residence_type not in cfg.taux_apport_pct:
        raise KeyError(
            f"residence_type {residence_type!r} inconnu "
            f"(attendu : {sorted(cfg.taux_apport_pct)})."
        )
    return round(float(prix_usd) * cfg.taux_apport_pct[residence_type] / 100.0, 2)


def apport_verse_usd(dossier: dict) -> float:
    """Somme des versements enregistrés (0 si aucun)."""
    total = 0.0
    for p in dossier.get("paiements", []):
        total += float(p.get("montant_verse_usd", 0) or 0)
    return round(total, 2)


def required_document_ids(residence_type: str, cfg: GateConfig) -> list[str]:
    """Documents `is_required` applicables au type de résidence du client.

    Un document est exigé si `is_required` ET (`applies_to` == "all" OU
    `applies_to` == residence_type). Les documents « étrangers uniquement » ne
    s'appliquent donc pas à un résident RD.
    """
    out: list[str] = []
    for d in cfg.documents:
        if not d.get("is_required"):
            continue
        applies = d.get("applies_to", "all")
        if applies in ("all", residence_type):
            out.append(d["id"])
    return out


def _cond_apport(dossier: dict, cfg: GateConfig) -> dict:
    residence = dossier.get("residence_type")
    required = apport_requis_usd(dossier.get("prix_usd", 0), residence, cfg)
    paid = apport_verse_usd(dossier)
    remaining = round(max(0.0, required - paid), 2)
    pct = _round1(min(100.0, (paid / required * 100.0) if required > 0 else 100.0))
    return {
        "ok": paid + 1e-9 >= required,
        "required": required,
        "paid": paid,
        "remaining": remaining,
        "percent": pct,
    }


def _cond_documents(dossier: dict, cfg: GateConfig) -> dict:
    residence = dossier.get("residence_type")
    required_ids = required_document_ids(residence, cfg)
    by_id = {d["id"]: d for d in dossier.get("documents", [])}
    deposited = 0
    validated = 0
    for did in required_ids:
        statut = (by_id.get(did) or {}).get("statut", "Non déposé")
        if statut != "Non déposé":
            deposited += 1
        if statut in cfg.statuts_recevables:
            validated += 1
    total = len(required_ids)
    pct = _round1((validated / total * 100.0) if total > 0 else 100.0)
    return {
        "ok": validated == total,
        "total": total,
        "deposited": deposited,
        "validated": validated,
        "percent": pct,
    }


def _cond_autorisations(dossier: dict, cfg: GateConfig) -> dict:
    required_ids = [a["id"] for a in cfg.autorisations]
    by_id = {a["id"]: a for a in dossier.get("autorisations", [])}
    signed = 0
    for aid in required_ids:
        if (by_id.get(aid) or {}).get("signature_date"):
            signed += 1
    total = len(required_ids)
    pct = _round1((signed / total * 100.0) if total > 0 else 100.0)
    return {"ok": signed == total, "total": total, "signed": signed, "percent": pct}


def _cond_validation_wag(dossier: dict) -> dict:
    validated_by = dossier.get("wag_validated_by")
    ok = bool(validated_by)
    return {"ok": ok, "validated_by": validated_by, "percent": 100.0 if ok else 0.0}


def gate_status(dossier: dict, cfg: GateConfig) -> dict:
    """État complet du gate — forme consommée par le frontend (barres 0-100 %).

    Correspond au contrat de l'endpoint
    `GET /api/hypotheque/dossier/{id}/gate-status` (DIRECTIVE amendement).
    """
    conditions = {
        "apport_initial_complet": _cond_apport(dossier, cfg),
        "documents_exiges": _cond_documents(dossier, cfg),
        "autorisations_signees": _cond_autorisations(dossier, cfg),
        "validation_wag": _cond_validation_wag(dossier),
    }
    can_submit = all(conditions[k]["ok"] for k in CONDITION_KEYS)
    completed = sum(1 for k in CONDITION_KEYS if conditions[k]["ok"])
    overall = _round1(
        sum(conditions[k]["percent"] for k in CONDITION_KEYS) / len(CONDITION_KEYS)
    )
    return {
        "can_submit": can_submit,
        "conditions_total": len(CONDITION_KEYS),
        "conditions_completed": completed,
        "overall_percent": overall,
        "conditions": conditions,
    }


def can_submit_dossier(dossier: dict, cfg: GateConfig) -> tuple[bool, list[str]]:
    """(peut_soumettre, raisons_de_blocage) — vide si soumissible.

    `submit_to_bank()` doit renvoyer 403 + ces raisons tant que la liste n'est
    pas vide. Ordre des raisons = ordre canonique des conditions.
    """
    status = gate_status(dossier, cfg)
    reasons: list[str] = []
    c = status["conditions"]
    if not c["apport_initial_complet"]["ok"]:
        a = c["apport_initial_complet"]
        reasons.append(
            f"Apport initial incomplet : {a['paid']:.0f}/{a['required']:.0f} USD versés "
            f"(reste {a['remaining']:.0f} USD · {a['percent']}%)."
        )
    if not c["documents_exiges"]["ok"]:
        d = c["documents_exiges"]
        reasons.append(
            f"Documents exigés incomplets : {d['validated']}/{d['total']} validés WAG."
        )
    if not c["autorisations_signees"]["ok"]:
        s = c["autorisations_signees"]
        reasons.append(
            f"Autorisations non signées : {s['signed']}/{s['total']} signées."
        )
    if not c["validation_wag"]["ok"]:
        reasons.append("Validation WAG manquante (aucun conseiller référent n'a validé).")
    return status["can_submit"], reasons
