"""Change intelligence — Step 2: "who broke it and when".

Every crawl stores a normalized fingerprint of the SEO-critical elements
of each page. Between crawls we diff at the *element* level and correlate
regressions with the most recent deploy reported to /webhook/deploy.
"""
from __future__ import annotations
import json
from bs4 import BeautifulSoup

from .db import DB

# element -> severity when it regresses (changes badly or disappears)
SEVERITY = {
    "noindex":       "critical",   # page became noindexed
    "canonical":     "critical",
    "title":         "warning",
    "meta_description": "warning",
    "h1":            "warning",
    "schema_types":  "critical",   # structured data disappeared
    "robots_meta":   "critical",
    "og_title":      "info",
    "internal_links": "info",
    "hreflang_count": "warning",
    "status":        "critical",
}


def extract_fingerprint(url: str, html: str, status: int) -> dict:
    soup = BeautifulSoup(html, "html.parser")

    def meta(name):
        m = soup.find("meta", attrs={"name": name})
        return (m.get("content") or "").strip() if m else ""

    canon = soup.find("link", rel="canonical")
    og = soup.find("meta", property="og:title")
    h1 = soup.find("h1")

    schema_types = []
    for b in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(b.string or "{}")
            for it in (data if isinstance(data, list) else [data]):
                t = it.get("@type", "")
                schema_types.extend(t if isinstance(t, list) else [t])
        except Exception:
            pass

    robots = meta("robots").lower()
    internal = [a for a in soup.find_all("a", href=True)
                if not a["href"].startswith(("http", "//", "mailto:", "tel:"))]

    return {
        "status": status,
        "title": (soup.title.string or "").strip() if soup.title and soup.title.string else "",
        "meta_description": meta("description"),
        "canonical": canon.get("href", "") if canon else "",
        "robots_meta": robots,
        "noindex": "noindex" in robots,
        "h1": h1.get_text(" ", strip=True) if h1 else "",
        "schema_types": sorted(set(t for t in schema_types if t)),
        "og_title": (og.get("content") or "").strip() if og else "",
        "internal_links": len(internal),
        "hreflang_count": len(soup.find_all("link", rel="alternate", hreflang=True)),
    }


def _severity(element: str, old, new) -> str:
    base = SEVERITY.get(element, "info")
    # value disappeared entirely -> escalate one level
    vanished = (old not in (None, "", [], 0, False)) and (new in (None, "", [], 0))
    if element == "noindex" and new is True:
        return "critical"
    if element == "status" and old == 200 and new != 200:
        return "critical"
    if vanished and base == "warning":
        return "critical"
    if element == "internal_links":
        try:
            if old and new is not None and new < old * 0.5:
                return "warning"   # lost half its internal links
        except TypeError:
            pass
        return "info"
    return base


def diff_and_record(db: DB, site: str, crawl_id: int,
                    last_audit_started: str | None) -> list[dict]:
    """Compare this crawl's fingerprints against the previous crawl.

    Returns the list of changes recorded (also persisted with deploy
    correlation).
    """
    prev_id = db.previous_crawl_id(site, crawl_id)
    if prev_id is None:
        return []
    old_fps = db.fingerprints_for(prev_id)
    new_fps = db.fingerprints_for(crawl_id)

    deploy = None
    if last_audit_started:
        deploy = db.latest_deploy_since(site, last_audit_started)
    deploy_id = deploy[0] if deploy else None

    changes: list[dict] = []

    for url, new in new_fps.items():
        old = old_fps.get(url)
        if old is None:
            changes.append({"url": url, "element": "page", "old": None,
                            "new": "new page discovered", "severity": "info"})
            continue
        for key in new:
            if old.get(key) != new.get(key):
                changes.append({"url": url, "element": key,
                                "old": old.get(key), "new": new.get(key),
                                "severity": _severity(key, old.get(key), new.get(key))})

    for url in old_fps:
        if url not in new_fps:
            changes.append({"url": url, "element": "page",
                            "old": "existed", "new": "gone from crawl",
                            "severity": "warning"})

    for c in changes:
        db.record_change(site, c["url"], c["element"],
                         c["old"], c["new"], c["severity"], deploy_id)
        if deploy:
            c["deploy"] = {"ref": deploy[2], "source": deploy[3], "at": deploy[1]}

    return changes


def summarize_changes(changes: list[dict]) -> dict:
    crit = [c for c in changes if c["severity"] == "critical"]
    warn = [c for c in changes if c["severity"] == "warning"]
    deploy = next((c.get("deploy") for c in changes if c.get("deploy")), None)
    headline = ""
    if crit:
        c = crit[0]
        headline = f"{len(crit)} critical change(s) — e.g. '{c['element']}' on {c['url']}"
        if deploy:
            headline += f" · correlates with deploy {deploy['ref']} ({deploy['source']}) at {deploy['at']}"
    return {"critical": len(crit), "warnings": len(warn),
            "total": len(changes), "headline": headline}
