"""The image upload pipeline: validation, WebP conversion and storage."""
import io
import os
import re
import secrets
from datetime import datetime

from nexora.config import (
    ALLOWED_IMAGE_EXTENSIONS,
    ALLOWED_IMAGE_MIME_TYPES,
    CANONICAL_IMAGE_DIRS,
    DEFAULT_HOME_BACKGROUND,
    DEFAULT_SITE_LOGO,
    GAME_SUBFOLDER_CATEGORIES,
    IMAGE_ROOT,
    IMAGE_SIGNATURES,
    STATIC_DIR,
    UPLOAD_DIR,
    logger,
)
from nexora.core import app
from nexora.database.connection import execute, query
from nexora.services.images import _IMAGE_EXTENSIONS, invalidate_image_cache, static_file_abs


MAX_STORED_IMAGE_BYTES = 900 * 1024
MAX_STORED_IMAGE_DIMENSION = 1920


# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
# Canonical path helpers
# â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
def sanitize_folder_name(title):
    """Game title -> safe folder name. Spaces are preserved so new folders
    match the existing canonical ones (e.g. 'TEKKEN 7', 'PUBG Mobile')."""
    name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "", (title or ""))
    name = re.sub(r"\s+", " ", name).strip().rstrip(". ")
    return name[:60]


def resolve_game_dir(base_dir, game_title):
    """Return base_dir/<Game Name>/ reusing an existing folder when only the
    letter case differs, so titles like 'Tekken 7' reuse 'TEKKEN 7'."""
    folder = sanitize_folder_name(game_title) or "Unsorted"
    try:
        for child in base_dir.iterdir():
            if child.is_dir() and child.name.lower() == folder.lower():
                return child
    except OSError:
        pass
    return base_dir / folder


def ensure_canonical_image_dirs():
    """Create every canonical directory once at startup (idempotent)."""
    for sub in CANONICAL_IMAGE_DIRS.values():
        (IMAGE_ROOT / sub).mkdir(parents=True, exist_ok=True)
    # Per-game subfolders for games already registered in the DB
    try:
        for row in query("SELECT title FROM games"):
            title = sanitize_folder_name(row["title"])
            if not title:
                continue
            (IMAGE_ROOT / "Games" / title).mkdir(parents=True, exist_ok=True)
            (IMAGE_ROOT / "Registrations" / title).mkdir(parents=True, exist_ok=True)
    except Exception:
        pass  # DB may not exist yet on very first boot


def repair_broken_settings_images():
    """If branding settings point at files that no longer exist, search both
    Images/ and uploads/ for any matching filename (any extension), then fall
    back to the bare-name defaults that resolve via uploads/."""
    settings = query("SELECT * FROM settings WHERE id = 1", one=True)
    if not settings:
        return

    def find_any(ref):
        """Return the best matching path for *ref*, or None."""
        p = static_file_abs(ref)
        if p and p.is_file():
            return p
        bare = os.path.basename((ref or "").strip())
        if not bare:
            return None
        stem = os.path.splitext(bare)[0]
        for folder in ("uploads", "Images"):
            parent = STATIC_DIR / folder
            if not parent.is_dir():
                continue
            for ext in _IMAGE_EXTENSIONS:
                c = parent / (stem + ext)
                if c.is_file():
                    return c
        return None

    updates = {}
    if not find_any(settings["site_logo"]):
        updates["site_logo"] = "Nexora Logo.png"
    if not find_any(settings["home_background"]):
        updates["home_background"] = "NexoraGame.png"
    if "payment_qr_image" in settings.keys() and settings["payment_qr_image"] and not find_any(settings["payment_qr_image"]):
        bank_qr = static_file_abs("Images/Bank QR/Bank QR.webp")
        if bank_qr and bank_qr.is_file():
            updates["payment_qr_image"] = "Images/Bank QR/Bank QR.webp"
    if updates:
        assignments = ", ".join(f"{name} = ?" for name in updates)
        execute(f"UPDATE settings SET {assignments} WHERE id = 1", list(updates.values()))
        logger.info("Repaired broken branding references: %s", ", ".join(updates))


