"""Athena scoring engine — Sprint 1: Pure Rule-Based Bucket Classifier + Scorer.

DESIGN CONSTRAINT (founder directive, 2026-07-15):
    Pure rules only. No embeddings, no semantic similarity, no LLM classification.

Pipeline position:
    ingestion/dedup  ->  [attach_scoring]  ->  rendering
"""
import json
import re
import sqlite3
from typing import Optional

from oracle.config import DB_PATH

# ── Score weights ──────────────────────────────────────────────────────────
WEIGHTS = {
    "shipping":      0.20,
    "utility":       0.20,
    "replication":   0.25,
    "enthusiast":    0.20,
    "novelty":       0.15,
}
HYPE_CAP = 0.45

# ── Keyword sets ───────────────────────────────────────────────────────────
KW_SHIPPING = [
    "released", "launch", "v1.0", "v2.0", "v3.0", "shipping", "now available",
    "open source", "open-source", "open weights", "weights released", "live now",
    "beta", "public beta", "ga release", "general availability", "ships", "deployed",
    "production", "now in", "available today", "download", "gradio", "demo", "playground",
]
KW_LOCAL_AI = [
    "local llm", "local model", "local ai", "run locally", "run it locally", "on-device",
    "on device", "ollama", "llama.cpp", "llamacpp", "gguf", "ggml", "lm studio",
    "consumer hardware", "consumer gpu", "rtx", "your own gpu", "offline", "private ai",
    "local-only", "self-host", "self-hosted", "home server", "edge device", "edge inference",
    "quantized", "quantization", "q4", "q8", "int4", "fp16", "fine-tune at home",
    "train at home", "local inference", "local deployment", "no api", "no cloud",
]
KW_PROBLEM_SOLVED = [
    "how to", "how i", "solved", "fix", "fixed", "workaround", "benchmark", "improves",
    "improved", "speedup", "speed-up", "reduces", "reduce", "cut", "cuts", "boost",
    "optimize", "optimized", "optimisation", "faster", "3x", "10x", "2x", "latency",
    "throughput", "roi", "cost", "cheaper", "save", "saves", "eliminate", "eliminated",
    "from 117s to 30s", "p95", "memory usage", "vram", "token cost", "bottleneck",
    "case study", "results", "we measured", "we tested", "showdown", "comparison",
]
KW_MODEL_RELEASE = [
    "releases", "released", "unveils", "introduces", "new model", "new flagship",
    "gpt-", "claude", "gemini", "llama", "mistral", "qwen", "deepseek", "grok",
    "phi-", "command-r", "api access", "weights", "open model", "open-models",
    "frontier", "checkpoint", "fine-tune", "finetune", "rl-trained", "rl train",
    "trained", "post-training", "post training", "distilled", "distillation",
]
KW_RESEARCH = [
    "paper", "arxiv", "preprint", "study", "research", "we propose", "we present",
    "we introduce", "we show", "method", "framework", "theorem", "analysis of",
    "survey", "benchmark", "dataset", "neural", "transformer", "diffusion",
    "gradient", "ablation", "we find", "our approach", "novel", "state-of-the-art",
    "sota", "cs.lg", "cs.cl", "cs.cv", "cs.ai",
]
KW_BUSINESS = [
    "raises", "raised", "$", "valuation", "series a", "series b", "funding", "round",
    "ipo", "acquisition", "acquires", "merger", "deal", "revenue", "layoff", "hiring",
    "partnership", "invests", "investment", "market", "vc", "compute deal",
    "billion", "million", "forecast", "miss", "earnings", "stock",
]
KW_INFRA = [
    "gpu", "tpu", "data center", "datacenter", "data centre", "cluster", "cuda",
    "rocm", "vllm", "tensorrt", "inference server", "serving", "kubernetes", "docker",
    "pipeline", "mlops", "ci/cd", "rag", "vector db", "vector database", "agent",
    "agents", "orchestration", "observability", "evaluation", "eval", "guardrail",
    "safety", "red team", "jailbreak", "prompt injection", "fine-tuning stack",
]
KW_CULTURE = [
    "says", "argues", "opinion", "essay", "think", "thinks", "the real", "why we",
    "the future of", "dystopia", "utopia", "philosophy", "ethics", "regulation",
    "policy", "ban", "lawsuit", "eu", "senate", "congress", "interview", "podcast",
    "controversy", "controversial", "debate", "critic", "criticism",
    "creepy", "creeping", "not sexy", "vibe", "hot take", "unpopular",
]

BUCKETS = {
    "SHIPPING":         {"kw": KW_SHIPPING,      "source_whitelist": None, "order": 0},
    "LOCAL AI":         {"kw": KW_LOCAL_AI,       "source_whitelist": None, "order": 1},
    "PROBLEM SOLVED":   {"kw": KW_PROBLEM_SOLVED, "source_whitelist": None, "order": 2},
    "MODEL RELEASE":    {"kw": KW_MODEL_RELEASE,  "source_whitelist": None, "order": 3},
    "RESEARCH":         {"kw": KW_RESEARCH,       "source_whitelist": ["arxiv"], "order": 4},
    "BUSINESS":         {"kw": KW_BUSINESS,       "source_whitelist": None, "order": 5},
    "INFRASTRUCTURE":   {"kw": KW_INFRA,          "source_whitelist": None, "order": 6},
    "CULTURE":          {"kw": KW_CULTURE,        "source_whitelist": None, "order": 7},
}
BUCKET_ORDER = sorted(BUCKETS.keys(), key=lambda b: BUCKETS[b]["order"])

