"""Extension marketplace — SEO Forge : audit SEO technique + outline contenu.

Outils :
    - ``seo_audit_page`` — audit on-page (title, meta, headings, OG, images) ;
    - ``seo_site_crawl`` — crawl local de pages HTML liées ;
    - ``seo_content_outline`` — outline SEO pour une intention de recherche.
"""

from __future__ import annotations

import json
import re
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urlparse

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


class _SeoParser(HTMLParser):
    def __init__(self) -> None:
        super().__init__()
        self.title = ""
        self.meta: dict[str, str] = {}
        self.canonical = ""
        self.headings: list[tuple[str, str]] = []
        self.images: list[dict] = []
        self.links: list[str] = []
        self.json_ld = 0
        self._cap: str | None = None
        self._buf: list[str] = []
        self._in_script_ld = False

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        ad = {k: (v or "") for k, v in attrs}
        if tag == "title":
            self._cap = "title"
            self._buf = []
        elif tag in {"h1", "h2", "h3"}:
            self._cap = tag
            self._buf = []
        elif tag == "meta":
            key = ad.get("name") or ad.get("property") or ad.get("http-equiv")
            if key and "content" in ad:
                self.meta[key.lower()] = ad["content"]
        elif tag == "link" and ad.get("rel", "").lower() == "canonical":
            self.canonical = ad.get("href", "")
        elif tag == "img":
            self.images.append({"src": ad.get("src", ""), "alt": ad.get("alt", "")})
        elif tag == "a" and ad.get("href"):
            self.links.append(ad["href"])
        elif tag == "script" and "ld+json" in ad.get("type", "").lower():
            self._in_script_ld = True

    def handle_endtag(self, tag: str) -> None:
        if self._cap == tag:
            text = " ".join("".join(self._buf).split())
            if tag == "title":
                self.title = text
            else:
                self.headings.append((tag, text[:160]))
            self._cap = None
            self._buf = []
        if tag == "script":
            self._in_script_ld = False

    def handle_data(self, data: str) -> None:
        if self._cap:
            self._buf.append(data)
        if self._in_script_ld and data.strip():
            self.json_ld += 1