def process_image_upload(file, category="uploads", game_title=None, override_filename=None):
    """Accept verified image uploads and save them to the canonical
    static/Images/<Category>[/Game]/ folder.
    """
    if file is None or not getattr(file, "filename", ""):
        return None, ""

    # ── 1. Read the raw bytes ──
    data = file.read()
    if not data:
        return None, "The selected file is empty."
    if len(data) > app.config["MAX_CONTENT_LENGTH"]:
        return None, "File is too large. Maximum upload size is 8 MB."

    # ── 2. Allow image formats only: extension, browser MIME, and file signature ──
    ext = ""
    if "." in (file.filename or ""):
        ext = file.filename.rsplit(".", 1)[1].lower()
    if ext == "jfif":
        ext = "jpg"
    content_type = (getattr(file, "mimetype", "") or "").lower()
    signature_ok = any(data.startswith(sig) for sig in IMAGE_SIGNATURES)
    if data.startswith(b"RIFF") and data[8:12] != b"WEBP":
        signature_ok = False
    if ext not in ALLOWED_IMAGE_EXTENSIONS or content_type not in ALLOWED_IMAGE_MIME_TYPES or not signature_ok:
        return None, "Only image uploads are allowed (PNG, JPG, WEBP, or GIF)."

    # ── 3. Resolve the canonical destination folder ──
    rel_subdir = CANONICAL_IMAGE_DIRS.get(category)
    dest_dir = UPLOAD_DIR
    rel_prefix = "uploads"
    if rel_subdir:
        try:
            candidate_dir = IMAGE_ROOT / rel_subdir
            if category in GAME_SUBFOLDER_CATEGORIES and game_title:
                candidate_dir = resolve_game_dir(candidate_dir, game_title)
            candidate_dir.mkdir(parents=True, exist_ok=True)
            test_file = candidate_dir / ".write_test"
            test_file.write_bytes(b"ok")
            test_file.unlink()
            dest_dir = candidate_dir
            rel_prefix = f"Images/{rel_subdir}"
            if category in GAME_SUBFOLDER_CATEGORIES and game_title:
                rel_prefix = f"{rel_prefix}/{dest_dir.name}"
        except OSError as exc:
            logger.exception("Image upload save failed for category %s", category)
            return None, f"cPanel Permission Error: Could not save image to folder. (Check static/ folder permissions on cPanel). Details: {exc}"

    try:
        dest_dir.mkdir(parents=True, exist_ok=True)
    except OSError as exc:
        return None, f"cPanel Permission Error (Storage): Could not create the upload folder. Details: {exc}"

    stored_ext = ext
    stored_data = data
    if ext != "gif":
        try:
            from PIL import Image, ImageOps
            with Image.open(io.BytesIO(data)) as image:
                image.verify()
            with Image.open(io.BytesIO(data)) as image:
                image = ImageOps.exif_transpose(image)
                if image.mode not in ("RGB", "RGBA"):
                    image = image.convert("RGBA" if "A" in image.getbands() else "RGB")
                if max(image.size) > MAX_STORED_IMAGE_DIMENSION:
                    image.thumbnail((MAX_STORED_IMAGE_DIMENSION, MAX_STORED_IMAGE_DIMENSION))
                best = None
                for quality in (82, 74, 66, 58):
                    out = io.BytesIO()
                    image.save(out, format="WEBP", quality=quality, method=6)
                    candidate = out.getvalue()
                    best = candidate
                    if len(candidate) <= MAX_STORED_IMAGE_BYTES:
                        break
                stored_data = best or data
                stored_ext = "webp"
        except Exception:
            logger.exception("Image optimisation failed for %s", file.filename)
            return None, "The selected image could not be processed. Please upload a valid PNG, JPG, WEBP, or GIF."

    # ── 4. Generate filename ──
    if override_filename:
        safe_name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', override_filename.strip())
        for old_ext in ALLOWED_IMAGE_EXTENSIONS | {"webp"}:
            old_file = dest_dir / f"{safe_name}.{old_ext}"
            if old_file.is_file():
                old_file.unlink()
        filename = f"{safe_name}.{stored_ext}"
    else:
        stamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
        filename = f"{stamp}_{secrets.token_hex(4)}.{stored_ext}"

    # ── 5. Write optimized bytes ──
    try:
        dest_file = dest_dir / filename
        dest_file.write_bytes(stored_data)
        # Verify the file was actually written (catches silent cPanel failures)
        if not dest_file.is_file() or dest_file.stat().st_size != len(stored_data):
            logger.error("Post-write verification failed: expected %d bytes, got %s",
                         len(stored_data), str(dest_file.stat().st_size) if dest_file.is_file() else "file missing")
            return None, "File could not be saved. The server may have disk permission issues."
    except OSError as exc:
        logger.exception("Image upload write failed for %s", dest_dir)
        return None, f"Could not write file on the server: {exc}"

    relative = f"{rel_prefix}/{filename}"
    invalidate_image_cache()
    logger.info("Upload saved: %d -> %d bytes -> %s (exists=%s)", len(data), len(stored_data), relative, dest_file.is_file())
    return relative, ""


def delete_upload(reference):
    """Delete a stored image by its DB reference. Accepts canonical
    'Images/...' paths and legacy bare uploads/ filenames. Path-traversal
    safe: the resolved target must remain inside static/."""
    if not reference:
        return
    # Canonical assets referenced as defaults are never deleted implicitly
    if reference in (DEFAULT_SITE_LOGO, DEFAULT_HOME_BACKGROUND):
        return
    target = static_file_abs(reference)
    try:
        if target and target.is_file():
            target.unlink()
    except OSError:
        pass