HYPE_TERMS = [
    "revolutionary", "game-changing", "game changer", "breakthrough", "mind-blowing",
    "insane", "crazy", "unbelievable", "shocking", "the future is here", "omg",
    "you won't believe", "secret", "they don't want you to know", "leaked", "viral",
    "hype", "buzzword", "disrupt", "disrupting everything", "ai will replace",
    "will change everything", "paradigm shift", "godlike", "magic", "miracle",
]

ENTHUSIAST_SIGNALS = [
    "github", "repo", "repository", "self-host", "local", "ollama", "llamacpp",
    "hugging face", "huggingface", "colab", "notebook", "pip install", "docker",
    "cli", "open source", "open-source", "diy", "build your own", "tutorial",
    "how to", "implementation", "agent", "agents", "fine-tune", "finetune",
    "quantiz", "vllm", "rtx", "gpu", "consumer", "homelab", "self-hosted",
    "machine-learning", "machine learning", "deep learning", "python", "rust",
    "benchmark", "reproduc", "weights", "gguf",
]
SOURCE_ENTHUSIAST_BONUS = {
    "github": 0.20, "huggingface": 0.20, "arxiv": 0.10,
    "hackernews": 0.10, "reddit": 0.05, "rss": 0.0,
}

NEW_COLUMNS = [
    "bucket TEXT",
    "shipping_score REAL DEFAULT 0",
    "utility_score REAL DEFAULT 0",
    "replication_score REAL DEFAULT 0",
    "enthusiast_score REAL DEFAULT 0",
    "novelty_score REAL DEFAULT 0",
    "hype_penalty REAL DEFAULT 0",
    "final_score REAL DEFAULT 0",
    "actionability_score REAL DEFAULT 0",
    "narrative_id TEXT",
    "topic_id TEXT",
    "relation_json TEXT",
]


# ── Helpers ────────────────────────────────────────────────────────────────
def _norm(text):
    if not text:
        return ""
    if isinstance(text, bytes):
        text = text.decode("utf-8", "replace")
    return " " + re.sub(r"\s+", " ", text.lower()) + " "


def _summary_text(raw):
    if not raw:
        return ""
    try:
        d = json.loads(raw)
        if isinstance(d, dict):
            return " ".join(str(v) for v in d.values() if isinstance(v, str))
    except Exception:
        pass
    return raw


# ── Classification ─────────────────────────────────────────────────────────
def classify(entry: dict) -> tuple[str, list[str]]:
    """Pure rule classification.

    Returns (bucket, matched_list) where matched_list is human-readable proof.
    """
    title = _norm(entry.get("title") or "")
    summary = _norm(_summary_text(entry.get("summary")))
    tags_raw = entry.get("category_tags") or ""
    try:
        tags = " ".join(json.loads(tags_raw)) if tags_raw else ""
    except Exception:
        tags = tags_raw
    tags = _norm(tags)
    source = (entry.get("source") or "").lower()
    haystack = title + " " + summary + " " + tags

    matched = [f"source={source}"]
    best_bucket = "UNCATEGORIZED"
    best_hits = 0

    for bucket in BUCKET_ORDER:
        spec = BUCKETS[bucket]
        whitelist = spec["source_whitelist"]
        if whitelist and source not in whitelist:
            continue
        hits = []
        for kw in spec["kw"]:
            if f" {kw.lower()} " in haystack:
                hits.append(kw)
        if hits:
            matched.extend(f"kw:{h}" for h in hits[:8])
            if len(hits) > best_hits:
                best_hits = len(hits)
                best_bucket = bucket

    if best_bucket == "UNCATEGORIZED":
        matched.append("(no rule fired)")

    return best_bucket, matched


