from __future__ import annotations

import hashlib
import json
from pathlib import Path
from typing import Any


ALLOWED_ACTIONS = (
    "idle",
    "sleep",
    "walk",
    "look",
    "sit",
    "tail_wag",
)

DEFAULT_RUNTIME_VERSION = "0.1.0"
DEFAULT_FRAMES_PER_ACTION = 8


class PetPackageError(ValueError):
    pass


def load_pet_package(package_dir: str | Path) -> dict[str, Any]:
    package_path = Path(package_dir)
    manifest_path = package_path / "pet.json"
    if not manifest_path.exists():
        raise PetPackageError("pet.json is missing")

    try:
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        raise PetPackageError(f"pet.json is invalid JSON: {exc.msg}") from exc

    return validate_pet_package(package_path, manifest)


def validate_pet_package(package_dir: str | Path, manifest: dict[str, Any]) -> dict[str, Any]:
    package_path = Path(package_dir)
    pet_id = _required_text(manifest, "id")
    display_name = _required_text(manifest, "displayName")
    description = _required_text(manifest, "description")
    spritesheet_path = _required_text(manifest, "spritesheetPath")

    if Path(spritesheet_path).is_absolute() or ".." in Path(spritesheet_path).parts:
        raise PetPackageError("spritesheet path must be a package-relative file")

    spritesheet_file = package_path / spritesheet_path
    if not spritesheet_file.exists():
        raise PetPackageError("spritesheet file is missing")

    animations = _normalize_animations(manifest.get("animations"))
    checksum = hashlib.sha256(spritesheet_file.read_bytes()).hexdigest()

    return {
        "id": pet_id,
        "displayName": display_name,
        "description": description,
        "runtimeVersion": str(manifest.get("runtimeVersion") or DEFAULT_RUNTIME_VERSION),
        "asset": {
            "spritesheetPath": spritesheet_path,
            "checksumAlgorithm": "sha256",
            "checksum": checksum,
            "mimeType": _guess_mime_type(spritesheet_file),
        },
        "animations": animations,
        "userApproved": bool(manifest.get("userApproved", False)),
        "source": {
            "fixtureKind": "public",
            "sensitiveContentStored": False,
        },
    }


def _required_text(manifest: dict[str, Any], key: str) -> str:
    value = manifest.get(key)
    if not isinstance(value, str) or not value.strip():
        raise PetPackageError(f"{key} is required")
    return value.strip()


def _normalize_animations(raw: Any) -> dict[str, dict[str, int | str]]:
    if raw is None:
        return {
            action: {"row": index, "frames": DEFAULT_FRAMES_PER_ACTION, "mode": "low_disturbance"}
            for index, action in enumerate(ALLOWED_ACTIONS)
        }
    if not isinstance(raw, dict):
        raise PetPackageError("animations must be an object")

    normalized: dict[str, dict[str, int | str]] = {}
    for action, spec in raw.items():
        if action not in ALLOWED_ACTIONS:
            raise PetPackageError(f"animation action is not allowed: {action}")
        if not isinstance(spec, dict):
            raise PetPackageError(f"animation spec for {action} must be an object")
        frames = int(spec.get("frames", 1))
        if frames < 1:
            raise PetPackageError(f"animation frames for {action} must be positive")
        normalized[action] = {
            "row": int(spec.get("row", 0)),
            "frames": frames,
            "mode": str(spec.get("mode", "low_disturbance")),
        }

    missing = set(ALLOWED_ACTIONS) - set(normalized)
    for action in missing:
        normalized[action] = {
            "row": ALLOWED_ACTIONS.index(action),
            "frames": DEFAULT_FRAMES_PER_ACTION,
            "mode": "low_disturbance",
        }
    return normalized


def _guess_mime_type(path: Path) -> str:
    if path.suffix.lower() == ".webp":
        return "image/webp"
    if path.suffix.lower() == ".png":
        return "image/png"
    return "application/octet-stream"
