from __future__ import annotations

import re
from collections import Counter
from typing import Optional
from urllib.parse import urlparse

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

router = APIRouter(prefix="/dataset")


# ── License risk scores ────────────────────────────────────────────────────────
# Score 0.0 = no risk, 1.0 = maximum risk

LICENSE_RISK: dict[str, float] = {
    # ── Fully open / public domain ────────────────────────────────────────────────
    "cc0":                   0.0,
    "unlicense":             0.0,
    "wtfpl":                 0.0,
    "public-domain":         0.0,
    "public domain":         0.0,
    "pddl":                  0.0,
    "mit-0":                 0.0,
    # ── Permissive open source ────────────────────────────────────────────────────
    "mit":                   0.05,
    "apache-2.0":            0.05,
    "apache-1.1":            0.07,
    "bsd-2-clause":          0.05,
    "bsd-3-clause":          0.05,
    "bsd-4-clause":          0.08,
    "isc":                   0.05,
    "artistic-2.0":          0.07,
    "zlib":                  0.05,
    "boost-1.0":             0.05,
    "phi-3":                 0.05,
    "phi-4":                 0.05,
    "grok":                  0.05,
    "mistral":               0.05,
    "deepseek-r1":           0.05,
    "yi-1.5":                0.05,
    "falcon3":               0.05,
    "gemma4":                0.05,
    "qwen3":                 0.05,
    # ── Attribution-required ──────────────────────────────────────────────────────
    "cc-by":                 0.10,
    "cc-by-2.0":             0.10,
    "cc-by-3.0":             0.10,
    "cc-by-4.0":             0.10,
    "odc-by":                0.10,
    "cdla-permissive-1.0":   0.10,
    "cdla-permissive-2.0":   0.10,
    # ── Weak copyleft / share-alike ───────────────────────────────────────────────
    "cc-by-sa":              0.20,
    "cc-by-sa-3.0":          0.20,
    "cc-by-sa-4.0":          0.20,
    "mpl-2.0":               0.22,
    "cddl-1.0":              0.22,
    "epl-1.0":               0.22,
    "epl-2.0":               0.22,
    "eupl-1.1":              0.22,
    "eupl-1.2":              0.22,
    "lgpl-2.0":              0.22,
    "lgpl-2.1":              0.25,
    "lgpl-3.0":              0.25,
    "odc-odbl":              0.25,
    "cdla-sharing-1.0":      0.25,
    # ── Strong copyleft ───────────────────────────────────────────────────────────
    "gpl-2.0":               0.30,
    "gpl-2.0-only":          0.30,
    "gpl-2.0-or-later":      0.30,
    "gpl-3.0":               0.30,
    "gpl-3.0-only":          0.30,
    "gpl-3.0-or-later":      0.30,
    "agpl-3.0":              0.35,
    "agpl-3.0-only":         0.35,
    # ── AI-safety / OpenRAIL (use-case restrictions) ──────────────────────────────
    "openrail":              0.20,
    "openrail++":            0.20,
    "openrail-m":            0.20,
    "openrail-d":            0.20,
    "openrail-c":            0.20,
    "bigscience-openrail-m": 0.22,
    "bigcode-openrail-m":    0.22,
    "creativeml-openrail-m": 0.22,
    "bloom-rail-1.0":        0.28,
    "starcoder":             0.22,
    "stable-diffusion":      0.22,
    # ── AI model custom licenses ──────────────────────────────────────────────────
    "gemma":                 0.30,
    "gemma2":                0.30,
    "gemma3":                0.30,
    "llama3":                0.42,
    "llama4":                0.42,
    "llama2":                0.50,
    "qwen":                  0.38,
    "deepseek":              0.40,
    "falcon":                0.35,
    "yi":                    0.55,
    "cohere-command":        0.55,
    "mistral-research":      0.55,
    # ── Non-commercial restriction ────────────────────────────────────────────────
    "cc-by-nd":              0.50,
    "cc-by-nd-4.0":          0.50,
    "cc-by-nc":              0.55,
    "cc-by-nc-2.0":          0.55,
    "cc-by-nc-3.0":          0.55,
    "cc-by-nc-4.0":          0.55,
    "cc-by-nc-sa":           0.60,
    "cc-by-nc-sa-3.0":       0.60,
    "cc-by-nc-sa-4.0":       0.60,
    "cc-by-nc-nd":           0.70,
    "cc-by-nc-nd-4.0":       0.70,
    # ── Proprietary / unknown ─────────────────────────────────────────────────────
    "proprietary":           0.90,
    "other":                 0.60,
    "unknown":               0.65,
}

