#!/usr/bin/env python3
"""Collect review metadata and reproducible theme counts without storing review text."""

from __future__ import annotations

import csv
import json
import re
import time
import urllib.parse
import urllib.request
from collections import Counter
from datetime import UTC, datetime
from pathlib import Path
from typing import Any


HERE = Path(__file__).resolve().parent
USER_AGENT = "Mozilla/5.0 (compatible; competitor-research/1.0)"

APP_STORE_APPS = {
    "ai-dungeon": {"name": "AI Dungeon", "id": "1491268416", "country": "us"},
    "ai-game-master": {
        "name": "AI Game Master - Dungeon RPG",
        "id": "6475002750",
        "country": "us",
    },
    "neverend": {"name": "Neverend: DnD AI Games", "id": "6760265252", "country": "us"},
    "quest-portal": {
        "name": "Quest Portal VTT",
        "id": "1588540503",
        "country": "us",
    },
    "fablecraft": {
        "name": "Tales of Fablecraft",
        "id": "6621180679",
        "country": "us",
    },
    "fableai": {
        "name": "FableAI: AI Text Adventure RPG",
        "id": "6484402877",
        "country": "us",
    },
    "everweave": {
        "name": "Everweave",
        "id": "6747261740",
        "country": "us",
    },
    "goat-rpg": {
        "name": "Goat RPG: Beyond DnD Adventure",
        "id": "6748677526",
        "country": "us",
    },
    "taleborn": {
        "name": "Taleborn: AI Hero Story RPG",
        "id": "6749387618",
        "country": "us",
    },
    "endless-fantasy-ai": {
        "name": "Endless Fantasy AI",
        "id": "6758146753",
        "country": "us",
    },
    "ai-dnd": {
        "name": "AI DnD",
        "id": "6752244837",
        "country": "us",
    },
    "keeper-of-the-realms": {
        "name": "Keeper of the Realms",
        "id": "6740112469",
        "country": "us",
    },
    "neural-initiative": {
        "name": "Neural Initiative",
        "id": "6780307395",
        "country": "us",
    },
}

STEAM_APPS = {
    "ai-dungeon": {"name": "AI Dungeon", "id": "1519310"},
    "fablecraft": {"name": "Tales of Fablecraft", "id": "2176900"},
    "rolemi-aster": {"name": "RoleMIAster", "id": "4210600"},
}

APP_STORE_CSV_FIELDS = ("rating", "review_id", "updated_at", "version")
STEAM_CSV_FIELDS = (
    "created_at_utc",
    "language",
    "review_id",
    "updated_at_utc",
    "voted_up",
    "votes_up",
)

THEME_PATTERNS = {
    "creativity": (r"\bcreativ", r"\bfreedom\b", r"imagin", r"story", r"immers"),
    "memory_continuity": (
        r"\bmemory\b",
        r"\bremember",
        r"\bforget",
        r"inconsisten",
        r"continu",
        r"contradict",
        r"\bplot\b",
    ),
    "rules_combat_state": (
        r"\bcombat\b",
        r"\bdamage\b",
        r"\bdice\b",
        r"\broll",
        r"\bstats?\b",
        r"\binventory\b",
        r"\bitems?\b",
        r"\bquests?\b",
        r"\blevel",
        r"\bspells?\b",
        r"\babilit",
    ),
    "price_monetization": (
        r"\bprice",
        r"expensive",
        r"subscription",
        r"\btokens?\b",
        r"\bcredits?\b",
        r"paywall",
        r"\bmoney\b",
        r"\bcost",
        r"purchase",
    ),
    "bugs_stability": (
        r"\bbugs?\b",
        r"\bbuggy\b",
        r"\bcrash",
        r"\bfreez",
        r"\bloading\b",
        r"black screen",
        r"\bglitch",
        r"\bbroken\b",
        r"\berrors?\b",
    ),
    "repetition_output_quality": (
        r"\brepeat",
        r"repetiti",
        r"\bloop",
        r"\bgeneric\b",
        r"\bboring\b",
        r"\bincoherent",
    ),
    "ui_ux": (
        r"interface",
        r"\bui\b",
        r"\bux\b",
        r"\bmenus?\b",
        r"\bfonts?\b",
        r"text box",
        r"navigation",
        r"\bscreen\b",
    ),
    "multiplayer_voice": (
        r"multiplayer",
        r"\bfriends?\b",
        r"\bvoice\b",
        r"narration",
        r"\baudio\b",
        r"\bspeak",
    ),
    "moderation_privacy": (
        r"censor",
        r"\bfilter",
        r"privacy",
        r"moderation",
        r"nsfw",
        r"adult",
    ),
}


def classify_text(text: str) -> set[str]:
    """Return overlapping keyword-coded themes present in a review."""
    normalized = text.casefold()
    return {
        theme
        for theme, patterns in THEME_PATTERNS.items()
        if any(re.search(pattern, normalized) for pattern in patterns)
    }


def aggregate_rows(rows: list[dict[str, Any]]) -> dict[str, Any]:
    """Aggregate literal App Store ratings and overlapping theme labels."""
    ratings = [int(row["rating"]) for row in rows]
    theme_counts = Counter(theme for row in rows for theme in row["themes"])
    return {
        "reviews_retrieved": len(rows),
        "mean_rating": round(sum(ratings) / len(ratings), 3) if ratings else None,
        "rating_distribution": dict(sorted(Counter(map(str, ratings)).items())),
        "theme_counts": dict(sorted(theme_counts.items())),
    }


def fetch_json(url: str) -> dict[str, Any]:
    request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
    with urllib.request.urlopen(request, timeout=45) as response:
        return json.load(response)


