#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "openai>=1.50.0",
#     "python-dotenv>=1.0.0",
# ]
# ///
"""
Edit existing images using OpenAI's gpt-image-2 (ChatGPT Images 2.0).

Pass one or more input images. With multiple inputs, gpt-image-2 composes them.

Usage:
    python edit_gpt_image.py input.png "edit instruction" output.png [options]
    python edit_gpt_image.py "Put cat on couch" result.png cat.png couch.png [options]

Examples:
    python edit_gpt_image.py photo.png "Add a rainbow in the sky" edited.png
    python edit_gpt_image.py "Make a group photo" group.png p1.png p2.png p3.png

Environment:
    OPENAI_API_KEY - Required API key
"""

import argparse
import base64
import os
import shutil
import sys
from datetime import datetime
from pathlib import Path

from dotenv import load_dotenv
from openai import OpenAI

load_dotenv(Path.cwd() / ".env")


VALID_QUALITY = ["auto", "low", "medium", "high"]
VALID_FORMATS = ["png", "jpeg", "webp"]
# gpt-image-2 does NOT support "transparent" — only opaque/auto.
VALID_BACKGROUND = ["auto", "opaque"]


def backup_if_exists(output_path: str) -> None:
    """Copy an existing output file into ./backup/ before it gets overwritten.

    Edits often target a path that already holds an image (sometimes the input
    itself), so back the original up first — losing it to an edit is silent and
    unrecoverable. backup/ self-ignores via a backup/.gitignore of "*".
    """
    out = Path(output_path)
    if not out.exists():
        return
    backup_dir = Path.cwd() / "backup"
    backup_dir.mkdir(exist_ok=True)
    gitignore = backup_dir / ".gitignore"
    if not gitignore.exists():
        gitignore.write_text("*\n")
    ts = datetime.now().strftime("%Y%m%d-%H%M%S")
    dest = backup_dir / f"{out.stem}_{ts}{out.suffix}"
    counter = 1
    while dest.exists():
        dest = backup_dir / f"{out.stem}_{ts}_{counter}{out.suffix}"
        counter += 1
    shutil.copy2(out, dest)
    print(f"Backed up existing {output_path} -> {dest}")


def edit_gpt_image(
    input_paths: list[str],
    instruction: str,
    output_path: str,
    model: str = "gpt-image-2",
    size: str = "auto",
    quality: str = "auto",
    output_format: str = "png",
    output_compression: int | None = None,
    mask_path: str | None = None,
    background: str = "auto",
) -> None:
    """Edit/compose images using gpt-image-2."""
    api_key = os.environ.get("OPENAI_API_KEY")
    if not api_key:
        raise EnvironmentError("OPENAI_API_KEY environment variable not set")

    for p in input_paths:
        if not os.path.exists(p):
            raise FileNotFoundError(f"Input image not found: {p}")

    client = OpenAI(api_key=api_key)

    image_files = [open(p, "rb") for p in input_paths]
    try:
        kwargs = {
            "model": model,
            "image": image_files if len(image_files) > 1 else image_files[0],
            "prompt": instruction,
            "size": size,
            "quality": quality,
            "output_format": output_format,
            "background": background,
        }
        if output_compression is not None and output_format in {"jpeg", "webp"}:
            kwargs["output_compression"] = output_compression
        if mask_path:
            if not os.path.exists(mask_path):
                raise FileNotFoundError(f"Mask not found: {mask_path}")
            kwargs["mask"] = open(mask_path, "rb")

        print(f"Model:      {model}")
        print(f"Inputs:     {', '.join(input_paths)}")
        print(f"Size:       {size}")
        print(f"Quality:    {quality}")
        print(f"Format:     {output_format}")
        print(f"Background: {background}")
        print(f"Prompt:     {instruction[:120]}{'...' if len(instruction) > 120 else ''}")
        print()
        print("Editing image...")

        result = client.images.edit(**kwargs)
    finally:
        for f in image_files:
            f.close()
        if mask_path and "mask" in kwargs:
            kwargs["mask"].close()

    item = result.data[0]
    backup_if_exists(output_path)
    Path(output_path).write_bytes(base64.b64decode(item.b64_json))
    print(f"Saved: {output_path}")

    if getattr(result, "usage", None):
        print(f"Usage: {result.usage}")


def main():
    parser = argparse.ArgumentParser(
        description="Edit/compose images using OpenAI gpt-image-2",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument("instruction", help="Edit/compose instruction")
    parser.add_argument("output", help="Output file path")
    parser.add_argument(
        "inputs",
        nargs="+",
        help="One or more input image paths (multiple = composition)",
    )
    parser.add_argument(
        "--model",
        "-m",
        default="gpt-image-2",
        help="Model ID (default: gpt-image-2)",
    )
    parser.add_argument(
        "--size",
        "-s",
        default="auto",
        help="Image size WxH (default: auto). E.g. 1024x1024, 1536x1024, 2048x2048.",
    )
    parser.add_argument(
        "--quality",
        "-q",
        default="auto",
        choices=VALID_QUALITY,
        help="Quality tier (default: auto)",
    )
    parser.add_argument(
        "--format",
        "-f",
        default="png",
        choices=VALID_FORMATS,
        help="Output format (default: png)",
    )
    parser.add_argument(
        "--compression",
        type=int,
        default=None,
        help="Output compression 0-100 (jpeg/webp only)",
    )
    parser.add_argument(
        "--mask",
        default=None,
        help="Optional mask PNG (transparent areas = regions to edit)",
    )
    parser.add_argument(
        "--background",
        default="auto",
        choices=VALID_BACKGROUND,
        help=(
            "Background mode (default: auto). gpt-image-2 supports only "
            "'auto' or 'opaque' — 'transparent' is NOT supported by this model."
        ),
    )

    args = parser.parse_args()

    try:
        edit_gpt_image(
            input_paths=args.inputs,
            instruction=args.instruction,
            output_path=args.output,
            model=args.model,
            size=args.size,
            quality=args.quality,
            output_format=args.format,
            output_compression=args.compression,
            mask_path=args.mask,
            background=args.background,
        )
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