LICENSE_ALIASES: dict[str, str] = {
    # Creative Commons
    "creative commons zero": "cc0",
    "cc 0": "cc0",
    "cc zero": "cc0",
    "public domain": "public-domain",
    "cc by": "cc-by",
    "cc by 4.0": "cc-by-4.0",
    "cc by 3.0": "cc-by-3.0",
    "cc by-sa": "cc-by-sa",
    "cc by sa": "cc-by-sa",
    "cc by-sa 4.0": "cc-by-sa-4.0",
    "cc by-nc": "cc-by-nc",
    "cc by nc": "cc-by-nc",
    "cc by-nd": "cc-by-nd",
    "cc by nd": "cc-by-nd",
    "cc by-nc-sa": "cc-by-nc-sa",
    "cc by nc sa": "cc-by-nc-sa",
    "cc by-nc-nd": "cc-by-nc-nd",
    "cc by nc nd": "cc-by-nc-nd",
    # Apache
    "apache": "apache-2.0",
    "apache2": "apache-2.0",
    "apache 2": "apache-2.0",
    "apache license 2.0": "apache-2.0",
    "apache software license": "apache-2.0",
    # GPL
    "gpl": "gpl-3.0",
    "gpl2": "gpl-2.0",
    "gpl3": "gpl-3.0",
    "gnu gpl": "gpl-3.0",
    "gnu gpl v3": "gpl-3.0",
    "gnu gpl v2": "gpl-2.0",
    "gnu general public license": "gpl-3.0",
    "lgpl": "lgpl-2.1",
    "lgpl2": "lgpl-2.0",
    "lgpl3": "lgpl-3.0",
    "agpl": "agpl-3.0",
    "gnu agpl": "agpl-3.0",
    # BSD
    "bsd": "bsd-3-clause",
    "bsd2": "bsd-2-clause",
    "bsd3": "bsd-3-clause",
    "new bsd": "bsd-3-clause",
    "simplified bsd": "bsd-2-clause",
    # MIT
    "mit license": "mit",
    "the mit license": "mit",
    # OpenRAIL
    "openrail-m": "openrail-m",
    "bigscience-openrail": "bigscience-openrail-m",
    "creativeml openrail": "creativeml-openrail-m",
    "creativeml-openrail": "creativeml-openrail-m",
    # LLaMA
    "llama 2": "llama2",
    "llama 3": "llama3",
    "llama 4": "llama4",
    "llama-2": "llama2",
    "llama-3": "llama3",
    "llama-4": "llama4",
    "meta llama 2": "llama2",
    "meta llama 3": "llama3",
    # Gemma
    "google gemma": "gemma",
    "gemma terms": "gemma",
    "gemma terms of use": "gemma",
    # Other AI
    "mistral research license": "mistral-research",
    "mrl": "mistral-research",
    "tongyi qianwen": "qwen",
    "deepseek license": "deepseek",
    "tii falcon": "falcon",
    "bloom rail": "bloom-rail-1.0",
}

