from __future__ import annotations

import json
import shutil
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from .models import SkillManifest

DEFAULT_SKILL_TEMPLATE = """---
name: {name}
description: {description}
---

# {title}

Explain when this skill should be used, what tools it exposes, and any safety limits.
"""

DEFAULT_ENTRYPOINT_TEMPLATE = '''from __future__ import annotations


def register(context: dict) -> dict:
    return {
        "metadata": {"source": "workspace"},
        "prompts": ["This skill is loaded and ready."],
        "tools": [],
    }
'''


RESOURCE_DIRNAME = "resources"


def _safe_name(name: str) -> str:
    cleaned = "".join(char if char.isalnum() or char in {"-", "_"} else "-" for char in name.strip().lower())
    cleaned = cleaned.strip("-_")
    if not cleaned:
        raise ValueError("Skill name must contain at least one valid character")
    return cleaned


def _normalise_relative_path(path: str) -> Path:
    candidate = Path(path)
    if candidate.is_absolute():
        raise ValueError("Skill bundle file paths must be relative")
    if ".." in candidate.parts:
        raise ValueError("Skill bundle file paths may not escape the skill directory")
    return candidate


def _write_extra_files(skill_dir: Path, extra_files: dict[str, str] | None) -> list[str]:
    written: list[str] = []
    for relative_path, content in (extra_files or {}).items():
        target = skill_dir / _normalise_relative_path(relative_path)
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(content, encoding="utf-8")
        written.append(relative_path)
    return sorted(written)


def _default_manifest(name: str, description: str, with_entrypoint: bool) -> SkillManifest:
    return SkillManifest(
        name=name,
        description=description,
        entrypoint="main.py" if with_entrypoint else None,
    )


def backup_skill(skill_dir: str | Path, backup_root: str | Path | None = None) -> Path:
    source = Path(skill_dir).expanduser().resolve()
    if not source.exists():
        raise FileNotFoundError(source)
    destination_root = Path(backup_root).expanduser().resolve() if backup_root else source.parent / ".backups"
    destination_root.mkdir(parents=True, exist_ok=True)
    timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
    destination = destination_root / f"{source.name}-{timestamp}"
    shutil.copytree(source, destination)
    return destination


def create_skill(
    root: str | Path,
    name: str,
    description: str,
    *,
    instructions: str | None = None,
    entrypoint_code: str | None = None,
    with_entrypoint: bool = True,
    manifest_overrides: dict[str, Any] | None = None,
    extra_files: dict[str, str] | None = None,
    exist_ok: bool = False,
) -> Path:
    skill_root = Path(root).expanduser().resolve()
    safe_name = _safe_name(name)
    skill_dir = skill_root / safe_name
    if skill_dir.exists() and not exist_ok:
        raise FileExistsError(skill_dir)
    skill_dir.mkdir(parents=True, exist_ok=True)

    manifest = _default_manifest(safe_name, description, with_entrypoint)
    if manifest_overrides:
        merged = manifest.to_dict()
        merged.update(manifest_overrides)
        manifest = SkillManifest.from_dict(merged)

    skill_md = instructions or DEFAULT_SKILL_TEMPLATE.format(
        name=safe_name,
        description=description,
        title=safe_name.replace("-", " ").replace("_", " ").title(),
    )

    if extra_files:
        manifest.metadata.setdefault("bundle_files", sorted(extra_files))

    (skill_dir / "skill.json").write_text(json.dumps(manifest.to_dict(), indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    (skill_dir / manifest.instructions_file).write_text(skill_md, encoding="utf-8")

    if manifest.entrypoint:
        code = entrypoint_code or DEFAULT_ENTRYPOINT_TEMPLATE
        (skill_dir / manifest.entrypoint).write_text(code, encoding="utf-8")

    _write_extra_files(skill_dir, extra_files)
    return skill_dir


def update_skill(
    skill_dir: str | Path,
    *,
    description: str | None = None,
    instructions: str | None = None,
    entrypoint_code: str | None = None,
    manifest_updates: dict[str, Any] | None = None,
    extra_files: dict[str, str] | None = None,
    create_backup: bool = True,
    backup_root: str | Path | None = None,
) -> dict[str, Path | None]:
    target = Path(skill_dir).expanduser().resolve()
    manifest_path = target / "skill.json"
    if not manifest_path.exists():
        raise FileNotFoundError(manifest_path)

    backup_path: Path | None = None
    if create_backup:
        backup_path = backup_skill(target, backup_root=backup_root)

    manifest = SkillManifest.from_dict(json.loads(manifest_path.read_text(encoding="utf-8")))
    merged = manifest.to_dict()
    if description is not None:
        merged["description"] = description
    if manifest_updates:
        merged.update(manifest_updates)
    if extra_files is not None:
        metadata = dict(merged.get("metadata", {}) or {})
        existing = set(metadata.get("bundle_files", []))
        existing.update(extra_files)
        metadata["bundle_files"] = sorted(existing)
        merged["metadata"] = metadata
    manifest = SkillManifest.from_dict(merged)
    manifest_path.write_text(json.dumps(manifest.to_dict(), indent=2, ensure_ascii=False) + "\n", encoding="utf-8")

    if instructions is not None:
        (target / manifest.instructions_file).write_text(instructions, encoding="utf-8")

    if entrypoint_code is not None:
        if not manifest.entrypoint:
            raise ValueError("Cannot write entrypoint code for a skill without an entrypoint")
        (target / manifest.entrypoint).write_text(entrypoint_code, encoding="utf-8")

    _write_extra_files(target, extra_files)
    return {"skill_dir": target, "backup_dir": backup_path}
