from __future__ import annotations

import argparse
import base64
from collections import deque
import json
import os
import uuid
from io import BytesIO
from pathlib import Path

import numpy as np
from flask import Flask, abort, jsonify, render_template, request, send_file, url_for
from PIL import Image, ImageOps, UnidentifiedImageError
from rembg import new_session, remove as rembg_remove

from src.perfect_pixel import get_perfect_pixel
from tools.texture_atlas_slicer import slice_atlas


BASE_DIR = Path(__file__).resolve().parent
WEB_DIR = BASE_DIR / "web"
EXPORT_DIR = BASE_DIR / ".local_exports"
ALLOWED_SAMPLE_METHODS = {"center", "majority", "median"}
ATLAS_EXPORTS: dict[str, Path] = {}
ATLAS_IMAGE_EXPORTS: dict[str, Path] = {}
REMBG_SESSION = None

app = Flask(
    __name__,
    template_folder=str(WEB_DIR / "templates"),
    static_folder=str(WEB_DIR / "static"),
)
app.config["MAX_CONTENT_LENGTH"] = 32 * 1024 * 1024


def load_rgba_image(file_storage) -> np.ndarray:
    image = Image.open(file_storage.stream)
    image = ImageOps.exif_transpose(image).convert("RGBA")
    return np.asarray(image, dtype=np.uint8)


def estimate_border_rgba(image_array: np.ndarray) -> np.ndarray:
    border_pixels = np.concatenate(
        [
            image_array[0, :, :],
            image_array[-1, :, :],
            image_array[:, 0, :],
            image_array[:, -1, :],
        ],
        axis=0,
    ).astype(np.float32)
    return np.median(border_pixels, axis=0).astype(np.uint8)


def build_working_rgb(image_rgba: np.ndarray) -> np.ndarray:
    rgb = image_rgba[:, :, :3].copy()
    alpha = image_rgba[:, :, 3]

    if np.any(alpha < 255):
        border_rgba = estimate_border_rgba(image_rgba)
        rgb[alpha == 0] = border_rgba[:3]

    return rgb.astype(np.uint8)


def image_mode_from_array(image_array: np.ndarray) -> str:
    if image_array.ndim == 2:
        return "L"

    channels = image_array.shape[2]
    if channels == 1:
        return "L"
    if channels == 3:
        return "RGB"
    if channels == 4:
        return "RGBA"
    raise ValueError("Unsupported image channel count.")


def resize_alpha_channel(alpha_channel: np.ndarray, width: int, height: int) -> np.ndarray:
    alpha_image = Image.fromarray(alpha_channel.astype(np.uint8), mode="L")
    alpha_image = alpha_image.resize((width, height), resample=Image.Resampling.NEAREST)
    return np.asarray(alpha_image, dtype=np.uint8)


def build_connected_background_alpha(image_rgb: np.ndarray, threshold: int = 18) -> np.ndarray:
    height, width = image_rgb.shape[:2]
    background_rgb = estimate_border_rgba(
        np.dstack([image_rgb, np.full((height, width), 255, dtype=np.uint8)])
    )[:3].astype(np.int16)
    rgb16 = image_rgb.astype(np.int16)
    similar = np.max(np.abs(rgb16 - background_rgb.reshape(1, 1, 3)), axis=2) <= threshold
    visited = np.zeros((height, width), dtype=bool)
    queue: deque[tuple[int, int]] = deque()

    def try_enqueue(x: int, y: int) -> None:
        if visited[y, x] or not similar[y, x]:
            return
        visited[y, x] = True
        queue.append((x, y))

    for x in range(width):
        try_enqueue(x, 0)
        try_enqueue(x, height - 1)

    for y in range(height):
        try_enqueue(0, y)
        try_enqueue(width - 1, y)

    while queue:
        x, y = queue.popleft()
        if x > 0:
            try_enqueue(x - 1, y)
        if x < width - 1:
            try_enqueue(x + 1, y)
        if y > 0:
            try_enqueue(x, y - 1)
        if y < height - 1:
            try_enqueue(x, y + 1)

    alpha = np.full((height, width), 255, dtype=np.uint8)
    alpha[visited] = 0
    return alpha