def register(workspace: Path) -> list:
    """Enregistre les outils SEO Forge."""

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

    def seo_audit_page(path: str = "index.html") -> str:
        """Audit SEO on-page d’un fichier HTML local.

        Args:
            path: Fichier HTML à auditer.
        """
        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")
        p = _SeoParser()
        try:
            p.feed(html)
        except Exception as exc:  # noqa: BLE001
            return json.dumps({"error": str(exc)}, ensure_ascii=False)

        issues: list[dict] = []
        score = 100
        title_len = len(p.title)
        if not p.title:
            issues.append({"sev": "high", "msg": "Title manquant"})
            score -= 20
        elif title_len < 25 or title_len > 65:
            issues.append({"sev": "med", "msg": f"Title longueur {title_len} (cible ~50–60)"})
            score -= 8

        desc = p.meta.get("description", "")
        if not desc:
            issues.append({"sev": "high", "msg": "Meta description absente"})
            score -= 15
        elif not (70 <= len(desc) <= 165):
            issues.append({"sev": "med", "msg": f"Meta description longueur {len(desc)}"})
            score -= 6

        h1 = [h for h in p.headings if h[0] == "h1"]
        if len(h1) != 1:
            issues.append({"sev": "high", "msg": f"{len(h1)} H1 (attendu: 1)"})
            score -= 12
        if not p.canonical:
            issues.append({"sev": "med", "msg": "Canonical manquant"})
            score -= 6
        if "og:title" not in p.meta:
            issues.append({"sev": "low", "msg": "og:title manquant"})
            score -= 4
        if "og:image" not in p.meta:
            issues.append({"sev": "med", "msg": "og:image manquant"})
            score -= 6
        missing_alt = sum(1 for img in p.images if not (img.get("alt") or "").strip())
        if missing_alt:
            issues.append({"sev": "med", "msg": f"{missing_alt} images sans alt"})
            score -= min(12, missing_alt * 2)
        if p.json_ld == 0:
            issues.append({"sev": "low", "msg": "Pas de JSON-LD"})
            score -= 3

        return json.dumps(
            {
                "path": str(target),
                "score": max(0, score),
                "title": p.title,
                "description": desc,
                "canonical": p.canonical,
                "robots": p.meta.get("robots", ""),
                "og": {k: v for k, v in p.meta.items() if k.startswith("og:")},
                "headings": [{"tag": t, "text": txt} for t, txt in p.headings[:30]],
                "images": len(p.images),
                "internal_ish_links": sum(1 for href in p.links if not href.startswith(("http://", "https://", "mailto:", "tel:"))),
                "json_ld_blocks": p.json_ld,
                "issues": issues,
            },
            ensure_ascii=False,
            indent=2,
        )

    def seo_site_crawl(where: str = ".", max_pages: int = 40) -> str:
        """Crawl local des HTML : inventaire titles + liens cassés relatifs.

        Args:
            where: Racine du site.
            max_pages: Plafond de pages.
        """
        root = _resolve(where)
        if not root.is_dir():
            return json.dumps({"error": f"répertoire introuvable: {root}"}, ensure_ascii=False)

        pages = []
        for path in sorted(root.rglob("*.html")):
            if any(part in _SKIP for part in path.parts):
                continue
            pages.append(path)
            if len(pages) >= max_pages:
                break

        inventory = []
        broken: list[dict] = []
        for path in pages:
            html = path.read_text(encoding="utf-8", errors="replace")
            p = _SeoParser()
            try:
                p.feed(html)
            except Exception:
                continue
            rel = str(path.relative_to(root))
            inventory.append({"page": rel, "title": p.title, "h1": next((t for tag, t in p.headings if tag == "h1"), "")})
            for href in p.links:
                if href.startswith(("http://", "https://", "mailto:", "tel:", "#", "javascript:")):
                    continue
                clean = href.split("#")[0].split("?")[0]
                if not clean:
                    continue
                # ignore absolute filesystem-looking
                if urlparse(clean).scheme:
                    continue
                candidate = (path.parent / clean).resolve()
                if not candidate.exists():
                    # try as site-root relative
                    alt = (root / clean.lstrip("/")).resolve()
                    if not alt.exists():
                        broken.append({"from": rel, "href": href})

        titles = [x["title"] for x in inventory if x["title"]]
        dup_titles = sorted({t for t in titles if titles.count(t) > 1})

        return json.dumps(
            {
                "root": str(root),
                "pages": len(inventory),
                "inventory": inventory,
                "duplicate_titles": dup_titles[:20],
                "broken_links_sample": broken[:40],
                "broken_links_count": len(broken),
            },
            ensure_ascii=False,
            indent=2,
        )

    def seo_content_outline(keyword: str, intent: str = "informational", locale: str = "fr") -> str:
        """Génère un outline SEO actionnable pour une requête.

        Args:
            keyword: Mot-clé / requête cible.
            intent: informational | commercial | transactional | navigational.
            locale: Locale de contenu.
        """
        intent = intent.lower().strip()
        sections = [
            {"h2": f"Qu’est-ce que {keyword} ?", "goal": "définition claire + entités"},
            {"h2": f"Pourquoi {keyword} compte", "goal": "bénéfices / enjeux"},
            {"h2": "Comment démarrer", "goal": "étapes concrètes", "h3": ["Prérequis", "Installation", "Premier succès"]},
            {"h2": "Erreurs fréquentes", "goal": "PAA / objections"},
            {"h2": "FAQ", "goal": "featured snippets", "bullets": 5},
        ]
        if intent in {"commercial", "transactional"}:
            sections.insert(2, {"h2": "Comparatif / alternatives", "goal": "aide à la décision"})
            sections.append({"h2": "Passer à l’action", "goal": "CTA + preuve"})
        outline = {
            "keyword": keyword,
            "intent": intent,
            "locale": locale,
            "title_ideas": [
                f"{keyword} : guide complet ({locale.upper()})",
                f"Comment maîtriser {keyword} sans friction",
                f"{keyword} — checklist pratique",
            ],
            "meta_description_seed": f"Guide {keyword} : étapes, erreurs à éviter, checklist. Local, actionnable, {locale}.",
            "serp_features_to_target": ["featured snippet", "FAQ", "People Also Ask"],
            "sections": sections,
            "internal_linking": [
                "relier vers page produit / install",
                "relier vers doc technique",
                "relier vers marketplace / extensions si pertinent",
            ],
        }
        return json.dumps(outline, ensure_ascii=False, indent=2)

    return [seo_audit_page, seo_site_crawl, seo_content_outline]