def build_app_store_review_url(app: dict[str, str], page: int) -> str:
    """Build Apple's case-sensitive customer-review feed URL."""
    return (
        f"https://itunes.apple.com/{app['country']}/rss/customerreviews/"
        f"page={page}/id={app['id']}/sortBy=mostRecent/json"
    )


def collect_app_store(app: dict[str, str]) -> tuple[list[dict[str, Any]], list[str]]:
    rows_by_id: dict[str, dict[str, Any]] = {}
    urls: list[str] = []
    for page in range(1, 11):
        url = build_app_store_review_url(app, page)
        urls.append(url)
        payload = fetch_json(url)
        entries = payload.get("feed", {}).get("entry", [])
        for entry in entries:
            review_id = entry.get("id", {}).get("label")
            rating = entry.get("im:rating", {}).get("label")
            if not review_id or not rating:
                continue
            text = " ".join(
                (
                    entry.get("title", {}).get("label", ""),
                    entry.get("content", {}).get("label", ""),
                )
            )
            rows_by_id[review_id] = {
                "review_id": review_id,
                "updated_at": entry.get("updated", {}).get("label", ""),
                "rating": int(rating),
                "version": entry.get("im:version", {}).get("label", ""),
                "themes": classify_text(text),
            }
        time.sleep(0.15)
    return list(rows_by_id.values()), urls


def collect_steam(
    app: dict[str, str],
) -> tuple[list[dict[str, Any]], list[str], int | None]:
    rows_by_id: dict[str, dict[str, Any]] = {}
    urls: list[str] = []
    cursor = "*"
    total_reviews: int | None = None
    while True:
        url = (
            "https://store.steampowered.com/appreviews/"
            + app["id"]
            + "?"
            + urllib.parse.urlencode(
                {
                    "json": 1,
                    "filter": "all",
                    "language": "all",
                    "day_range": 9223372036854775807,
                    "review_type": "all",
                    "purchase_type": "all",
                    "num_per_page": 100,
                    "cursor": cursor,
                }
            )
        )
        urls.append(url)
        payload = fetch_json(url)
        if total_reviews is None:
            total_reviews = payload.get("query_summary", {}).get("total_reviews")
        reviews = payload.get("reviews", [])
        if not reviews:
            break
        for review in reviews:
            review_id = str(review["recommendationid"])
            rows_by_id[review_id] = {
                "review_id": review_id,
                "created_at_utc": datetime.fromtimestamp(
                    int(review["timestamp_created"]), UTC
                ).isoformat(),
                "updated_at_utc": datetime.fromtimestamp(
                    int(review["timestamp_updated"]), UTC
                ).isoformat(),
                "voted_up": bool(review["voted_up"]),
                "language": review.get("language", ""),
                "votes_up": int(review.get("votes_up", 0)),
                "themes": classify_text(review.get("review", "")),
            }
        next_cursor = payload.get("cursor")
        if (
            not next_cursor
            or next_cursor == cursor
            or len(rows_by_id) >= (total_reviews or 0)
        ):
            break
        cursor = next_cursor
        time.sleep(0.15)
    return list(rows_by_id.values()), urls, total_reviews


def write_rows(
    path: Path,
    rows: list[dict[str, Any]],
    declared_fieldnames: tuple[str, ...] = (),
) -> None:
    fieldnames = sorted(
        set(declared_fieldnames)
        | {key for row in rows for key in row if key != "themes"}
    ) + ["themes"]
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fieldnames)
        writer.writeheader()
        for row in rows:
            writer.writerow({**row, "themes": ";".join(sorted(row["themes"]))})


def main() -> int:
    started_at = datetime.now(UTC).isoformat()
    sources: list[dict[str, Any]] = []
    aggregates: dict[str, Any] = {"app_store_us": {}, "steam": {}}

    for slug, app in APP_STORE_APPS.items():
        rows, urls = collect_app_store(app)
        write_rows(HERE / f"app-store-us-{slug}.csv", rows, APP_STORE_CSV_FIELDS)
        aggregates["app_store_us"][slug] = {
            "product": app["name"],
            **aggregate_rows(rows),
            "scope": "Latest reviews exposed by Apple US RSS, maximum 10 pages / 500 reviews",
        }
        sources.append(
            {"product": app["name"], "surface": "Apple US RSS", "urls": urls}
        )

    for slug, app in STEAM_APPS.items():
        rows, urls, reported_total = collect_steam(app)
        write_rows(HERE / f"steam-{slug}.csv", rows, STEAM_CSV_FIELDS)
        positive = sum(1 for row in rows if row["voted_up"])
        theme_counts = Counter(theme for row in rows for theme in row["themes"])
        aggregates["steam"][slug] = {
            "product": app["name"],
            "reviews_retrieved": len(rows),
            "api_reported_total": reported_total,
            "positive": positive,
            "negative": len(rows) - positive,
            "positive_share": round(positive / len(rows), 4) if rows else None,
            "theme_counts": dict(sorted(theme_counts.items())),
            "scope": "All reviews returned by the official Steam Reviews API; text not retained",
        }
        sources.append(
            {"product": app["name"], "surface": "Steam Reviews API", "urls": urls}
        )

    manifest = {
        "schema_version": 1,
        "started_at_utc": started_at,
        "finished_at_utc": datetime.now(UTC).isoformat(),
        "privacy_and_copyright": (
            "Review bodies, titles, and author identities were processed in memory and not stored. "
            "CSV files contain only review IDs, dates, ratings/recommendations, and overlapping theme flags."
        ),
        "coding_warning": (
            "Themes are literal keyword matches, overlap, and are not sentiment analysis. "
            "Counts support discovery of repeated topics but require qualitative reading for interpretation."
        ),
        "sources": sources,
        "aggregates": aggregates,
    }
    (HERE / "review-aggregates.json").write_text(
        json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
    )
    print(json.dumps(aggregates, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