def detect_content_bounds(
    image_rgba: np.ndarray,
    color_threshold: int = 18,
    alpha_threshold: int = 8,
) -> tuple[int, int, int, int] | None:
    height, width = image_rgba.shape[:2]
    alpha = image_rgba[:, :, 3]

    if np.any(alpha < 250):
        foreground_mask = alpha > alpha_threshold
    else:
        background_rgba = estimate_border_rgba(image_rgba).astype(np.int16)
        rgba16 = image_rgba.astype(np.int16)
        rgb_delta = np.max(
            np.abs(rgba16[:, :, :3] - background_rgba[:3].reshape(1, 1, 3)),
            axis=2,
        )
        alpha_delta = np.abs(rgba16[:, :, 3] - int(background_rgba[3]))
        foreground_mask = (rgb_delta > color_threshold) | (alpha_delta > alpha_threshold)

    points = np.argwhere(foreground_mask)
    if points.size == 0:
        return None

    min_y, min_x = points.min(axis=0)
    max_y, max_x = points.max(axis=0)
    return int(min_x), int(min_y), int(max_x + 1), int(max_y + 1)


def crop_to_content_bounds(image_rgba: np.ndarray) -> tuple[np.ndarray, dict[str, int], bool]:
    height, width = image_rgba.shape[:2]
    bounds = detect_content_bounds(image_rgba)
    default_bounds = {
        "x": 0,
        "y": 0,
        "width": int(width),
        "height": int(height),
    }

    if bounds is None:
        return image_rgba, default_bounds, False

    left, top, right, bottom = bounds
    content_bounds = {
        "x": int(left),
        "y": int(top),
        "width": int(right - left),
        "height": int(bottom - top),
    }

    if left == 0 and top == 0 and right == width and bottom == height:
        return image_rgba, content_bounds, False

    return image_rgba[top:bottom, left:right, :].copy(), content_bounds, True


def build_output_alpha(
    source_rgba: np.ndarray,
    output_rgb: np.ndarray,
    width: int,
    height: int,
    *,
    enabled: bool = True,
) -> tuple[np.ndarray, str]:
    if not enabled:
        return np.full((height, width), 255, dtype=np.uint8), "disabled"

    source_alpha = source_rgba[:, :, 3]

    if np.any(source_alpha < 250):
        return resize_alpha_channel(source_alpha, width, height), "source-alpha"

    return build_connected_background_alpha(output_rgb), "auto-background"


def clamp_refine_intensity(value: str | None) -> float:
    try:
        refine_intensity = float(value or 0.25)
    except ValueError as exc:
        raise ValueError("细化强度必须是数字。") from exc

    return max(0.0, min(0.5, refine_intensity))


def validate_dimension(value: str | None, label: str, max_value: int) -> int:
    try:
        parsed = int(value or "")
    except ValueError as exc:
        raise ValueError(f"{label} 必须是整数。") from exc

    if parsed < 1:
        raise ValueError(f"{label} 必须大于 0。")
    if parsed > max_value:
        raise ValueError(f"{label} 不能大于原图对应尺寸 {max_value}。")

    return parsed


def parse_positive_int(value: str | None, label: str, default: int | None = None) -> int:
    raw = value if value not in (None, "") else default
    if raw is None:
        raise ValueError(f"{label} 不能为空。")

    try:
        parsed = int(raw)
    except ValueError as exc:
        raise ValueError(f"{label} 必须是整数。") from exc

    if parsed <= 0:
        raise ValueError(f"{label} 必须大于 0。")

    return parsed


def parse_non_negative_int(value: str | None, label: str, default: int = 0) -> int:
    raw = value if value not in (None, "") else default

    try:
        parsed = int(raw)
    except ValueError as exc:
        raise ValueError(f"{label} 必须是整数。") from exc

    if parsed < 0:
        raise ValueError(f"{label} 不能小于 0。")

    return parsed


def parse_optional_positive_int(value: str | None, label: str) -> int | None:
    if value in (None, ""):
        return None

    return parse_positive_int(value, label)


def parse_optional_non_negative_float(value: str | None, label: str) -> float | None:
    if value in (None, ""):
        return None

    try:
        parsed = float(value)
    except ValueError as exc:
        raise ValueError(f"{label} 必须是数字。") from exc

    if parsed < 0:
        raise ValueError(f"{label} 不能小于 0。")

    return parsed


def parse_checkbox(value: str | None) -> bool:
    return str(value or "").strip().lower() in {"1", "true", "on", "yes"}


