"""
app.py

Minimal web wrapper around the brand-manual pipeline, for testing this
live on a server. Single endpoint: upload a logo + client name, get a
generated brand manual PDF back.

Deliberately synchronous (no job queue) -- fine for testing under low
concurrency. See the notes at the bottom of this file for what changes
once real traffic shows up.

Run locally:
    uvicorn app:app --host 0.0.0.0 --port 8000

Then visit http://localhost:8000 for a basic upload form, or POST
directly to /generate.
"""

import json
import re
import shutil
import uuid
from pathlib import Path

from fastapi import FastAPI, File, Form, UploadFile, HTTPException
from fastapi.responses import FileResponse, HTMLResponse
from PIL import Image

from batch_generate import run_batch
from render_brand_manual import render as render_manual
from render_pdf import render_pdf
from fonts import ALLOWED_FONT_EXTS
from photo_mockups import composite_photo_mockup, CATEGORY_CONFIG

# --- config -----------------------------------------------------------

MOCKUPS_DIR = Path("library")            # your mockup library, shipped with the server
TEMPLATE_PATH = Path("brand-manual-template.html")
WORK_DIR = Path("jobs")                  # scratch space, safe to purge periodically
MAX_LOGO_BYTES = 8 * 1024 * 1024         # 8MB upload cap
MAX_FONT_BYTES = 4 * 1024 * 1024         # 4MB per font file
ALLOWED_CONTENT_TYPES = {"image/png", "image/jpeg", "image/webp"}

Image.MAX_IMAGE_PIXELS = 40_000_000      # guards against decompression-bomb uploads

WORK_DIR.mkdir(exist_ok=True)

app = FastAPI(title="Brand Manual Generator")


# --- helpers ------------------------------------------------------------

def slugify(name: str, fallback: str = "client") -> str:
    slug = re.sub(r"[^a-zA-Z0-9]+", "-", name).strip("-").lower()
    slug = slug[:60]
    return slug or fallback


def validate_logo_upload(file: UploadFile, raw_bytes: bytes):
    if file.content_type not in ALLOWED_CONTENT_TYPES:
        raise HTTPException(400, f"Unsupported file type '{file.content_type}'. Upload PNG, JPEG, or WEBP.")
    if len(raw_bytes) > MAX_LOGO_BYTES:
        raise HTTPException(400, "Logo file too large (8MB limit).")
    try:
        img = Image.open(__import__("io").BytesIO(raw_bytes))
        img.verify()  # raises if not a valid, non-malicious image
    except Exception:
        raise HTTPException(400, "Could not read this file as a valid image.")


def save_optional_font(upload: UploadFile, job_dir: Path, label: str):
    """Validates and saves an optional font upload, returns its path or None."""
    if upload is None or not upload.filename:
        return None
    ext = Path(upload.filename).suffix.lstrip(".").lower()
    if ext not in ALLOWED_FONT_EXTS:
        raise HTTPException(400, f"{label} font must be OTF, TTF, WOFF, or WOFF2 (got '.{ext}').")
    raw = upload.file.read()
    if len(raw) > MAX_FONT_BYTES:
        raise HTTPException(400, f"{label} font file too large (4MB limit).")
    # keep the original stem (sanitized) so the display name derives from the
    # actual font, e.g. "BigShoulders-Bold.ttf" -> "Bigshoulders Bold", not
    # the generic "heading-font.ttf" -> "Heading Font"
    safe_stem = re.sub(r"[^a-zA-Z0-9_-]+", "", Path(upload.filename).stem)[:60] or label.lower()
    path = job_dir / f"{safe_stem}.{ext}"
    path.write_bytes(raw)
    return str(path)


def save_optional_photo(upload: UploadFile, job_dir: Path, category: str):
    """Validates and saves an optional client-supplied mockup photo, returns its path or None."""
    if upload is None or not upload.filename:
        return None
    raw = upload.file.read()
    if len(raw) > MAX_LOGO_BYTES:
        raise HTTPException(400, f"{category} photo too large (8MB limit).")
    try:
        img = Image.open(__import__("io").BytesIO(raw))
        img.verify()
    except Exception:
        raise HTTPException(400, f"Could not read the {category} photo as a valid image.")
    ext = Path(upload.filename).suffix or ".jpg"
    path = job_dir / f"photo-{category}{ext}"
    path.write_bytes(raw)
    return str(path)


# --- routes ---------------------------------------------------------------

@app.get("/", response_class=HTMLResponse)
def landing():
    return Path("templates/index.html").read_text()


@app.get("/create", response_class=HTMLResponse)
def form():
    return Path("templates/create.html").read_text()


@app.get("/terms", response_class=HTMLResponse)
def terms():
    return Path("templates/terms.html").read_text()


@app.get("/privacy", response_class=HTMLResponse)
def privacy():
    return Path("templates/privacy.html").read_text()


