"""Per-user custom sticker packs — users/{user_id}/stickers/."""

from __future__ import annotations

import json
import re
from datetime import datetime, timezone
from io import BytesIO
from pathlib import Path
from typing import Any
from uuid import uuid4

import yaml

from niannian.users.paths import user_root

USER_STICKER_PACK_ID = "user_my"
USER_STICKER_PACK_LABEL = "我的表情"
MAX_USER_STICKERS = 48
MAX_SOURCE_BYTES = 512 * 1024
STICKER_SIZE = 256

_STICKER_ID_RE = re.compile(r"^user_[a-f0-9]{8}$")


class UserStickerError(Exception):
    def __init__(self, code: str, message: str) -> None:
        super().__init__(message)
        self.code = code
        self.message = message


def _now_iso() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat()


def stickers_root(home: Path, user_id: str) -> Path:
    return user_root(home, user_id) / "stickers"


def manifest_path(home: Path, user_id: str) -> Path:
    return stickers_root(home, user_id) / "manifest.yaml"


def images_dir(home: Path, user_id: str) -> Path:
    return stickers_root(home, user_id) / "images"


def _default_manifest() -> dict[str, Any]:
    return {
        "version": 1,
        "packs": [{"id": "my", "label": USER_STICKER_PACK_LABEL}],
        "stickers": [],
    }


def _load_manifest(path: Path) -> dict[str, Any]:
    if not path.is_file():
        return _default_manifest()
    with path.open(encoding="utf-8") as f:
        data = yaml.safe_load(f) or {}
    if not isinstance(data, dict):
        return _default_manifest()
    data.setdefault("version", 1)
    data.setdefault("packs", _default_manifest()["packs"])
    data.setdefault("stickers", [])
    if not isinstance(data["stickers"], list):
        data["stickers"] = []
    return data