def parse_manual_mode(value: str | None) -> str:
    manual_mode = str(value or "grid").strip().lower().replace("-", "_")
    if manual_mode not in {"grid", "cell_size"}:
        raise ValueError("手动切片模式不支持，请使用按行列切片或按单张尺寸反推。")
    return manual_mode


def parse_export_mode(value: str | None) -> str:
    export_mode = str(value or "original").strip().lower().replace("-", "_")
    if export_mode not in {"original", "pixel"}:
        raise ValueError("导出模式不支持，请使用原图切片或像素结果。")
    return export_mode


def to_png_data_url(image_array: np.ndarray) -> str:
    image = Image.fromarray(image_array.astype(np.uint8), mode=image_mode_from_array(image_array))
    buffer = BytesIO()
    image.save(buffer, format="PNG", optimize=True)
    encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
    return f"data:image/png;base64,{encoded}"


def enforce_output_size(image_array: np.ndarray, width: int, height: int) -> np.ndarray:
    if image_array.shape[1] == width and image_array.shape[0] == height:
        return image_array

    image = Image.fromarray(image_array.astype(np.uint8), mode=image_mode_from_array(image_array))
    image = image.resize((width, height), resample=Image.Resampling.NEAREST)
    return np.asarray(image, dtype=np.uint8)


def safe_filename(stem: str, width: int, height: int) -> str:
    cleaned = "".join(ch for ch in stem if ch.isalnum() or ch in {"-", "_"}).strip("-_")
    cleaned = cleaned or "pixel-art"
    return f"{cleaned}-{width}X{height}.png"


def read_image_bytes(file_storage) -> tuple[bytes, Image.Image]:
    payload = file_storage.read()
    if not payload:
        raise ValueError("上传内容为空。")

    try:
        image = Image.open(BytesIO(payload))
        image.load()
    except (UnidentifiedImageError, OSError) as exc:
        raise ValueError("图片读取失败，请换一张 PNG、JPG 或 WebP 试试。") from exc

    return payload, image


def ensure_export_dir() -> None:
    EXPORT_DIR.mkdir(parents=True, exist_ok=True)


def ensure_rembg_session():
    global REMBG_SESSION
    if REMBG_SESSION is None:
        REMBG_SESSION = new_session("u2netp")
    return REMBG_SESSION


def has_transparent_pixels(image: Image.Image) -> bool:
    if "A" not in image.getbands():
        return False
    alpha = image.getchannel("A")
    return alpha.getextrema()[0] < 255


def build_transparent_filename(source_name: str) -> str:
    stem = Path(source_name or "image").stem
    cleaned = "".join(ch for ch in stem if ch.isalnum() or ch in {"-", "_"}).strip("-_") or "image"
    return f"{cleaned}-transparent.png"


def remove_background_payload(upload) -> dict[str, object]:
    payload, image = read_image_bytes(upload)
    image = ImageOps.exif_transpose(image).convert("RGBA")

    if has_transparent_pixels(image):
        buffer = BytesIO()
        image.save(buffer, format="PNG", optimize=True)
        output_bytes = buffer.getvalue()
        engine = "source-alpha"
    else:
        output_bytes = rembg_remove(payload, session=ensure_rembg_session())
        engine = "rembg-u2netp"

    output_image = Image.open(BytesIO(output_bytes))
    output_image.load()

    return {
        "image_url": f"data:image/png;base64,{base64.b64encode(output_bytes).decode('ascii')}",
        "filename": build_transparent_filename(upload.filename or "image.png"),
        "width": int(output_image.width),
        "height": int(output_image.height),
        "engine": engine,
        "source_width": int(image.width),
        "source_height": int(image.height),
        "has_transparent_background": has_transparent_pixels(output_image.convert("RGBA")),
    }