LICENSE_RISK_LABEL: dict[str, str] = {
    # Public domain
    "cc0":                   "Public domain — no restrictions",
    "unlicense":             "Public domain — no restrictions",
    "wtfpl":                 "Public domain — no restrictions",
    "public-domain":         "Public domain — no restrictions",
    "mit-0":                 "Public domain equivalent (MIT-0)",
    "pddl":                  "Public Domain Dedication and License",
    # Permissive
    "mit":                   "Permissive — attribution only",
    "apache-2.0":            "Permissive — attribution + patent grant",
    "apache-1.1":            "Permissive — attribution only",
    "bsd-2-clause":          "Permissive — attribution only",
    "bsd-3-clause":          "Permissive — attribution + no endorsement",
    "bsd-4-clause":          "Permissive — attribution + advertising clause",
    "isc":                   "Permissive — attribution only",
    "artistic-2.0":          "Permissive — modified distribution conditions",
    "zlib":                  "Permissive — attribution in source",
    "boost-1.0":             "Permissive — attribution only",
    "phi-3":                 "Permissive MIT — commercial allowed",
    "phi-4":                 "Permissive MIT — commercial allowed",
    "grok":                  "Permissive Apache 2.0",
    "mistral":               "Permissive Apache 2.0",
    "deepseek-r1":           "Permissive MIT",
    "yi-1.5":                "Permissive Apache 2.0",
    "falcon3":               "Permissive Apache 2.0",
    "gemma4":                "Permissive Apache 2.0",
    "qwen3":                 "Permissive Apache 2.0",
    # Attribution
    "cc-by":                 "Attribution required",
    "cc-by-2.0":             "Attribution required (CC BY 2.0)",
    "cc-by-3.0":             "Attribution required (CC BY 3.0)",
    "cc-by-4.0":             "Attribution required (CC BY 4.0)",
    "odc-by":                "Open Data Attribution required",
    "cdla-permissive-1.0":   "Community Data License — attribution only",
    "cdla-permissive-2.0":   "Community Data License — attribution only v2",
    # Share-alike
    "cc-by-sa":              "Attribution + share-alike required",
    "cc-by-sa-3.0":          "Attribution + share-alike (CC BY-SA 3.0)",
    "cc-by-sa-4.0":          "Attribution + share-alike (CC BY-SA 4.0)",
    "mpl-2.0":               "File-level copyleft (MPL 2.0)",
    "lgpl-2.1":              "Weak copyleft — library linking exception",
    "lgpl-3.0":              "Weak copyleft — library linking exception v3",
    "odc-odbl":              "Open Database share-alike required",
    "cdla-sharing-1.0":      "Community Data License — share-alike",
    # Copyleft
    "gpl-2.0":               "Copyleft — derivatives must be GPL v2",
    "gpl-3.0":               "Copyleft — derivatives must be GPL v3",
    "agpl-3.0":              "Strong copyleft — network use triggers share-alike",
    # OpenRAIL
    "openrail":              "OpenRAIL — use-case restrictions apply",
    "openrail++":            "OpenRAIL++ — use-case restrictions apply",
    "openrail-m":            "OpenRAIL-M (model) — use-case restrictions",
    "openrail-d":            "OpenRAIL-D (data) — use-case restrictions",
    "bigscience-openrail-m": "BigScience OpenRAIL-M — use-case restrictions",
    "bigcode-openrail-m":    "BigCode OpenRAIL-M — code use-case restrictions",
    "creativeml-openrail-m": "CreativeML OpenRAIL-M — creative use restrictions",
    "bloom-rail-1.0":        "BLOOM RAIL 1.0 — most restrictive OpenRAIL variant",
    "starcoder":             "BigCode OpenRAIL-M — code generation restrictions",
    "stable-diffusion":      "CreativeML OpenRAIL-M — image generation restrictions",
    # AI Custom
    "llama2":                "LLaMA 2 Community License — commercial use limited",
    "llama3":                "LLaMA 3 Community License — commercial >700M MAU requires deal",
    "llama4":                "LLaMA 4 Community License — commercial >700M MAU requires deal",
    "gemma":                 "Gemma Terms of Use — prohibited uses clause",
    "gemma2":                "Gemma 2 Terms of Use — prohibited uses clause",
    "gemma3":                "Gemma 3 Terms of Use — prohibited uses clause",
    "qwen":                  "Tongyi Qianwen License — commercial permitted",
    "deepseek":              "DeepSeek License 2.0 — attribution required",
    "falcon":                "TII Falcon License — hosted API use requires consent",
    "yi":                    "Yi License — non-commercial by default",
    "cohere-command":        "CC BY-NC 4.0 — non-commercial only",
    "mistral-research":      "Mistral Research License — non-commercial",
    # NC
    "cc-by-nd":              "No derivatives allowed",
    "cc-by-nc":              "Non-commercial use only",
    "cc-by-nc-4.0":          "Non-commercial use only (CC BY-NC 4.0)",
    "cc-by-nc-sa":           "Non-commercial + share-alike",
    "cc-by-nc-sa-4.0":       "Non-commercial + share-alike (CC BY-NC-SA 4.0)",
    "cc-by-nc-nd":           "Non-commercial, no derivatives",
    "cc-by-nc-nd-4.0":       "Non-commercial, no derivatives (CC BY-NC-ND 4.0)",
    # Other
    "proprietary":           "Proprietary — all rights reserved",
    "other":                 "Unknown license terms",
    "unknown":               "Unrecognised license — assume restricted",
}


