"""
Staff signature image handling.

Uploads are normalized server-side with pillow: white paper backgrounds
become transparent so the signature overlays cleanly on the exact
signature block of the softcopy diagnostic PDF. Each staff member gets
their own folder — `{LOCAL_STORAGE_PATH}/signatures/{user_id}/signature.png`;
the DB stores the relative path only.
"""

from collections.abc import Iterable
from io import BytesIO
from pathlib import Path
from typing import cast
from uuid import UUID

from PIL import Image

from app.config import settings
from app.core.exceptions import ValidationException

MAX_SIGNATURE_BYTES = 2 * 1024 * 1024
MAX_SIGNATURE_WIDTH = 600
ALLOWED_CONTENT_TYPES = {"image/png", "image/jpeg"}
WHITE_THRESHOLD = 240


def signature_dir() -> Path:
    """Resolve (creating) the on-disk signature root folder."""
    path = Path(settings.LOCAL_STORAGE_PATH) / "signatures"
    path.mkdir(parents=True, exist_ok=True)
    return path


def staff_signature_dir(user_id: UUID) -> Path:
    """Resolve (creating) a staff member's own signature folder."""
    path = signature_dir() / str(user_id)
    path.mkdir(parents=True, exist_ok=True)
    return path


def signature_path_for(user_id: UUID) -> Path:
    """Absolute PNG path for a staff member's signature."""
    return staff_signature_dir(user_id) / "signature.png"


def whiten_to_transparent(image: Image.Image) -> Image.Image:
    """Convert near-white pixels to transparent, keep dark ink opaque."""
    rgba = image.convert("RGBA")
    data = rgba.getdata()
    cleaned: list[tuple[int, int, int, int]] = []
    for r, g, b, _ in cast(Iterable[tuple[int, int, int, int]], data):
        lum = (r + g + b) // 3
        if lum >= WHITE_THRESHOLD:
            cleaned.append((255, 255, 255, 0))
        else:
            alpha = max(0, 255 - (lum * 255 // WHITE_THRESHOLD))
            cleaned.append((r, g, b, max(alpha, 60)))
    rgba.putdata(cleaned)
    return rgba


def save_signature(user_id: UUID, raw: bytes, content_type: str | None) -> str:
    """Validate, normalize, and persist a signature PNG. Returns the relative path."""
    if not raw:
        raise ValidationException(message="Signature file is empty.")
    if len(raw) > MAX_SIGNATURE_BYTES:
        raise ValidationException(message="Signature must be under 2 MB.")
    if content_type and content_type not in ALLOWED_CONTENT_TYPES:
        raise ValidationException(message="Signature must be a PNG or JPEG image.")

    try:
        image = Image.open(BytesIO(raw))
        if image.width * image.height > 12_000_000:
            raise ValidationException(message="Signature image dimensions are too large.")
        image.thumbnail((MAX_SIGNATURE_WIDTH, MAX_SIGNATURE_WIDTH))
    except Exception as exc:
        raise ValidationException(message="Could not read the image file.") from exc

    cleaned = whiten_to_transparent(image)
    if cleaned.width > MAX_SIGNATURE_WIDTH:
        ratio = MAX_SIGNATURE_WIDTH / cleaned.width
        resampling = getattr(Image, "Resampling", Image).LANCZOS
        cleaned = cleaned.resize((MAX_SIGNATURE_WIDTH, max(1, int(cleaned.height * ratio))), resampling)

    target = signature_path_for(user_id)
    cleaned.save(target, format="PNG")
    # Drop the pre-folder-era flat file if present (same staff, safe).
    legacy = signature_dir() / f"{user_id}.png"
    try:
        if legacy.is_file() and legacy.resolve() != target.resolve():
            legacy.unlink()
    except OSError:
        pass
    return f"signatures/{user_id}/signature.png"


def delete_signature(user_id: UUID) -> None:
    """Remove a staff member's signature file (and prune their folder) if present."""
    target = signature_path_for(user_id)
    if target.exists():
        target.unlink()
    legacy = signature_dir() / f"{user_id}.png"
    if legacy.exists():
        legacy.unlink()
    try:
        staff_signature_dir(user_id).rmdir()
    except OSError:
        pass


def read_signature_bytes(relative_path: str | None) -> bytes | None:
    """Read a stored signature file by its DB relative path."""
    if not relative_path:
        return None
    target = Path(settings.LOCAL_STORAGE_PATH) / relative_path
    try:
        resolved = target.resolve()
        base = Path(settings.LOCAL_STORAGE_PATH).resolve()
    except OSError:
        return None
    if base not in resolved.parents and resolved != base:
        return None
    if not resolved.is_file():
        return None
    return resolved.read_bytes()