def build_atlas_slice_namespace(source_path: Path, form) -> argparse.Namespace:
    slice_mode = (form.get("slice_mode") or "auto").strip().lower()
    if slice_mode not in {"auto", "manual"}:
        raise ValueError("切片模式不支持，请使用自动识别或手动切片。")

    manual_mode = parse_manual_mode(form.get("manual_mode"))
    export_mode = parse_export_mode(form.get("export_mode"))
    rows = parse_positive_int(form.get("rows"), "行数", default=8)
    cols = parse_positive_int(form.get("cols"), "列数", default=8)
    cell_width = parse_optional_non_negative_float(form.get("cell_width"), "单格宽度")
    cell_height = parse_optional_non_negative_float(form.get("cell_height"), "单格高度")
    pixel_output_width = parse_optional_positive_int(form.get("pixel_output_width"), "像素结果宽度")
    pixel_output_height = parse_optional_positive_int(form.get("pixel_output_height"), "像素结果高度")
    pixel_sample_method = (form.get("pixel_sample_method") or "center").strip().lower()
    if pixel_sample_method not in ALLOWED_SAMPLE_METHODS:
        raise ValueError("像素结果采样方式不支持。")
    pixel_refine_intensity = clamp_refine_intensity(form.get("pixel_refine_intensity"))

    if slice_mode == "manual" and manual_mode == "cell_size":
        if cell_width in (None, 0) or cell_height in (None, 0):
            raise ValueError("按单张尺寸反推时，请同时填写单格宽度和单格高度。")

    if export_mode == "pixel" and (pixel_output_width is None or pixel_output_height is None):
        raise ValueError("导出像素结果时，请同时填写单张像素结果宽度和高度。")

    return argparse.Namespace(
        source=source_path,
        output_dir=source_path.parent / "slices",
        slice_mode=slice_mode,
        manual_mode=manual_mode,
        export_mode=export_mode,
        pixel_output_width=pixel_output_width,
        pixel_output_height=pixel_output_height,
        pixel_sample_method=pixel_sample_method,
        pixel_refine_intensity=pixel_refine_intensity,
        rows=rows,
        cols=cols,
        origin_x=parse_non_negative_int(form.get("origin_x"), "起始 X"),
        origin_y=parse_non_negative_int(form.get("origin_y"), "起始 Y"),
        region_width=parse_optional_positive_int(form.get("region_width"), "切割区域宽度"),
        region_height=parse_optional_positive_int(form.get("region_height"), "切割区域高度"),
        cell_width=cell_width,
        cell_height=cell_height,
        margin_left=parse_non_negative_int(form.get("margin_left"), "左边距"),
        margin_top=parse_non_negative_int(form.get("margin_top"), "上边距"),
        margin_right=parse_non_negative_int(form.get("margin_right"), "右边距"),
        margin_bottom=parse_non_negative_int(form.get("margin_bottom"), "下边距"),
        spacing_x=parse_non_negative_int(form.get("spacing_x"), "横向间距"),
        spacing_y=parse_non_negative_int(form.get("spacing_y"), "纵向间距"),
        index_base=int(form.get("index_base") or 0),
        visual_boundary_match=parse_checkbox(form.get("visual_boundary_match")),
        visual_search_padding=parse_non_negative_int(form.get("visual_search_padding"), "视觉搜索范围", default=4),
        color_threshold=parse_non_negative_int(form.get("color_threshold"), "颜色阈值", default=18),
        alpha_threshold=parse_non_negative_int(form.get("alpha_threshold"), "透明阈值", default=8),
        auto_close_radius=parse_non_negative_int(form.get("auto_close_radius"), "自动连接半径", default=6),
        auto_min_area=parse_non_negative_int(form.get("auto_min_area"), "自动最小面积", default=24),
        auto_padding=parse_non_negative_int(form.get("auto_padding"), "自动导出留白", default=2),
    )


