#!/usr/bin/env python3
"""Run live snapshot checks for plugins.

Use --update to refresh stored snapshots.
"""
from __future__ import annotations

import argparse
import asyncio
import importlib.util
import inspect
import json
import sys
from pathlib import Path
from typing import Any

ROOT_DIR = Path(__file__).resolve().parents[1]
PLUGIN_DIR = ROOT_DIR / "addon" / "plugins"
SNAPSHOT_PATH = ROOT_DIR / "qa" / "plugin_snapshots.json"

sys.path.insert(0, str(ROOT_DIR / "addon"))

try:
    from plugin_interface import BasisPlugin  # type: ignore
except Exception as exc:  # pragma: no cover
    raise SystemExit(f"Failed to import BasisPlugin: {exc}")

CONFIG = [
    {"plugin": "Serienstream", "method": "search_titles", "args": ["trek"], "max_items": 20},
    {"plugin": "Aniworld", "method": "search_titles", "args": ["naruto"], "max_items": 20},
    {"plugin": "Topstreamfilm", "method": "search_titles", "args": ["matrix"], "max_items": 20},
    {"plugin": "Einschalten", "method": "new_titles_page", "args": [1], "max_items": 20},
    {"plugin": "Filmpalast", "method": "search_titles", "args": ["trek"], "max_items": 20},
    {"plugin": "Doku-Streams", "method": "search_titles", "args": ["japan"], "max_items": 20},
]


def _import_module(path: Path):
    spec = importlib.util.spec_from_file_location(path.stem, path)
    if spec is None or spec.loader is None:
        raise ImportError(f"Missing spec for {path}")
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


def _discover_plugins() -> dict[str, BasisPlugin]:
    plugins: dict[str, BasisPlugin] = {}
    for file_path in sorted(PLUGIN_DIR.glob("*.py")):
        if file_path.name.startswith("_"):
            continue
        module = _import_module(file_path)
        preferred = getattr(module, "Plugin", None)
        if inspect.isclass(preferred) and issubclass(preferred, BasisPlugin) and preferred is not BasisPlugin:
            classes = [preferred]
        else:
            classes = [
                obj
                for obj in module.__dict__.values()
                if inspect.isclass(obj) and issubclass(obj, BasisPlugin) and obj is not BasisPlugin
            ]
            classes.sort(key=lambda cls: cls.__name__.casefold())
        for cls in classes:
            instance = cls()
            name = str(getattr(instance, "name", "") or "").strip()
            if name and name not in plugins:
                plugins[name] = instance
    return plugins


def _normalize_titles(value: Any, max_items: int) -> list[str]:
    if not value:
        return []
    titles = [str(item).strip() for item in list(value) if item and str(item).strip()]
    seen = set()
    normalized: list[str] = []
    for title in titles:
        key = title.casefold()
        if key in seen:
            continue
        seen.add(key)
        normalized.append(title)
        if len(normalized) >= max_items:
            break
    return normalized


def _snapshot_key(entry: dict[str, Any]) -> str:
    args = entry.get("args", [])
    return f"{entry['plugin']}::{entry['method']}::{','.join(str(a) for a in args)}"


def _call_method(plugin: BasisPlugin, method_name: str, args: list[Any]):
    method = getattr(plugin, method_name, None)
    if not callable(method):
        raise RuntimeError(f"Method missing: {method_name}")
    result = method(*args)
    if asyncio.iscoroutine(result):
        return asyncio.run(result)
    return result


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--update", action="store_true")
    args = parser.parse_args()

    snapshots: dict[str, Any] = {}
    if SNAPSHOT_PATH.exists():
        snapshots = json.loads(SNAPSHOT_PATH.read_text(encoding="utf-8"))
    data = snapshots.get("snapshots", {}) if isinstance(snapshots, dict) else {}
    if args.update:
        data = {}

    plugins = _discover_plugins()
    errors = []

    for entry in CONFIG:
        plugin_name = entry["plugin"]
        plugin = plugins.get(plugin_name)
        if plugin is None:
            errors.append(f"Plugin missing: {plugin_name}")
            continue
        key = _snapshot_key(entry)
        try:
            result = _call_method(plugin, entry["method"], entry.get("args", []))
            normalized = _normalize_titles(result, entry.get("max_items", 20))
        except Exception as exc:
            errors.append(f"Snapshot error: {key} ({exc})")
            if args.update:
                data[key] = {"error": str(exc)}
            continue
        if args.update:
            data[key] = normalized
        else:
            expected = data.get(key)
            if expected != normalized:
                errors.append(f"Snapshot mismatch: {key}\nExpected: {expected}\nActual:   {normalized}")

    if args.update:
        SNAPSHOT_PATH.parent.mkdir(parents=True, exist_ok=True)
        SNAPSHOT_PATH.write_text(json.dumps({"snapshots": data}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")

    if errors:
        for err in errors:
            print(err)
        return 1
    return 0


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