import json
import re
import struct
from functools import lru_cache
from io import BytesIO
from pathlib import Path

from PIL import Image, UnidentifiedImageError

from .settings import DATA_DIR


def load_json(path: Path) -> dict:
    with path.open("r", encoding="utf-8") as f:
        return json.load(f)


def load_project_version() -> str:
    project_file = DATA_DIR / "project.json"
    if not project_file.exists():
        return ""
    return str(load_json(project_file).get("projectVersion", ""))


def find_art_for_json(json_path: Path) -> Path | None:
    base = json_path.with_suffix("")
    for ext in (".png", ".jpg", ".jpeg"):
        candidate = base.with_suffix(ext)
        if candidate.exists():
            return candidate
    return None


@lru_cache(maxsize=256)
def _load_rgb_cached(path: str, mtime_ns: int, size: int) -> Image.Image:
    try:
        return Image.open(path).convert("RGBA")
    except UnidentifiedImageError:
        cleaned = strip_png_ancillary_chunks(Path(path))
        if cleaned is None:
            raise
        return Image.open(BytesIO(cleaned)).convert("RGBA")


def load_rgb(path: Path) -> Image.Image:
    stat = path.stat()
    return _load_rgb_cached(str(path), stat.st_mtime_ns, stat.st_size).copy()


def save_temp_png(img: Image.Image, path: Path, dpi: int) -> None:
    img.save(path, dpi=(dpi, dpi), compress_level=1, optimize=False)


def strip_png_ancillary_chunks(path: Path) -> bytes | None:
    """Return a PNG with nonessential chunks removed.

    A few source PNG templates carry metadata chunks that Pillow refuses to
    parse, while the pixel data itself is valid. Keeping critical chunks gives
    us a deterministic source-data repair without touching the copied assets.
    """
    data = path.read_bytes()
    signature = b"\x89PNG\r\n\x1a\n"
    if not data.startswith(signature):
        return None

    keep = {b"IHDR", b"PLTE", b"IDAT", b"IEND", b"tRNS"}
    out = bytearray(signature)
    pos = len(signature)
    try:
        while pos + 12 <= len(data):
            size = struct.unpack(">I", data[pos : pos + 4])[0]
            chunk_type = data[pos + 4 : pos + 8]
            chunk_end = pos + 8 + size + 4
            if chunk_end > len(data):
                return None
            if chunk_type in keep:
                out.extend(data[pos:chunk_end])
            pos = chunk_end
            if chunk_type == b"IEND":
                return bytes(out)
    except struct.error:
        return None
    return None


def cover_resize(img: Image.Image, size: tuple[int, int]) -> Image.Image:
    target_w, target_h = size
    src = img.convert("RGBA")
    ratio = max(target_w / src.width, target_h / src.height)
    new_size = (round(src.width * ratio), round(src.height * ratio))
    resized = src.resize(new_size, Image.Resampling.LANCZOS)
    left = max(0, (resized.width - target_w) // 2)
    top = max(0, (resized.height - target_h) // 2)
    return resized.crop((left, top, left + target_w, top + target_h))


def render_work_scale(meta: dict, target_size: tuple[int, int]) -> float:
    sx = target_size[0] / float(meta["cardWidth"])
    sy = target_size[1] / float(meta["cardHeight"])
    return max(2.0, sx, sy)


def meta_unit_scale(meta: dict) -> float:
    return float(meta.get("cardWidth", 825)) / 825.0


def hex_color(value: str | None, fallback: str = "#ffffff") -> tuple[int, int, int]:
    value = value or fallback
    value = value.strip()
    if value.startswith("#"):
        value = value[1:]
    if len(value) != 6:
        return hex_color(fallback)
    return tuple(int(value[i : i + 2], 16) for i in (0, 2, 4))


def safe_name(value: str) -> str:
    value = re.sub(r'[\\/:*?"<>|]', "", value)
    value = re.sub(r"\s+", "_", value.strip())
    return value or "untitled"