def create_atlas_job(upload, form) -> dict[str, object]:
    ensure_export_dir()

    payload, image = read_image_bytes(upload)
    job_id = uuid.uuid4().hex
    job_dir = EXPORT_DIR / "atlas" / job_id
    job_dir.mkdir(parents=True, exist_ok=True)

    suffix = Path(upload.filename or "atlas.png").suffix.lower() or ".png"
    if suffix not in {".png", ".jpg", ".jpeg", ".webp"}:
        suffix = ".png"

    source_path = job_dir / f"source{suffix}"
    source_path.write_bytes(payload)

    try:
        args = build_atlas_slice_namespace(source_path, form)
        args.archive_stem = Path(upload.filename or source_path.name).stem
        output_dir, zip_path = slice_atlas(args)
    except Exception:
        if job_dir.exists():
            for child in sorted(job_dir.rglob("*"), reverse=True):
                if child.is_file():
                    child.unlink(missing_ok=True)
                elif child.is_dir():
                    child.rmdir()
            job_dir.rmdir()
        raise

    manifest_path = output_dir / "atlas_slice_manifest.json"
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    config = manifest.get("config", {})
    slice_mode = config.get("slice_mode", args.slice_mode)
    manual_mode = config.get("manual_mode", getattr(args, "manual_mode", "grid"))
    auto_meta = manifest.get("auto_detection") or {}
    widths = sorted({item.get("canvas", {}).get("width", item["box"]["width"]) for item in manifest["slices"]})
    heights = sorted({item.get("canvas", {}).get("height", item["box"]["height"]) for item in manifest["slices"]})

    token = uuid.uuid4().hex
    ATLAS_EXPORTS[token] = zip_path
    processed_image = manifest.get("processed_image") or {}
    processed_image_url = None
    processed_image_name = None
    processed_image_width = None
    processed_image_height = None

    if processed_image.get("file"):
        processed_image_path = output_dir / str(processed_image["file"])
        if processed_image_path.exists():
            image_token = uuid.uuid4().hex
            ATLAS_IMAGE_EXPORTS[image_token] = processed_image_path
            processed_image_url = url_for("download_atlas_image", token=image_token)
            processed_image_name = processed_image_path.name
            processed_image_width = int(processed_image.get("width") or 0)
            processed_image_height = int(processed_image.get("height") or 0)

    return {
        "job_id": job_id,
        "source_width": image.width,
        "source_height": image.height,
        "source_mode": image.mode,
        "slice_mode": slice_mode,
        "manual_mode": manual_mode if slice_mode == "manual" else None,
        "export_mode": config.get("export_mode", getattr(args, "export_mode", "original")),
        "pixel_output_width": config.get("pixel_output_width"),
        "pixel_output_height": config.get("pixel_output_height"),
        "rows": config.get("rows") if slice_mode == "manual" else auto_meta.get("detected_rows"),
        "cols": config.get("cols") if slice_mode == "manual" else auto_meta.get("detected_cols"),
        "slice_count": len(manifest["slices"]),
        "slice_widths": widths,
        "slice_heights": heights,
        "zip_name": zip_path.name,
        "zip_url": url_for("download_atlas_zip", token=token),
        "processed_image_name": processed_image_name,
        "processed_image_url": processed_image_url,
        "processed_image_width": processed_image_width,
        "processed_image_height": processed_image_height,
        "visual_boundary_match": args.visual_boundary_match,
        "auto_detection": auto_meta,
        "first_slice": manifest["slices"][0]["file"] if manifest["slices"] else None,
        "last_slice": manifest["slices"][-1]["file"] if manifest["slices"] else None,
    }


@app.get("/")
def index():
    return render_template("index.html")


@app.get("/transparent-studio")
def transparent_studio():
    return render_template("transparent_studio.html")


@app.errorhandler(413)
def handle_too_large(_error):
    return jsonify({"error": "图片太大了，请换一张 32MB 以内的图片。"}), 413


