from __future__ import annotations

import hashlib
import importlib.util
import json
import sys
from pathlib import Path
from types import ModuleType
from typing import Any

from .models import LoadedSkill, SkillExport, SkillManifest


def discover_skill_dirs(root: Path) -> list[Path]:
    if not root.exists():
        return []
    return sorted(path for path in root.iterdir() if path.is_dir() and not path.name.startswith("."))


def _fingerprint_files(files: list[Path], root: Path) -> str:
    digest = hashlib.sha256()
    for file_path in sorted(files, key=lambda item: str(item.relative_to(root))):
        relative = str(file_path.relative_to(root)).encode("utf-8")
        stat = file_path.stat()
        digest.update(relative)
        digest.update(str(stat.st_mtime_ns).encode("utf-8"))
        digest.update(str(stat.st_size).encode("utf-8"))
    return digest.hexdigest()


def _collect_skill_files(skill_dir: Path) -> list[Path]:
    files: list[Path] = []
    for path in skill_dir.rglob("*"):
        if not path.is_file():
            continue
        if path.name.endswith((".pyc", ".pyo")):
            continue
        if "__pycache__" in path.parts:
            continue
        files.append(path)
    return files


def _load_module(entrypoint_path: Path, module_name: str) -> ModuleType:
    spec = importlib.util.spec_from_file_location(module_name, entrypoint_path)
    if spec is None or spec.loader is None:
        raise RuntimeError(f"Could not create import spec for {entrypoint_path}")
    module = importlib.util.module_from_spec(spec)
    previous = sys.dont_write_bytecode
    sys.dont_write_bytecode = True
    try:
        spec.loader.exec_module(module)
    finally:
        sys.dont_write_bytecode = previous
    return module


def load_skill(skill_dir: Path, *, context: dict[str, Any] | None = None) -> LoadedSkill:
    manifest_path = skill_dir / "skill.json"
    if not manifest_path.exists():
        raise FileNotFoundError(f"Missing skill manifest: {manifest_path}")
    payload = json.loads(manifest_path.read_text(encoding="utf-8"))
    manifest = SkillManifest.from_dict(payload)

    instructions_path = skill_dir / manifest.instructions_file
    instructions = instructions_path.read_text(encoding="utf-8") if instructions_path.exists() else ""

    export = SkillExport()
    module_name: str | None = None
    if manifest.entrypoint:
        entrypoint_path = skill_dir / manifest.entrypoint
        if entrypoint_path.exists():
            module_name = f"openclaw_lite_skill_{manifest.name}_{abs(hash(str(skill_dir))) & 0xFFFF_FFFF}"
            module = _load_module(entrypoint_path, module_name)
            register = getattr(module, manifest.register_function, None)
            if callable(register):
                export = SkillExport.normalize(register(context or {}))

    files = _collect_skill_files(skill_dir)
    fingerprint = _fingerprint_files(files, skill_dir)
    return LoadedSkill(
        manifest=manifest,
        path=skill_dir,
        instructions=instructions,
        export=export,
        fingerprint=fingerprint,
        files=tuple(sorted(files)),
        module_name=module_name,
    )


class SkillLoader:
    def __init__(self, root: str | Path):
        self.root = Path(root).expanduser().resolve()

    def discover(self) -> list[Path]:
        return discover_skill_dirs(self.root)

    def load(self, name: str, *, context: dict[str, Any] | None = None) -> LoadedSkill:
        return load_skill(self.root / name, context=context)

    def load_all(self, *, context: dict[str, Any] | None = None) -> list[LoadedSkill]:
        loaded: list[LoadedSkill] = []
        for skill_dir in self.discover():
            loaded.append(load_skill(skill_dir, context=context))
        return loaded
