"""Deterministic keyword scoring, engagement bonus, dedup."""

from __future__ import annotations

import math
import re
from functools import lru_cache
from urllib.parse import urlparse

from platoon.models import Item


@lru_cache(maxsize=256)
def _kw_pattern(keyword: str) -> re.Pattern:
    """Compile a word-boundary regex for a keyword."""
    escaped = re.escape(keyword)
    return re.compile(rf"\b{escaped}\b", re.IGNORECASE)


def _normalize_url(url: str) -> str:
    """Normalize URL for dedup: strip fragments, trailing slashes, www."""
    try:
        p = urlparse(url)
        host = p.netloc.lower().removeprefix("www.")
        path = p.path.rstrip("/")
        return f"{host}{path}"
    except Exception:
        return url.lower().strip()


def _engagement_score(engagement: dict) -> float:
    """Log-scale engagement to 0-1 range."""
    total = 0
    for key, val in engagement.items():
        if isinstance(val, (int, float)) and val > 0:
            total += math.log1p(val)
    # Normalize: log1p(1000) ~ 6.9, so /7 gives ~1.0 for popular items
    return min(total / 7.0, 1.0)


def score_item(item: Item, config: dict) -> tuple[float, str, list[str]]:
    """
    Score an item deterministically.
    Returns (score, category, derived_tags).
    """
    categories = config.get("categories", {})
    interests = config.get("interests", [])

    text = f"{item.title} {item.summary}".lower()

    best_score = 0.0
    best_category = "Uncategorised"
    matched_keywords = []

    for cat_name, cat_cfg in categories.items():
        keywords = [k.lower() for k in cat_cfg.get("keywords", [])]
        weight = cat_cfg.get("weight", 1.0)

        hits = [kw for kw in keywords if _kw_pattern(kw).search(text)]
        if not keywords:
            continue
        raw = len(hits) / len(keywords)
        score = raw * weight * 10

        if score > best_score:
            best_score = score
            best_category = cat_name
            matched_keywords = hits

    # Interest boost
    interest_text = " ".join(interests).lower()
    interest_words = set(interest_text.split())
    text_words = set(text.split())
    overlap = len(interest_words & text_words)
    best_score += overlap * 0.15

    # Engagement bonus (0-1 added to score)
    eng = _engagement_score(item.engagement)
    best_score += eng

    # Derive tags from source tags + category + top keywords
    derived_tags = list(item.tags)
    if best_category != "Uncategorised":
        derived_tags.append(best_category)
    derived_tags.extend(matched_keywords[:3])
    # Deduplicate while preserving order
    seen = set()
    unique_tags = []
    for t in derived_tags:
        tl = t.lower()
        if tl not in seen:
            seen.add(tl)
            unique_tags.append(t)

    return best_score, best_category, unique_tags


def score_and_sort(items: list[Item], config: dict) -> list[Item]:
    """Score all items, deduplicate, filter, and sort."""
    # Score each item
    for item in items:
        score, category, tags = score_item(item, config)
        item.score = score
        item.category = category
        item.tags = tags

    # Dedup by normalized URL — keep highest engagement
    seen: dict[str, Item] = {}
    for item in items:
        key = _normalize_url(item.url)
        if key in seen:
            existing = seen[key]
            # Merge: keep higher score, combine tags
            if item.score > existing.score:
                item.tags = list(set(item.tags + existing.tags))
                seen[key] = item
            else:
                existing.tags = list(set(existing.tags + item.tags))
        else:
            seen[key] = item

    deduped = list(seen.values())

    # Filter by min_score
    output_cfg = config.get("output", {})
    min_score = output_cfg.get("min_score", 0.2)
    filtered = [i for i in deduped if i.score >= min_score]

    # Sort by score descending
    filtered.sort(key=lambda i: i.score, reverse=True)

    # Cap per category
    max_per_cat = output_cfg.get("max_items_per_category", 8)
    cat_counts: dict[str, int] = {}
    result = []
    for item in filtered:
        cat = item.category
        if cat_counts.get(cat, 0) >= max_per_cat:
            continue
        result.append(item)
        cat_counts[cat] = cat_counts.get(cat, 0) + 1

    return result