@app.post("/api/pixelize")
def pixelize():
    upload = request.files.get("image")
    if upload is None or not upload.filename:
        return jsonify({"error": "请先上传一张图片。"}), 400

    try:
        source_rgba = load_rgba_image(upload)
    except (UnidentifiedImageError, OSError):
        return jsonify({"error": "图片读取失败，请换一张 PNG、JPG 或 WebP 试试。"}), 400

    source_height, source_width = source_rgba.shape[:2]
    size_mode = (request.form.get("size_mode") or "manual").strip().lower()
    sample_method = (request.form.get("sample_method") or "center").strip().lower()
    transparent_background_raw = request.form.get("transparent_background")
    transparent_background = (
        True if transparent_background_raw is None else parse_checkbox(transparent_background_raw)
    )
    fit_content_bounds = size_mode == "auto" or parse_checkbox(request.form.get("fit_content_bounds"))

    if sample_method not in ALLOWED_SAMPLE_METHODS:
        return jsonify({"error": "采样方式不支持。"}), 400

    try:
        refine_intensity = clamp_refine_intensity(request.form.get("refine_intensity"))
    except ValueError as exc:
        return jsonify({"error": str(exc)}), 400

    grid_size = None
    fix_square = size_mode == "auto"
    working_source_rgba = source_rgba
    content_bounds = {
        "x": 0,
        "y": 0,
        "width": int(source_width),
        "height": int(source_height),
    }
    content_crop_applied = False

    if fit_content_bounds:
        working_source_rgba, content_bounds, content_crop_applied = crop_to_content_bounds(source_rgba)

    rgb = build_working_rgb(working_source_rgba)

    if size_mode == "manual":
        try:
            output_width = validate_dimension(
                request.form.get("output_width"),
                "输出宽度",
                source_width,
            )
            output_height = validate_dimension(
                request.form.get("output_height"),
                "输出高度",
                source_height,
            )
        except ValueError as exc:
            return jsonify({"error": str(exc)}), 400

        grid_size = (output_width, output_height)

    refined_width, refined_height, output = get_perfect_pixel(
        rgb,
        sample_method=sample_method,
        grid_size=grid_size,
        refine_intensity=refine_intensity,
        fix_square=fix_square,
        debug=False,
    )

    if refined_width is None or refined_height is None:
        return jsonify(
            {
                "error": "生成失败。建议改成手动尺寸，或者换一张更清晰的像素风图片。",
            }
        ), 400

    output = np.asarray(output, dtype=np.uint8)
    if output.ndim == 2:
        output = np.repeat(output[..., None], 3, axis=2)
    if output.shape[-1] > 3:
        output = output[..., :3]
    if size_mode == "manual" and grid_size is not None:
        output = enforce_output_size(output, grid_size[0], grid_size[1])
        refined_width, refined_height = grid_size

    output_alpha, transparency_mode = build_output_alpha(
        working_source_rgba,
        output,
        int(refined_width),
        int(refined_height),
        enabled=transparent_background,
    )
    output_rgba = np.dstack([output, output_alpha])
    visible_pixels = output_rgba[output_rgba[:, :, 3] > 0]
    color_count = int(np.unique(visible_pixels, axis=0).shape[0]) if visible_pixels.size else 0
    filename = safe_filename(Path(upload.filename).stem, refined_width, refined_height)

    return jsonify(
        {
            "image_url": to_png_data_url(output_rgba),
            "filename": filename,
            "width": int(refined_width),
            "height": int(refined_height),
            "source_width": int(source_width),
            "source_height": int(source_height),
            "working_source_width": int(working_source_rgba.shape[1]),
            "working_source_height": int(working_source_rgba.shape[0]),
            "colors": color_count,
            "size_mode": size_mode,
            "sample_method": sample_method,
            "has_transparent_background": bool(np.any(output_alpha == 0)),
            "background_transparency_enabled": transparent_background,
            "transparency_mode": transparency_mode,
            "fit_content_bounds": fit_content_bounds,
            "content_crop_applied": content_crop_applied,
            "content_bounds": content_bounds,
        }
    )


@app.post("/api/slice-atlas")
def slice_atlas_route():
    upload = request.files.get("atlas")
    if upload is None or not upload.filename:
        return jsonify({"error": "请先上传一张图集 PNG。"}), 400

    try:
        payload = create_atlas_job(upload, request.form)
    except ValueError as exc:
        return jsonify({"error": str(exc)}), 400
    except FileNotFoundError as exc:
        return jsonify({"error": str(exc)}), 400
    except Exception as exc:
        return jsonify({"error": f"切片失败：{exc}"}), 500

    return jsonify(payload)


@app.post("/api/background/remove")
def background_remove_route():
    upload = request.files.get("image")
    if upload is None or not upload.filename:
        return jsonify({"error": "请先上传一张图片。"}), 400

    try:
        payload = remove_background_payload(upload)
    except ValueError as exc:
        return jsonify({"error": str(exc)}), 400
    except FileNotFoundError as exc:
        return jsonify({"error": str(exc)}), 400
    except Exception as exc:
        return jsonify({"error": f"自动去背景失败：{exc}"}), 500

    return jsonify(payload)


@app.get("/downloads/atlas/<token>")
def download_atlas_zip(token: str):
    zip_path = ATLAS_EXPORTS.get(token)
    if zip_path is None or not zip_path.exists():
        abort(404)

    return send_file(
        zip_path,
        as_attachment=True,
        download_name=zip_path.name,
        mimetype="application/zip",
    )


@app.get("/downloads/atlas-image/<token>")
def download_atlas_image(token: str):
    image_path = ATLAS_IMAGE_EXPORTS.get(token)
    if image_path is None or not image_path.exists():
        abort(404)

    return send_file(
        image_path,
        as_attachment=True,
        download_name=image_path.name,
        mimetype="image/png",
    )


def main():
    port = int(os.environ.get("PERFECT_PIXEL_WEB_PORT", "5000"))
    print(f"Perfect Pixel Local Studio is running at http://127.0.0.1:{port}")
    app.run(host="127.0.0.1", port=port, debug=False)


if __name__ == "__main__":
    main()