def _license_risk_score(raw: str | None) -> tuple[float, str, str]:
    """Returns (score 0-1, resolved_key, label)."""
    if not raw:
        return 0.75, "unknown", "No license declared — assume restricted"
    key = raw.lower().strip().replace("_", "-")
    if key in LICENSE_RISK:
        return LICENSE_RISK[key], key, LICENSE_RISK_LABEL.get(key, key)
    if key in LICENSE_ALIASES:
        k = LICENSE_ALIASES[key]
        return LICENSE_RISK[k], k, LICENSE_RISK_LABEL.get(k, k)
    best = max(
        ((k, v) for k, v in LICENSE_RISK.items() if k in key),
        key=lambda x: len(x[0]),
        default=None,
    )
    if best:
        k = best[0]
        return best[1], k, LICENSE_RISK_LABEL.get(k, k)
    return 0.65, "unknown", "Unrecognised license — assume restricted"


# ── Domain risk heuristics ─────────────────────────────────────────────────────

HIGH_RISK_DOMAINS = {
    # Major news publishers (paywalled / strict copyright)
    "nytimes.com", "wsj.com", "bloomberg.com", "ft.com", "reuters.com",
    "apnews.com", "economist.com", "washingtonpost.com", "theguardian.com",
    "telegraph.co.uk", "bbc.co.uk", "bbc.com", "forbes.com", "cnbc.com",
    "businessinsider.com", "huffpost.com", "time.com", "newsweek.com",
    "thedailybeast.com", "theatlantic.com", "newyorker.com", "vanityfair.com",
    "wired.com", "slate.com", "salon.com", "motherjones.com",
    "politico.com", "thehill.com", "axios.com", "vox.com",
    "buzzfeed.com", "buzzfeednews.com", "dailymail.co.uk", "thesun.co.uk",
    "mirror.co.uk", "express.co.uk", "independent.co.uk", "standard.co.uk",
    "lemonde.fr", "lefigaro.fr", "liberation.fr",
    "spiegel.de", "faz.net", "sueddeutsche.de", "zeit.de",
    "elpais.com", "elmundo.es",
    "corriere.it", "repubblica.it",
    "afp.com", "dpa.com",
    # Academic / scientific publishers (strict copyright)
    "springer.com", "springerlink.com", "nature.com", "sciencedirect.com",
    "wiley.com", "onlinelibrary.wiley.com", "jstor.org", "elsevier.com",
    "ieee.org", "acm.org", "tandfonline.com", "sagepub.com",
    "oxfordacademic.com", "academic.oup.com", "cambridge.org",
    "karger.com", "thieme.com", "wolterskluwer.com",
    "clinicalkey.com", "embase.com", "cochranelibrary.com",
    # Books / media
    "books.google.com", "play.google.com", "amazon.com", "scribd.com",
    "audible.com", "kindle.amazon.com",
    # Stock media
    "gettyimages.com", "shutterstock.com", "istockphoto.com",
    "depositphotos.com", "alamy.com", "corbis.com",
    # Preprint servers with restricted downstream use
    "arxiv.org", "biorxiv.org", "medrxiv.org", "ssrn.com",
    # Music / lyrics
    "genius.com", "azlyrics.com", "musixmatch.com", "lyrics.com",
    "spotify.com", "apple.com", "music.apple.com",
}

LOW_RISK_DOMAINS = {
    # Open encyclopedias
    "wikipedia.org", "wikimedia.org", "wikidata.org", "wikisource.org",
    "wikibooks.org", "wikiquote.org", "wikinews.org",
    # Open archives
    "archive.org", "gutenberg.org", "standardebooks.org",
    # Open data crawls
    "commoncrawl.org", "c4.commoncrawl.org",
    # Government / EU open data
    "data.gov", "data.europa.eu", "data.gov.uk", "open.canada.ca",
    "data.un.org", "census.gov", "bls.gov",
    "ec.europa.eu", "eur-lex.europa.eu",
    # ML / AI community
    "huggingface.co", "datasets.huggingface.co",
    # Code hosting
    "github.com", "githubusercontent.com", "gitlab.com", "bitbucket.org",
    # Open scientific publishing
    "openreview.net", "plos.org", "plosone.org",
    "pubmed.ncbi.nlm.nih.gov", "ncbi.nlm.nih.gov", "nih.gov",
    "europepmc.org", "pmc.ncbi.nlm.nih.gov",
    "doaj.org", "unpaywall.org",
    # Open legal
    "law.cornell.edu", "courtlistener.com", "case.law",
    "legislation.gov.uk", "legifrance.gouv.fr",
    # Stack Exchange network
    "stackoverflow.com", "stackexchange.com", "superuser.com",
    "serverfault.com", "askubuntu.com", "mathoverflow.net",
    # Open discussion (CC-BY-SA)
    "reddit.com",
    # Open AI / research labs
    "openai.com", "anthropic.com", "deepmind.com",
    "ai.google", "research.google",
    # Open datasets registries
    "kaggle.com", "zenodo.org", "figshare.com", "osf.io",
    "dataverse.harvard.edu",
}