@app.post("/generate")
def generate(
    client_name: str = Form(...),
    logo: UploadFile = File(...),
    logo_mark: UploadFile = File(None),
    industry: str = Form(""),
    type: str = Form(""),
    heading_font: UploadFile = File(None),
    body_font: UploadFile = File(None),
    photo_tee: UploadFile = File(None),
    photo_polo: UploadFile = File(None),
    photo_cap: UploadFile = File(None),
    photo_sleeve: UploadFile = File(None),
    photo_van: UploadFile = File(None),
    photo_sedan: UploadFile = File(None),
    photo_letterhead: UploadFile = File(None),
    photo_business_card: UploadFile = File(None),
    photo_envelope: UploadFile = File(None),
    photo_social_avatar: UploadFile = File(None),
    photo_social_banner: UploadFile = File(None),
    photo_spray_bottle: UploadFile = File(None),
):
    client_name = client_name.strip()
    if not client_name or len(client_name) > 100:
        raise HTTPException(400, "Client name must be 1-100 characters.")

    raw_bytes = logo.file.read()
    validate_logo_upload(logo, raw_bytes)

    job_id = uuid.uuid4().hex[:8]
    slug = slugify(client_name)
    job_dir = WORK_DIR / f"{slug}-{job_id}"
    job_dir.mkdir(parents=True, exist_ok=True)

    logo_ext = Path(logo.filename or "logo.png").suffix or ".png"
    logo_path = job_dir / f"logo{logo_ext}"
    logo_path.write_bytes(raw_bytes)

    logo_mark_path = None
    if logo_mark is not None and logo_mark.filename:
        mark_bytes = logo_mark.file.read()
        validate_logo_upload(logo_mark, mark_bytes)
        mark_ext = Path(logo_mark.filename).suffix or ".png"
        logo_mark_path = job_dir / f"logo-mark{mark_ext}"
        logo_mark_path.write_bytes(mark_bytes)
        logo_mark_path = str(logo_mark_path)

    heading_font_path = save_optional_font(heading_font, job_dir, "Heading")
    body_font_path = save_optional_font(body_font, job_dir, "Body")

    # optional client-supplied photo mockups, keyed by category
    photo_uploads = {
        "tee": photo_tee, "polo": photo_polo, "cap": photo_cap, "sleeve": photo_sleeve,
        "van": photo_van, "sedan": photo_sedan, "letterhead": photo_letterhead,
        "business-card": photo_business_card, "envelope": photo_envelope,
        "social-avatar": photo_social_avatar, "social-banner": photo_social_banner,
        "spray-bottle": photo_spray_bottle,
    }
    saved_photos = {}
    for category, upload in photo_uploads.items():
        saved = save_optional_photo(upload, job_dir, category)
        if saved:
            saved_photos[category] = saved

    industry_filter = set(x.strip() for x in industry.split(",") if x.strip()) or None
    type_filter = set(x.strip() for x in type.split(",") if x.strip()) or None

    output_root = job_dir / "output"
    try:
        results = run_batch(
            str(MOCKUPS_DIR), str(logo_path), str(logo_mark_path or logo_path),
            slug, str(output_root), industry_filter, type_filter,
        )

        # composite the client's own photos over whatever the library produced --
        # same mockup_id, so this simply replaces the flat-vector version with
        # the real photo when one was supplied. "spray-bottle" has no library
        # equivalent, so it's appended fresh if supplied.
        client_output_dir = output_root / slug
        manifest_path = client_output_dir / "manifest.json"
        manifest = json.loads(manifest_path.read_text()) if manifest_path.exists() else []
        manifest_ids = {m["mockup_id"] for m in manifest}

        for category, photo_path in saved_photos.items():
            out_path = client_output_dir / f"{CATEGORY_CONFIG[category]['mockup_id']}.png"
            mockup_id, mtype = composite_photo_mockup(
                photo_path, category, str(logo_path), logo_mark_path, str(out_path)
            )
            if mockup_id not in manifest_ids:
                manifest.append({"mockup_id": mockup_id, "type": mtype, "relpath": f"{mockup_id}.png"})
                manifest_ids.add(mockup_id)

        manifest_path.write_text(json.dumps(manifest, indent=2))

        if not manifest:
            raise HTTPException(400, "No mockups matched the given industry/type filters, and no photos were supplied.")

        manual_html = job_dir / "manual.html"
        render_manual(
            str(TEMPLATE_PATH), str(client_output_dir), client_name,
            str(logo_path), str(manual_html),
            heading_font_path, body_font_path, logo_mark_path,
        )

        manual_pdf = job_dir / "manual.pdf"
        render_pdf(str(manual_html), str(manual_pdf))
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(500, f"Generation failed: {e}")

    return FileResponse(
        path=str(manual_pdf),
        media_type="application/pdf",
        filename=f"{slug}-brand-manual.pdf",
    )


# --- notes for going from "testing" to "real traffic" ---------------------
#
# 1. Synchronous generation blocks the request thread for the full
#    pipeline (image compositing + headless Chromium PDF render).
#    Fine for one person testing; for concurrent users, move /generate
#    to a background job (e.g. Celery/RQ) and poll or webhook for the
#    result instead of blocking the HTTP response.
#
# 2. job_dir folders under jobs/ are never cleaned up here -- add a
#    scheduled sweep (e.g. delete anything older than 24h) before this
#    runs unattended for any length of time.
#
# 3. No auth, no rate limiting, no per-user storage isolation --
#    all fine for a private test deployment, all required before
#    this is a public multi-tenant product.
