from __future__ import annotations

import hashlib
import importlib.util
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from types import ModuleType

from ccma.rules.api import RuleAction, RuleContext


class RuleLoadError(RuntimeError):
    pass


@dataclass(frozen=True, slots=True)
class LoadedRule:
    rule_id: str
    filename: str
    source: str
    path: Path
    script_hash: str
    evaluate: Callable[[RuleContext], list[RuleAction]]
    order: int


def load_rules(store_root: Path) -> list[LoadedRule]:
    builtin_root = Path(__file__).resolve().parent / "scripts"
    override_root = store_root / "rules"
    selected = {path.name: (path, "builtin") for path in _script_files(builtin_root)}
    for path in _script_files(override_root):
        selected[path.name] = (path, "store-override" if path.name in selected else "store")
    rules = [_load_rule(path, source) for _name, (path, source) in sorted(selected.items())]
    return sorted(rules, key=lambda rule: (rule.order, rule.filename))


def _script_files(root: Path) -> list[Path]:
    if not root.is_dir():
        return []
    return [path for path in root.glob("*.py") if not path.name.startswith("_")]


def _load_rule(path: Path, source: str) -> LoadedRule:
    content = path.read_bytes()
    digest = hashlib.sha256(content).hexdigest()
    module_name = f"ccma_dynamic_rule_{path.stem}_{digest[:12]}"
    spec = importlib.util.spec_from_file_location(module_name, path)
    if not spec or not spec.loader:
        raise RuleLoadError(f"Regel {path.name} konnte nicht geladen werden.")
    module = importlib.util.module_from_spec(spec)
    try:
        spec.loader.exec_module(module)
    except Exception as exc:
        raise RuleLoadError(f"Regel {path.name} konnte nicht importiert werden: {exc}") from exc
    return _validate_module(module, path, source, digest)


def _validate_module(module: ModuleType, path: Path, source: str, digest: str) -> LoadedRule:
    rule_id = str(getattr(module, "RULE_ID", "")).strip()
    evaluate = getattr(module, "evaluate", None)
    if not rule_id or not rule_id.replace("-", "").replace("_", "").isalnum():
        raise RuleLoadError(f"Regel {path.name} hat keine gültige RULE_ID.")
    if not callable(evaluate):
        raise RuleLoadError(f"Regel {path.name} definiert keine evaluate(context)-Funktion.")
    return LoadedRule(
        rule_id=rule_id,
        filename=path.name,
        source=source,
        path=path,
        script_hash=f"sha256:{digest}",
        evaluate=evaluate,
        order=int(getattr(module, "ORDER", 100)),
    )
