"""Extension marketplace — UX Atelier : studio UI/UX pour l’agent.

Outils :
    - ``ux_audit_html`` — audit structure / hiérarchie / accessibilité de base ;
    - ``ux_design_system`` — génère tokens CSS (couleurs, type, spacing) ;
    - ``ux_critique`` — critique structurée d’une page ou d’un CSS ;
    - ``ux_wireframe_spec`` — spécification wireframe JSON pour une intention.
"""

from __future__ import annotations

import json
import re
from html.parser import HTMLParser
from pathlib import Path

_SKIP = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build"}


class _DomProbe(HTMLParser):
    """Extracteur léger de signaux UX depuis du HTML."""

    def __init__(self) -> None:
        super().__init__()
        self.tags: list[str] = []
        self.headings: list[tuple[str, str]] = []
        self.images_missing_alt = 0
        self.images_total = 0
        self.links = 0
        self.buttons = 0
        self.forms = 0
        self.has_nav = False
        self.has_main = False
        self.has_footer = False
        self.has_h1 = False
        self.inline_styles = 0
        self._capture: str | None = None
        self._buf: list[str] = []

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        self.tags.append(tag)
        ad = {k: (v or "") for k, v in attrs}
        if tag in {"h1", "h2", "h3", "h4", "h5", "h6"}:
            self._capture = tag
            self._buf = []
            if tag == "h1":
                self.has_h1 = True
        if tag == "img":
            self.images_total += 1
            if not ad.get("alt", "").strip():
                self.images_missing_alt += 1
        if tag == "a":
            self.links += 1
        if tag == "button" or ad.get("role") == "button":
            self.buttons += 1
        if tag == "form":
            self.forms += 1
        if tag == "nav":
            self.has_nav = True
        if tag == "main":
            self.has_main = True
        if tag == "footer":
            self.has_footer = True
        if "style" in ad:
            self.inline_styles += 1

    def handle_endtag(self, tag: str) -> None:
        if self._capture == tag:
            text = " ".join("".join(self._buf).split())[:120]
            self.headings.append((tag, text))
            self._capture = None
            self._buf = []

    def handle_data(self, data: str) -> None:
        if self._capture:
            self._buf.append(data)