# ── Scoring ────────────────────────────────────────────────────────────────
def score_entry(bucket: str, matched: list, entry: dict) -> dict:
    """Return dict of component scores (0..1) + final (0..1)."""
    source = (entry.get("source") or "").lower()
    haystack = _norm(entry.get("title") or "") + " " + _norm(_summary_text(entry.get("summary")))

    tags_raw = entry.get("category_tags") or ""
    try:
        tags = " ".join(json.loads(tags_raw)) if tags_raw else ""
    except Exception:
        tags = tags_raw
    haystack += _norm(tags)

    # Enthusiast score
    ent_hits = sum(1 for s in ENTHUSIAST_SIGNALS if f" {s} " in haystack)
    enthusiast = min(ent_hits / 5.0 + SOURCE_ENTHUSIAST_BONUS.get(source, 0.0), 1.0)

    # Shipping score
    ship_kw = [k for k in KW_SHIPPING if f" {k} " in haystack]
    shipping = 0.0
    if bucket == "SHIPPING":
        shipping = 0.9
    elif ship_kw:
        shipping = min(0.4 + 0.1 * len(ship_kw), 0.8)
    if source in ("github", "huggingface"):
        shipping = max(shipping, 0.7)

    # Utility score
    util_kw = [k for k in KW_PROBLEM_SOLVED if f" {k} " in haystack]
    utility = 0.0
    if bucket == "PROBLEM SOLVED":
        utility = 0.85
    elif util_kw:
        utility = min(0.4 + 0.1 * len(util_kw), 0.8)
    if any(s in haystack for s in [" github ", " huggingface ", " demo "]):
        utility = max(utility, 0.6)

    # Replication score
    repl_kw = [k for k in KW_LOCAL_AI if f" {k} " in haystack]
    replication = 0.0
    if bucket == "LOCAL AI":
        replication = 1.0
    elif repl_kw:
        replication = min(0.5 + 0.1 * len(repl_kw), 0.9)
    if source in ("github", "huggingface"):
        replication = max(replication, 0.7)
    if any(s in haystack for s in [" open source ", " open-source ", " weights "]):
        replication = max(replication, 0.6)

    # Novelty score
    novelty = 0.0
    if bucket in ("RESEARCH", "MODEL RELEASE"):
        novelty = 0.6
    nov_kw = ["new", "novel", "first", "breakthrough-method", "we propose",
              "we introduce", "we present", "state-of-the-art", "sota", "unveils"]
    if any(f" {k} " in haystack for k in nov_kw):
        novelty = min(novelty + 0.2, 0.9)
    if bucket == "CULTURE":
        novelty = min(novelty, 0.3)

    # Hype penalty
    hype_penalty = min(0.1 * sum(1 for t in HYPE_TERMS if f" {t} " in haystack), 0.6)

    # Final score
    raw = (
        WEIGHTS["shipping"] * shipping
        + WEIGHTS["utility"] * utility
        + WEIGHTS["replication"] * replication
        + WEIGHTS["enthusiast"] * enthusiast
        + WEIGHTS["novelty"] * novelty
    )
    final = min(max(raw - hype_penalty, 0.0), 1.0)

    return {
        "shipping_score": round(shipping, 3),
        "utility_score": round(utility, 3),
        "replication_score": round(replication, 3),
        "enthusiast_score": round(enthusiast, 3),
        "novelty_score": round(novelty, 3),
        "hype_penalty": round(hype_penalty, 3),
        "final_score": round(final, 4),
    }


# ── DB Operations ──────────────────────────────────────────────────────────
def migrate(db_path: Optional[str] = None) -> list[str]:
    """Idempotent schema migration — only adds missing columns."""
    conn = sqlite3.connect(db_path or str(DB_PATH))
    cur = conn.cursor()
    cur.execute("PRAGMA table_info(entries)")
    existing = {row[1] for row in cur.fetchall()}
    added = []
    for col in NEW_COLUMNS:
        name = col.split(" ")[0]
        if name not in existing:
            cur.execute(f"ALTER TABLE entries ADD COLUMN {col}")
            added.append(name)
    conn.commit()
    conn.close()
    print(f"[migrate] added columns: {', '.join(added) if added else 'none (already present)'}")
    return added


def fetch_unscored(conn: sqlite3.Connection) -> list[dict]:
    cur = conn.cursor()
    cur.execute("""
        SELECT id, source, source_id, url, title, summary, category_tags, raw_metadata
        FROM entries WHERE bucket IS NULL OR bucket = ''
    """)
    cols = ["id", "source", "source_id", "url", "title", "summary", "category_tags", "raw_metadata"]
    return [dict(zip(cols, row)) for row in cur.fetchall()]


def attach_scoring(db_path: Optional[str] = None, dry_run: bool = False) -> None:
    """Score every unscored entry. Call after ingestion/dedup, before render."""
    conn = sqlite3.connect(db_path or str(DB_PATH))
    rows = fetch_unscored(conn)
    print(f"[attach] scoring {len(rows)} unscored entries")
    for e in rows:
        bucket, matched = classify(e)
        scores = score_entry(bucket, matched, e)
        if not dry_run:
            conn.execute(
                """UPDATE entries SET bucket=?, shipping_score=?, utility_score=?,
                   replication_score=?, enthusiast_score=?, novelty_score=?,
                   hype_penalty=?, final_score=?, actionability_score=?,
                   narrative_id=?, topic_id=?, relation_json=? WHERE id=?""",
                (bucket, scores["shipping_score"], scores["utility_score"],
                 scores["replication_score"], scores["enthusiast_score"],
                 scores["novelty_score"], scores["hype_penalty"], scores["final_score"],
                 0.0, None, None, json.dumps({"matched_rules": matched}), e["id"]),
            )
    if not dry_run:
        conn.commit()
    conn.close()
    print("[attach] done.")