MEDIUM_RISK_TLD = {".com", ".io", ".co"}
LOW_RISK_TLD    = {".gov", ".edu", ".org", ".ac.uk", ".ac.jp"}


def _domain_risk_score(url: str) -> tuple[float, str]:
    """Returns (score 0-1, reason)."""
    try:
        parsed = urlparse(url if url.startswith("http") else "https://" + url)
        host = (parsed.hostname or "").lower().lstrip("www.")
    except Exception:
        return 0.4, "Unparseable URL"

    for d in HIGH_RISK_DOMAINS:
        if host == d or host.endswith("." + d):
            return 0.85, f"High-risk publisher domain ({d})"

    for d in LOW_RISK_DOMAINS:
        if host == d or host.endswith("." + d):
            return 0.05, f"Low-risk open source ({d})"

    for tld in LOW_RISK_TLD:
        if host.endswith(tld):
            return 0.15, f"Low-risk TLD ({tld})"

    for tld in MEDIUM_RISK_TLD:
        if host.endswith(tld):
            return 0.40, f"Commercial TLD ({tld}) — unknown terms"

    return 0.30, "Unknown domain"


# ── Content heuristics ─────────────────────────────────────────────────────────

# Phrases that strongly suggest copyrighted / proprietary material
COPYRIGHT_RE = re.compile(
    r"©|\bcopyright\b|\ball rights reserved\b|\bproprietay\b"
    r"|\btrademark\b|\b™\b|\b®\b"
    r"|\bdo not reproduce\b|\breprint\b|\bforbidden to copy\b"
    r"|\bunder license\b|\blicensed to\b",
    re.IGNORECASE,
)

# Phrases that suggest explicitly licensed / open content
OPEN_CONTENT_RE = re.compile(
    r"\bcreative commons\b|\bcc\s*by\b|\bcc0\b|\bpublic domain\b"
    r"|\bopen access\b|\bopen license\b|\bfreely available\b"
    r"|\bmit license\b|\bapache license\b",
    re.IGNORECASE,
)

# Phrases suggesting scraped/syndicated news
NEWS_RE = re.compile(
    r"\breuters\b|\bassociated press\b|\bap news\b|\bbloomberg\b"
    r"|\bstaff reporter\b|\bour correspondent\b|\bnews desk\b",
    re.IGNORECASE,
)

# Book/publication patterns
BOOK_RE = re.compile(
    r"\bisbn\b|\bpublished by\b|\bfirst edition\b|\ball rights reserved\b"
    r"|\bprinted in\b|\bno part of this\b",
    re.IGNORECASE,
)


def _content_risk_score(texts: list[str], max_sample: int = 200) -> dict:
    sample = texts[:max_sample]
    n = max(len(sample), 1)

    copyright_hits = 0
    open_hits = 0
    news_hits = 0
    book_hits = 0

    for t in sample:
        if COPYRIGHT_RE.search(t):
            copyright_hits += 1
        if OPEN_CONTENT_RE.search(t):
            open_hits += 1
        if NEWS_RE.search(t):
            news_hits += 1
        if BOOK_RE.search(t):
            book_hits += 1

    copyright_pct  = round(copyright_hits / n * 100, 1)
    open_pct       = round(open_hits / n * 100, 1)
    news_pct       = round(news_hits / n * 100, 1)
    book_pct       = round(book_hits / n * 100, 1)

    # Score: copyright markers raise it, open markers lower it
    raw = (copyright_hits * 0.9 + news_hits * 0.5 + book_hits * 0.6) / n
    raw = min(raw - open_hits * 0.3 / n, 1.0)
    score = max(raw, 0.0)

    signals = []
    if copyright_hits:
        signals.append({"signal": "Copyright notices", "pct": copyright_pct, "direction": "risk"})
    if news_hits:
        signals.append({"signal": "News wire phrases", "pct": news_pct, "direction": "risk"})
    if book_hits:
        signals.append({"signal": "Book / publication markers", "pct": book_pct, "direction": "risk"})
    if open_hits:
        signals.append({"signal": "Open license mentions", "pct": open_pct, "direction": "safe"})

    return {
        "score": round(score, 4),
        "rows_analyzed": len(sample),
        "copyright_pct": copyright_pct,
        "open_pct": open_pct,
        "news_pct": news_pct,
        "book_pct": book_pct,
        "signals": signals,
    }


