"""Générateur HTML · projets_master.json → site public (vente.otov7.com).

Livrable Publiciste · Sprint 2 · Semaine 3 (aperçu ; le parser Semaine 2 est le
cœur du sprint courant). Rendu server-side pur stdlib, marque luxury (#4).

Interdits appliqués (AGENT.md §Règles absolues) :
  - ❌ Jamais éditer directement l'index publié : on passe TOUJOURS par ce
    générateur, qui écrit une sortie complète et horodatée.
  - ❌ Zéro prix inventé : un projet sans prix numérique n'affiche PAS de grille,
    mais « Prochainement · Détails à venir » (contrainte #6).
  - ❌ Rendus IA génériques interdits : on n'affiche que les rendus référencés
    dans la faisabilité (liés au projet réel).
"""

from __future__ import annotations

import html
import os
from typing import Any

from . import branding

_TEMPLATE = os.path.join(os.path.dirname(__file__), "..", "templates", "site_public.html.tmpl")

_TAGLINE = "Résidences d'exception en République dominicaine · faisabilités 4 volets."
_A_VENIR = "Prochainement · Détails à venir"


def _esc(text: Any) -> str:
    return html.escape(str(text), quote=True)


def _fmt_usd(value: float) -> str:
    return f"USD {value:,.0f}".replace(",", " ")


def _fmt_dop(value: float) -> str:
    return f"DOP {value:,.0f}".replace(",", " ")


def _fmt_m2(value: float) -> str:
    txt = f"{value:,.0f}".replace(",", " ") if float(value).is_integer() else f"{value:,.1f}".replace(",", " ")
    return f"{txt} m²"


def _prix_depuis(typologies: list[dict[str, Any]]) -> float | None:
    """Prix « à partir de » du projet = min des prix USD numériques présents."""
    prices = [t["prix_depuis_usd"] for t in typologies if isinstance(t.get("prix_depuis_usd"), (int, float))]
    return min(prices) if prices else None


def _render_typologies(typologies: list[dict[str, Any]]) -> str:
    rows = []
    for t in typologies:
        surface = t.get("surface_totale_m2") or t.get("surface_interieure_m2")
        surface_txt = _fmt_m2(surface) if isinstance(surface, (int, float)) else "—"
        prix = t.get("prix_depuis_usd")
        prix_txt = (
            f'<span class="prix-depuis">{_esc(_fmt_usd(prix))}</span>'
            if isinstance(prix, (int, float))
            else "—"
        )
        rows.append(
            "        <tr>"
            f"<td>{_esc(t.get('nom', ''))}</td>"
            f"<td>{surface_txt}</td>"
            f"<td>{prix_txt}</td>"
            "</tr>"
        )
    return (
        '      <table class="prix">\n'
        "        <tr><th>Typologie</th><th>Surface</th><th>À partir de</th></tr>\n"
        + "\n".join(rows)
        + "\n      </table>"
    )


def _render_inclus(inclus: list[str]) -> str:
    if not inclus:
        return ""
    items = "\n".join(f"        <li>{_esc(x)}</li>" for x in inclus)
    return f'      <ul class="inclus">\n{items}\n      </ul>'


def _hero_media(projet: dict[str, Any]) -> str:
    rendus = projet.get("rendus", [])
    hero = next((r for r in rendus if r.get("hero")), rendus[0] if rendus else None)
    if not hero:
        return '      <div class="media"></div>'
    # Convention de chemin public (miroir de /opt/oto/sites/static/projets/pXX/).
    code = projet["code"].lower()
    src = f"/static/projets/{code}/{hero['fichier']}"
    style = f"background-image:url('{_esc(src)}')"
    return f'      <div class="media" style="{style}" role="img" aria-label="{_esc(projet.get("nom",""))}"></div>'


def render_projet(projet: dict[str, Any]) -> str:
    statut = projet.get("statut", "en_developpement")
    statut_label = branding.statut_label(statut)
    nom = projet.get("nom", projet.get("code", ""))
    loc = projet.get("localisation", "")

    parts = [f'    <article class="projet" data-code="{_esc(projet.get("code",""))}">']
    parts.append(_hero_media(projet))
    parts.append('      <div class="corps">')
    parts.append(f'        <span class="statut {_esc(statut)}">{_esc(statut_label)}</span>')
    parts.append(f"        <h2>{_esc(nom)}</h2>")
    if loc:
        parts.append(f'        <p class="localisation">{_esc(loc)}</p>')

    positionnement = (projet.get("positionnement") or {}).get("fr")
    if positionnement:
        parts.append(f'        <p class="positionnement">{_esc(positionnement)}</p>')

    typologies = projet.get("typologies", [])
    show_prices = statut not in branding.STATUTS_SANS_PRIX and _prix_depuis(typologies) is not None
    if show_prices:
        parts.append(_render_typologies(typologies))
    else:
        # Aucun prix inventé — message d'attente (contrainte #6).
        parts.append(f'        <p class="a-venir">{_esc(_A_VENIR)}</p>')

    inclus_html = _render_inclus(projet.get("inclus", []))
    if inclus_html:
        parts.append(inclus_html)

    parts.append("      </div>")
    parts.append("    </article>")
    return "\n".join(parts)


def render_site(master: dict[str, Any]) -> str:
    """projets_master.json (dict) → page HTML complète (str)."""
    with open(_TEMPLATE, encoding="utf-8") as fh:
        tmpl = fh.read()

    projets_html = "\n".join(render_projet(p) for p in master.get("projets", []))
    generated_at = master.get("generated_at", "")
    template_version = master.get("template_version", "")
    footer = (
        f"Contenu généré automatiquement depuis les faisabilités canoniques · "
        f"template {_esc(template_version)} · {_esc(generated_at)}. "
        "Prix « à partir de » en USD + DOP (Cardnet). Aucune donnée inventée — "
        "chaque valeur provient de la faisabilité du projet."
    )

    replacements = {
        "{{TITRE}}": "Helios RD · Résidences d'exception",
        "{{META_DESCRIPTION}}": _TAGLINE,
        "{{TAGLINE}}": _TAGLINE,
        "{{COLOR_BG}}": branding.COLOR_BG,
        "{{COLOR_ACCENT}}": branding.COLOR_ACCENT,
        "{{COLOR_INK}}": branding.COLOR_INK,
        "{{COLOR_MUTED}}": branding.COLOR_MUTED,
        "{{FONT_DISPLAY}}": branding.FONT_DISPLAY,
        "{{FONT_BODY}}": branding.FONT_BODY,
        "{{PROJETS}}": projets_html,
        "{{FOOTER}}": footer,
    }
    for key, val in replacements.items():
        tmpl = tmpl.replace(key, val)
    return tmpl