def _save_manifest(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(yaml.safe_dump(data, allow_unicode=True, sort_keys=False), encoding="utf-8")


def _new_sticker_id(existing: set[str]) -> str:
    for _ in range(32):
        sid = f"user_{uuid4().hex[:8]}"
        if sid not in existing:
            return sid
    raise UserStickerError("sticker_limit_reached", "cannot allocate sticker id")


def _resize_png(data: bytes) -> bytes:
    try:
        from PIL import Image
    except ImportError as exc:
        raise UserStickerError("image_processing_unavailable", "Pillow not installed") from exc

    if len(data) > MAX_SOURCE_BYTES:
        raise UserStickerError("image_too_large", f"image exceeds {MAX_SOURCE_BYTES} bytes")
    try:
        with Image.open(BytesIO(data)) as img:
            img = img.convert("RGBA")
            img.thumbnail((STICKER_SIZE, STICKER_SIZE), Image.Resampling.LANCZOS)
            canvas = Image.new("RGBA", (STICKER_SIZE, STICKER_SIZE), (0, 0, 0, 0))
            ox = (STICKER_SIZE - img.width) // 2
            oy = (STICKER_SIZE - img.height) // 2
            canvas.paste(img, (ox, oy), img)
            out = BytesIO()
            canvas.save(out, format="PNG", optimize=True)
            return out.getvalue()
    except UserStickerError:
        raise
    except Exception as exc:
        raise UserStickerError("invalid_image", str(exc) or "invalid image") from exc


def list_user_stickers(home: Path, user_id: str) -> dict[str, Any]:
    manifest = _load_manifest(manifest_path(home, user_id))
    stickers: list[dict[str, Any]] = []
    for row in manifest.get("stickers") or []:
        if not isinstance(row, dict):
            continue
        sid = str(row.get("id") or "").strip()
        if not sid:
            continue
        stickers.append(
            {
                "id": sid,
                "pack": str(row.get("pack") or "my"),
                "file": str(row.get("file") or f"images/{sid}.png"),
                "label": str(row.get("label") or sid),
                "tags": str(row.get("tags") or "自定义"),
            }
        )
    return {
        "pack": USER_STICKER_PACK_ID,
        "packs": [{"id": "my", "label": USER_STICKER_PACK_LABEL}],
        "stickers": stickers,
    }


def get_user_sticker(home: Path, user_id: str, sticker_id: str) -> dict[str, Any] | None:
    sid = str(sticker_id).strip()
    if not _STICKER_ID_RE.match(sid):
        return None
    manifest = _load_manifest(manifest_path(home, user_id))
    for row in manifest.get("stickers") or []:
        if isinstance(row, dict) and str(row.get("id")) == sid:
            return row
    return None


def user_sticker_image_path(home: Path, user_id: str, sticker_id: str) -> Path | None:
    row = get_user_sticker(home, user_id, sticker_id)
    if not row:
        return None
    rel = str(row.get("file") or f"images/{sticker_id}.png")
    path = stickers_root(home, user_id) / rel
    return path if path.is_file() else None


def is_user_sticker_id(sticker_id: str) -> bool:
    return bool(_STICKER_ID_RE.match(str(sticker_id).strip()))


def validate_user_sticker_id(home: Path, user_id: str, sticker_id: str) -> dict[str, Any]:
    row = get_user_sticker(home, user_id, sticker_id)
    if not row:
        raise UserStickerError("sticker_not_found", f"unknown user sticker: {sticker_id!r}")
    return row


def user_sticker_label(home: Path, user_id: str, sticker_id: str) -> str:
    row = validate_user_sticker_id(home, user_id, sticker_id)
    return str(row.get("label") or sticker_id)


def add_user_sticker_from_bytes(
    home: Path,
    user_id: str,
    data: bytes,
    *,
    label: str | None = None,
    source_attachment_id: str | None = None,
) -> dict[str, Any]:
    manifest_file = manifest_path(home, user_id)
    manifest = _load_manifest(manifest_file)
    stickers = [s for s in (manifest.get("stickers") or []) if isinstance(s, dict)]
    if len(stickers) >= MAX_USER_STICKERS:
        raise UserStickerError("sticker_limit_reached", f"max {MAX_USER_STICKERS} user stickers")

    png = _resize_png(data)
    existing_ids = {str(s.get("id")) for s in stickers}
    sid = _new_sticker_id(existing_ids)
    img_dir = images_dir(home, user_id)
    img_dir.mkdir(parents=True, exist_ok=True)
    rel_file = f"images/{sid}.png"
    (stickers_root(home, user_id) / rel_file).write_bytes(png)

    clean_label = (label or "自定义").strip()[:32] or "自定义"
    row = {
        "id": sid,
        "pack": "my",
        "file": rel_file,
        "label": clean_label,
        "tags": "自定义",
        "created_at": _now_iso(),
    }
    if source_attachment_id:
        row["source_attachment_id"] = source_attachment_id
    stickers.append(row)
    manifest["stickers"] = stickers
    _save_manifest(manifest_file, manifest)
    return {
        "id": sid,
        "pack": "my",
        "file": rel_file,
        "label": clean_label,
        "tags": "自定义",
    }


def delete_user_sticker(home: Path, user_id: str, sticker_id: str) -> bool:
    manifest_file = manifest_path(home, user_id)
    manifest = _load_manifest(manifest_file)
    stickers = [s for s in (manifest.get("stickers") or []) if isinstance(s, dict)]
    kept: list[dict[str, Any]] = []
    removed: dict[str, Any] | None = None
    for row in stickers:
        if str(row.get("id")) == sticker_id:
            removed = row
        else:
            kept.append(row)
    if removed is None:
        return False
    manifest["stickers"] = kept
    _save_manifest(manifest_file, manifest)
    rel = str(removed.get("file") or f"images/{sticker_id}.png")
    img = stickers_root(home, user_id) / rel
    if img.is_file():
        img.unlink(missing_ok=True)
    return True


def add_user_sticker_from_attachment(
    home: Path,
    user_id: str,
    attachment_id: str,
    *,
    label: str | None = None,
) -> dict[str, Any]:
    from niannian.chat.attachments import attachment_blob_path, get_attachment_meta

    meta = get_attachment_meta(home, user_id, attachment_id)
    if not meta:
        raise UserStickerError("invalid_attachment", "attachment not found")
    mime = str(meta.get("mime") or "")
    if not mime.startswith("image/"):
        raise UserStickerError("not_image", "attachment is not an image")
    path = attachment_blob_path(home, user_id, attachment_id)
    if not path.is_file():
        raise UserStickerError("invalid_attachment", "attachment blob missing")
    data = path.read_bytes()
    return add_user_sticker_from_bytes(
        home,
        user_id,
        data,
        label=label,
        source_attachment_id=attachment_id,
    )