# ── Overall risk label ─────────────────────────────────────────────────────────

def _overall_label(score: float) -> str:
    if score < 0.15:
        return "low"
    if score < 0.40:
        return "moderate"
    if score < 0.65:
        return "elevated"
    return "high"


# ── Request / Response ─────────────────────────────────────────────────────────

class CopyrightRiskRequest(BaseModel):
    rows: list[dict]
    text_columns: list[str]
    url_columns: list[str] = []
    hf_license: Optional[str] = None
    max_rows: int = 500


# ── Route ──────────────────────────────────────────────────────────────────────

@router.post("/copyright-risk")
async def copyright_risk(req: CopyrightRiskRequest):
    rows = req.rows[:req.max_rows]
    if not rows:
        raise HTTPException(status_code=400, detail="No rows provided")

    # ── 1. License risk ───────────────────────────────────────────────────────
    lic_score, lic_key, lic_label = _license_risk_score(req.hf_license)

    # Also scan inline license columns
    inline_lic_cols = [
        c for c in (rows[0].keys() if rows else [])
        if any(k in c.lower() for k in ("license", "licence", "rights", "copyright"))
    ]
    inline_scores: list[float] = []
    inline_dist: Counter = Counter()
    for row in rows:
        for col in inline_lic_cols:
            v = row.get(col)
            if v and isinstance(v, str):
                s, _, _ = _license_risk_score(v.strip())
                inline_scores.append(s)
                inline_dist[v.strip()] += 1

    if inline_scores:
        avg_inline = sum(inline_scores) / len(inline_scores)
        lic_score = max(lic_score, avg_inline * 0.6)

    # ── 2. Domain risk ────────────────────────────────────────────────────────
    domain_scores: list[float] = []
    domain_reasons: Counter = Counter()
    url_count = 0

    for row in rows:
        for col in req.url_columns:
            v = row.get(col)
            if v and isinstance(v, str) and v.startswith("http"):
                url_count += 1
                ds, reason = _domain_risk_score(v)
                domain_scores.append(ds)
                domain_reasons[reason] += 1
                break

    domain_score = sum(domain_scores) / len(domain_scores) if domain_scores else None
    top_domain_reasons = [
        {"reason": r, "count": c}
        for r, c in domain_reasons.most_common(5)
    ]

    # ── 3. Content heuristic ──────────────────────────────────────────────────
    texts = []
    for row in rows:
        combined = " ".join(
            str(row[c]) for c in req.text_columns if c in row and row[c]
        ).strip()
        if combined:
            texts.append(combined)

    content_result = _content_risk_score(texts) if texts else {
        "score": 0.0, "rows_analyzed": 0, "copyright_pct": 0.0,
        "open_pct": 0.0, "news_pct": 0.0, "book_pct": 0.0, "signals": [],
    }

    # ── 4. Composite score ────────────────────────────────────────────────────
    # Weights: license 45%, domain 30% (if available), content 25%
    if domain_score is not None:
        composite = lic_score * 0.45 + domain_score * 0.30 + content_result["score"] * 0.25
        weights_used = {"license": 0.45, "domain": 0.30, "content": 0.25}
    else:
        composite = lic_score * 0.60 + content_result["score"] * 0.40
        weights_used = {"license": 0.60, "domain": None, "content": 0.40}

    composite = round(min(max(composite, 0.0), 1.0), 4)
    overall = _overall_label(composite)

    return {
        "composite_score": composite,
        "overall_risk": overall,
        "license": {
            "score": round(lic_score, 4),
            "raw": req.hf_license,
            "resolved_key": lic_key,
            "label": lic_label,
            "inline_columns": inline_lic_cols,
            "inline_distribution": [
                {"raw": r, "count": c} for r, c in inline_dist.most_common(10)
            ],
        },
        "domain": {
            "score": round(domain_score, 4) if domain_score is not None else None,
            "urls_analyzed": url_count,
            "top_reasons": top_domain_reasons,
        },
        "content": content_result,
        "weights": weights_used,
    }