def register(workspace: Path) -> list:
    """Enregistre les outils UX Atelier."""

    def _resolve(path: str) -> Path:
        p = Path(path).expanduser()
        return p.resolve() if p.is_absolute() else (workspace / path).resolve()

    def ux_audit_html(path: str = "index.html") -> str:
        """Audit UX/UI d’un fichier HTML (structure, a11y, densité, CTA).

        Args:
            path: Fichier HTML relatif au workspace ou absolu.
        """
        target = _resolve(path)
        if not target.is_file():
            return json.dumps({"error": f"fichier introuvable: {target}"}, ensure_ascii=False)

        html = target.read_text(encoding="utf-8", errors="replace")
        probe = _DomProbe()
        try:
            probe.feed(html)
        except Exception as exc:  # noqa: BLE001
            return json.dumps({"error": f"HTML illisible: {exc}"}, ensure_ascii=False)

        issues: list[dict] = []
        score = 100

        if not probe.has_h1:
            issues.append({"severity": "high", "code": "no_h1", "msg": "Aucun H1 — hiérarchie absente."})
            score -= 15
        h1s = [h for h in probe.headings if h[0] == "h1"]
        if len(h1s) > 1:
            issues.append({"severity": "med", "code": "multi_h1", "msg": f"{len(h1s)} H1 — un seul landmark titre."})
            score -= 8
        if not probe.has_main:
            issues.append({"severity": "med", "code": "no_main", "msg": "Pas de <main> — landmark sémantique manquant."})
            score -= 8
        if not probe.has_nav:
            issues.append({"severity": "low", "code": "no_nav", "msg": "Pas de <nav> détecté."})
            score -= 4
        if probe.images_missing_alt:
            issues.append({
                "severity": "high",
                "code": "img_alt",
                "msg": f"{probe.images_missing_alt}/{probe.images_total} images sans alt.",
            })
            score -= min(20, probe.images_missing_alt * 4)
        if probe.inline_styles > 5:
            issues.append({
                "severity": "med",
                "code": "inline_css",
                "msg": f"{probe.inline_styles} styles inline — freine le design system.",
            })
            score -= 6
        if probe.buttons + probe.links == 0:
            issues.append({"severity": "high", "code": "no_cta", "msg": "Aucun lien/bouton — pas de CTA."})
            score -= 12

        # Densité hero approximative : beaucoup de texte avant le 1er H1.
        first_h1 = html.lower().find("<h1")
        preamble = html[: first_h1 if first_h1 > 0 else 800]
        if preamble.count("<") > 40 and first_h1 > 0:
            issues.append({
                "severity": "med",
                "code": "hero_clutter",
                "msg": "DOM dense avant le H1 — risque de hero surchargé.",
            })
            score -= 7

        recommendations = [
            "Un seul job par section ; hero = marque + 1 headline + 1 phrase + CTAs.",
            "Éviter les cards décoratives ; réserver les cards aux interactions.",
            "Ancrer une direction visuelle claire (tokens) plutôt qu’un fond plat.",
            "Typo expressive hors Inter/Roboto/Arial ; contraste AA minimum.",
        ]

        return json.dumps(
            {
                "path": str(target.relative_to(workspace)) if target.is_relative_to(workspace) else str(target),
                "score": max(0, score),
                "landmarks": {
                    "nav": probe.has_nav,
                    "main": probe.has_main,
                    "footer": probe.has_footer,
                    "h1": probe.has_h1,
                },
                "counts": {
                    "headings": len(probe.headings),
                    "links": probe.links,
                    "buttons": probe.buttons,
                    "forms": probe.forms,
                    "images": probe.images_total,
                    "inline_styles": probe.inline_styles,
                },
                "heading_outline": [{"tag": t, "text": txt} for t, txt in probe.headings[:20]],
                "issues": issues,
                "recommendations": recommendations,
            },
            ensure_ascii=False,
            indent=2,
        )

    def ux_design_system(
        brand: str = "MiniAgentic",
        mood: str = "industrial dark",
        accent: str = "#ff6b1a",
        out_path: str = "design-tokens.css",
    ) -> str:
        """Génère un design system CSS (tokens couleur, type, spacing, motion).

        Args:
            brand: Nom de marque pour les commentaires.
            mood: Direction visuelle (ex: industrial dark, editorial light).
            accent: Couleur d'accent hex.
            out_path: Fichier CSS de sortie (relatif workspace).
        """
        target = _resolve(out_path)
        target.parent.mkdir(parents=True, exist_ok=True)
        mood_l = mood.lower()
        if "light" in mood_l or "editorial" in mood_l:
            bg, ink, mute = "#f6f1ea", "#1a1512", "#6b5e55"
            surface = "#fffdf9"
        else:
            bg, ink, mute = "#0a0a0a", "#f2f2f0", "#8a8a82"
            surface = "#141414"

        css = f"""/* {brand} — design tokens · mood: {mood} */
:root {{
  --brand: {brand!r};
  --color-bg: {bg};
  --color-surface: {surface};
  --color-ink: {ink};
  --color-mute: {mute};
  --color-accent: {accent};
  --color-accent-soft: color-mix(in oklab, {accent} 22%, transparent);
  --color-line: color-mix(in oklab, {ink} 14%, transparent);

  --font-display: "Syne", "Space Grotesk", sans-serif;
  --font-body: "IBM Plex Sans", "Source Sans 3", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, monospace;

  --step--1: clamp(0.78rem, 0.72rem + 0.2vw, 0.88rem);
  --step-0: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  --step-1: clamp(1.25rem, 1.1rem + 0.6vw, 1.6rem);
  --step-2: clamp(1.6rem, 1.3rem + 1.1vw, 2.25rem);
  --step-3: clamp(2.1rem, 1.6rem + 1.8vw, 3.2rem);
  --step-4: clamp(2.8rem, 2rem + 2.8vw, 4.5rem);

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2.5rem;
  --space-6: 4rem;
  --space-7: 6.5rem;

  --radius-sm: 2px;
  --radius-md: 6px;
  --shadow-soft: 0 18px 50px color-mix(in oklab, #000 35%, transparent);
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast: 160ms;
  --dur-med: 420ms;
}}

html {{ background: var(--color-bg); color: var(--color-ink); }}
body {{
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.55;
  background:
    radial-gradient(1200px 600px at 10% -10%, var(--color-accent-soft), transparent 55%),
    var(--color-bg);
}}
h1, h2, h3 {{
  font-family: var(--font-display);
  line-height: 1.05;
  letter-spacing: -0.03em;
  font-weight: 700;
}}
h1 {{ font-size: var(--step-4); }}
h2 {{ font-size: var(--step-3); }}
h3 {{ font-size: var(--step-2); }}
a {{ color: var(--color-accent); }}
.btn {{
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: 0.85rem 1.25rem;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-sm);
  background: var(--color-accent);
  color: #0a0a0a;
  font-weight: 650;
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-out), filter var(--dur-fast) var(--ease-out);
}}
.btn:hover {{ transform: translateY(-1px); filter: brightness(1.05); }}
.section {{ padding: var(--space-7) var(--space-4); }}
.lede {{ color: var(--color-mute); max-width: 36rem; font-size: var(--step-1); }}
"""
        target.write_text(css, encoding="utf-8")
        rel = str(target.relative_to(workspace)) if target.is_relative_to(workspace) else str(target)
        return json.dumps(
            {
                "written": rel,
                "brand": brand,
                "mood": mood,
                "accent": accent,
                "tokens": ["color-*", "font-*", "step-*", "space-*", "radius-*", "ease-*"],
                "hint": "Importe ce fichier en premier dans tes pages ; dérive composants depuis les tokens.",
            },
            ensure_ascii=False,
            indent=2,
        )

    def ux_critique(path: str, focus: str = "landing") -> str:
        """Critique UX structurée d’un HTML ou CSS (signaux + plan d’action).

        Args:
            path: Fichier à critiquer.
            focus: Contexte (landing, dashboard, docs, form).
        """
        target = _resolve(path)
        if not target.is_file():
            return json.dumps({"error": f"fichier introuvable: {target}"}, ensure_ascii=False)

        text = target.read_text(encoding="utf-8", errors="replace")
        lower = text.lower()
        smells: list[str] = []
        wins: list[str] = []

        for bad in ("font-family: inter", "font-family: roboto", "font-family: arial", "#667eea", "purple"):
            if bad in lower:
                smells.append(f"Cliché design détecté: `{bad}` — change de direction visuelle.")
        if text.count("border-radius") > 12 and focus == "landing":
            smells.append("Trop de radius — look « card soup » probable.")
        if lower.count("box-shadow") > 8:
            smells.append("Shadows empilées — simplifier la profondeur.")
        if "clamp(" in lower:
            wins.append("Typo/spacing fluides via clamp() — bon réflexe responsive.")
        if ":root" in lower or "--color" in lower:
            wins.append("Design tokens présents — base solide.")
        if focus == "landing" and lower.count("<section") >= 3:
            wins.append("Découpage en sections — bon rythme de scroll.")
        if focus == "landing" and re.search(r"stat|counter|badge|chip", lower):
            smells.append("Stats/badges/chips dans le flux — risque de clutter hors job de section.")

        actions = [
            {"priority": 1, "action": "Verrouiller une composition hero unique (marque dominante)."},
            {"priority": 2, "action": "Une intention par section + un ancrage visuel réel (produit/contexte)."},
            {"priority": 3, "action": "2–3 motions intentionnelles max (présence, pas bruit)."},
            {"priority": 4, "action": "Audit contraste + focus states clavier."},
        ]
        return json.dumps(
            {
                "path": str(target),
                "focus": focus,
                "wins": wins or ["Peu de signaux positifs automatiques — lire le fichier à la main."],
                "smells": smells or ["Pas de smell évident au scan lexical."],
                "action_plan": actions,
            },
            ensure_ascii=False,
            indent=2,
        )

    def ux_wireframe_spec(intent: str, page: str = "landing") -> str:
        """Produit une spécification wireframe JSON (sections, hiérarchie, CTAs).

        Args:
            intent: Intention produit / promesse.
            page: Type de page (landing, pricing, docs, app-shell).
        """
        templates = {
            "landing": [
                {"id": "hero", "job": "brand + promesse", "elements": ["logo/marque", "headline", "sub", "CTA primary", "CTA secondary"], "forbid": ["stats", "cards", "badges"]},
                {"id": "proof", "job": "ancrage réel", "elements": ["visuel produit full-bleed ou scène", "légende courte"]},
                {"id": "mechanism", "job": "comment ça marche", "elements": ["3 étapes max", "pas de cards décoratives"]},
                {"id": "install", "job": "passage à l’action", "elements": ["snippet", "CTA"]},
            ],
            "pricing": [
                {"id": "hero", "job": "cadrer l’offre", "elements": ["headline", "sub"]},
                {"id": "tiers", "job": "comparer", "elements": ["2–3 plans", "CTA par plan"]},
                {"id": "faq", "job": "lever objections", "elements": ["5 questions"]},
            ],
            "docs": [
                {"id": "nav", "job": "orientation", "elements": ["sidebar toc"]},
                {"id": "article", "job": "expliquer une chaîne", "elements": ["H1", "prose", "code"]},
            ],
            "app-shell": [
                {"id": "chrome", "job": "navigation persistante", "elements": ["topbar", "sidebar"]},
                {"id": "canvas", "job": "travail principal", "elements": ["vue active", "états vides"]},
            ],
        }
        sections = templates.get(page, templates["landing"])
        spec = {
            "page": page,
            "intent": intent,
            "principles": [
                "Une composition = un job",
                "Marque hero-level",
                "Pas de cards hors interaction",
                "Motion = hiérarchie",
            ],
            "sections": sections,
            "copy_skeleton": {
                "headline": f"À écrire autour de : {intent}",
                "sub": "Une phrase. Pas de jargon.",
                "cta_primary": "Action concrète",
                "cta_secondary": "En savoir plus",
            },
        }
        return json.dumps(spec, ensure_ascii=False, indent=2)

    return [ux_audit_html, ux_design_system, ux_critique, ux_wireframe_spec]
