#!/usr/bin/env python3
"""OpenAI Image Generation Wrapper — Bundle-Skill openai-image-gen.

Endpoints: generate, edit (inkl. Inpainting via Maske).

Sicherheits-Regel: Dieses Skript gibt den API-Key NIEMALS aus (kein print, kein
log, kein Stack-Trace mit dem Wert). Der Key bleibt ausschließlich in der
Subprozess-Umgebung.

Wrapper-Funktion: `generate_image(prompt, output_path, ...)` liefert das
Bundle-weit verbindliche Output-Dict mit vier Pflicht-Schlüsseln:

    {"path": Path, "format": str, "size": tuple[int, int], "prompt_hash": str}
"""

from __future__ import annotations

import argparse
import base64
import hashlib
import os
import sys
from pathlib import Path

# Bundle-Foundation: zentrale API-Key-Lookup-Lib (seit v0.17.3, vereinfacht v0.17.6).
# Seit v0.17.6 sind beide _lib/-Verzeichnisse Namespace Packages — direkter Import
# funktioniert in beiden sys.path-Reihenfolgen, solange .skills/ in sys.path liegt.
_SKILLS_ROOT = Path(__file__).resolve().parent.parent.parent
if str(_SKILLS_ROOT) not in sys.path:
    sys.path.insert(0, str(_SKILLS_ROOT))

from _lib.api_keys import get_api_key  # noqa: E402


# ---------------------------------------------------------------------------
# API-Key-Auflösung
# ---------------------------------------------------------------------------

# OpenAI-spezifischer Placeholder-Filter — Werte mit diesem Prefix sind keine
# echten Keys (typische Anleitungs-Platzhalter wie `OPENAI_API_KEY=sk-...`).
OPENAI_PLACEHOLDER_PREFIXES = ("sk-...",)


def find_api_key(env_var_name: str = "OPENAI_API_KEY") -> str:
    """API-Key-Lookup mit Drei-Stufen-Fallback und sk-...-Placeholder-Filter.

    Konsumiert _lib.api_keys.get_api_key (seit v0.17.3) für die Drei-Brillen-
    Konvergenz-Fixes (UTF-8-BOM, Prefix-Match mit =-Suffix, symmetrischer
    Quote-Strip). Behält den strikten sys.exit(1)-Vertrag für den CLI-Aufrufer
    und den OpenAI-spezifischen Placeholder-Filter.

    Der Key wird NIEMALS ausgegeben — nur die Fund-Quelle als Hinweis auf stderr.
    """
    skill_dir = Path(__file__).resolve().parent.parent
    key, source = get_api_key(env_var_name, skill_dir)

    if key and key.startswith(OPENAI_PLACEHOLDER_PREFIXES):
        # Placeholder ist kein echter Key.
        key, source = None, None

    if key:
        print(f"[API Key] Gefunden via {source}.", file=sys.stderr)
        return key

    skill_env = skill_dir / "assets" / ".env"
    print(
        f"\n[ERROR] {env_var_name} nicht gefunden.\n"
        "Optionen:\n"
        f"  1. Umgebungsvariable setzen: export {env_var_name}=sk-...\n"
        f"  2. In Projekt-.env eintragen: {env_var_name}=sk-...\n"
        f"  3. In Skill-assets/.env eintragen: {skill_env}\n",
        file=sys.stderr,
    )
    sys.exit(1)


# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------


def _compute_prompt_hash(prompt: str) -> str:
    """SHA-256-Hash des Prompts als Hex-String (64 Zeichen)."""
    return hashlib.sha256(prompt.encode("utf-8")).hexdigest()


def _parse_size(size_str: str | None) -> tuple[int, int] | None:
    """Wandelt einen Size-String wie '1024x1024' in ein (Breite, Höhe)-Tupel um."""
    if not size_str or size_str == "auto":
        return None
    parts = size_str.lower().split("x")
    if len(parts) != 2:
        return None
    try:
        return int(parts[0]), int(parts[1])
    except ValueError:
        return None


def _detect_image_size(image_bytes: bytes, fallback: tuple[int, int] | None) -> tuple[int, int]:
    """Liest die Bildgröße aus den PNG/JPEG/WebP-Bytes. Fällt auf fallback zurück."""
    try:
        from io import BytesIO
        from PIL import Image  # type: ignore

        with Image.open(BytesIO(image_bytes)) as img:
            return img.size  # (width, height)
    except Exception:
        if fallback is not None:
            return fallback
        return (0, 0)


# ---------------------------------------------------------------------------
# Wrapper-Funktion (Output-Schema-Pflicht)
# ---------------------------------------------------------------------------


def generate_image(
    prompt: str,
    output_path: Path | str,
    *,
    model: str = "gpt-image-2",
    size: str = "1024x1024",
    quality: str = "medium",
    output_format: str = "png",
    background: str | None = None,
    moderation: str | None = None,
    n: int = 1,
    api_key_env_var: str = "OPENAI_API_KEY",
    client=None,
) -> dict:
    """Erzeugt ein Bild über die OpenAI Image-API und liefert das Pflicht-Output-Dict.

    Output-Dict mit vier Pflicht-Schlüsseln:
        - "path": Path zur erzeugten Bild-Datei (absolut)
        - "format": "png" | "webp" | "jpeg"
        - "size": (Breite, Höhe) in Pixeln
        - "prompt_hash": SHA-256-Hex des Prompts (64 Zeichen)

    Der `client`-Parameter ist optional. Wird er nicht übergeben, wird ein
    neuer `OpenAI`-Client mit dem Key aus der Umgebung gebaut. Dient
    primär dem Mock-Test im Eval-Runner.
    """
    output_path = Path(output_path).resolve()
    output_path.parent.mkdir(parents=True, exist_ok=True)

    if client is None:
        from openai import OpenAI  # lazy import

        # Library-Pfad nutzt die gleiche Wave-I-gehärtete Drei-Stufen-Cascade wie
        # der CLI-Pfad (find_api_key). Vor v0.17.5 lag hier ein direkter
        # os.environ.get-Aufruf, der .env-Fallback, sk-...-Filter und BOM-Fix
        # umging — Library-Konsumenten wären dadurch unbemerkt von den Wave-I-
        # Härtungen ausgenommen gewesen (Brille B HIGH v0.17.5).
        skill_dir = Path(__file__).resolve().parent.parent
        api_key, source = get_api_key(api_key_env_var, skill_dir)
        if api_key and api_key.startswith(OPENAI_PLACEHOLDER_PREFIXES):
            api_key = None
        if not api_key:
            raise RuntimeError(
                f"{api_key_env_var} nicht in der Umgebung gesetzt. Siehe INSTALL.md."
            )
        client = OpenAI(api_key=api_key)

    params: dict = {
        "model": model,
        "prompt": prompt,
        "n": n,
    }
    if size:
        params["size"] = size
    if quality:
        params["quality"] = quality
    if output_format:
        params["output_format"] = output_format
    if background:
        params["background"] = background
    if moderation:
        params["moderation"] = moderation

    # Fix v0.15.1 (F-02/A-04 Befund-Konvergenz Post-Tag): API-Aufruf in
    # try/except, im Fehlerpfad konsistentes vier-Schlüssel-Output-Dict mit
    # path=None statt rohe Exception zu propagieren. Konsistent zu nano-banana-
    # image-Wrapper. Wave-H-Konsumenten müssen path-Check machen, nicht
    # Exception-Handling über Wrapper-Calls duplizieren.
    try:
        result = client.images.generate(**params)
        item = result.data[0]
        image_bytes = base64.b64decode(item.b64_json)
        output_path.write_bytes(image_bytes)

        declared_size = _parse_size(size)
        actual_size = _detect_image_size(image_bytes, declared_size)

        return {
            "path": output_path,
            "format": output_format,
            "size": actual_size,
            "prompt_hash": _compute_prompt_hash(prompt),
        }
    except Exception as exc:
        # Fehler-Typ und Nachricht in stderr loggen (Security-Hinweis: keine
        # Stack-Frames oder Request-Details, die ggf. den API-Key enthalten
        # könnten — nur Typ + str(exc) gekürzt auf 200 Zeichen).
        print(f"API-Fehler: {type(exc).__name__}: {str(exc)[:200]}", file=sys.stderr)
        return {
            "path": None,
            "format": output_format,
            "size": (0, 0),
            "prompt_hash": _compute_prompt_hash(prompt),
        }


# ---------------------------------------------------------------------------
# CLI-Funktionen
# ---------------------------------------------------------------------------


def _save_image(b64_data: str, output_path: str, index: int, n: int) -> Path:
    output_dir = Path("assets/generated_images")
    output_dir.mkdir(parents=True, exist_ok=True)

    p = Path(output_path)
    if n > 1:
        final_path = output_dir / f"{p.stem}_{index + 1}{p.suffix}"
    else:
        final_path = output_dir / p.name

    image_bytes = base64.b64decode(b64_data)
    final_path.write_bytes(image_bytes)
    print(f"[OK] Bild gespeichert: {final_path}", file=sys.stderr)
    return final_path.resolve()


def cmd_generate(args, client) -> None:
    """CLI-Generate: Text → Bild."""
    params: dict = {
        "model": args.model,
        "prompt": args.prompt,
        "n": args.n,
    }
    if args.size:
        params["size"] = args.size
    if args.quality:
        params["quality"] = args.quality
    if args.format:
        params["output_format"] = args.format
    if args.background:
        params["background"] = args.background
    if args.compression is not None:
        params["output_compression"] = args.compression
    if args.moderation:
        params["moderation"] = args.moderation

    print(
        f"[Generate] Modell: {args.model}, Qualität: {args.quality or 'auto'}, "
        f"Größe: {args.size or 'auto'}",
        file=sys.stderr,
    )

    result = client.images.generate(**params)

    saved_paths = []
    for i, item in enumerate(result.data):
        path = _save_image(item.b64_json, args.output, i, args.n)
        saved_paths.append(path)

    if hasattr(result, "usage") and result.usage:
        print(
            f"[Tokens] input: {result.usage.input_tokens}, "
            f"output: {result.usage.output_tokens}",
            file=sys.stderr,
        )

    for p in saved_paths:
        print(p)


def cmd_edit(args, client) -> None:
    """CLI-Edit: Bild + Prompt → neues Bild (auch Inpainting mit Maske)."""
    if not args.image:
        print("[ERROR] --image ist Pflicht für 'edit'.", file=sys.stderr)
        sys.exit(1)

    params: dict = {
        "model": args.model,
        "prompt": args.prompt,
        "n": args.n,
    }

    image_paths = args.image if isinstance(args.image, list) else [args.image]
    opened_files = []

    try:
        image_files = []
        for p in image_paths:
            f = open(p, "rb")
            opened_files.append(f)
            image_files.append(f)

        params["image"] = image_files[0] if len(image_files) == 1 else image_files

        if args.mask:
            mask_file = open(args.mask, "rb")
            opened_files.append(mask_file)
            params["mask"] = mask_file
            print(f"[Inpainting] Maske: {args.mask}", file=sys.stderr)

        if args.size:
            params["size"] = args.size
        if args.quality:
            params["quality"] = args.quality
        if args.format:
            params["output_format"] = args.format
        if args.background:
            params["background"] = args.background
        if args.compression is not None:
            params["output_compression"] = args.compression
        if args.input_fidelity:
            params["input_fidelity"] = args.input_fidelity

        print(f"[Edit] Bilder: {image_paths}, Modell: {args.model}", file=sys.stderr)

        result = client.images.edit(**params)

        saved_paths = []
        for i, item in enumerate(result.data):
            path = _save_image(item.b64_json, args.output, i, args.n)
            saved_paths.append(path)

        if hasattr(result, "usage") and result.usage:
            print(
                f"[Tokens] input: {result.usage.input_tokens}, "
                f"output: {result.usage.output_tokens}",
                file=sys.stderr,
            )

        for p in saved_paths:
            print(p)

    finally:
        for f in opened_files:
            f.close()


def main() -> int:
    parser = argparse.ArgumentParser(
        description="OpenAI Image Generation — generate und edit",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    common = argparse.ArgumentParser(add_help=False)
    common.add_argument("--output", "-o", type=str, default="output.png")
    common.add_argument(
        "--model",
        "-m",
        type=str,
        default="gpt-image-2",
        choices=["gpt-image-2", "gpt-image-1.5", "gpt-image-1", "gpt-image-1-mini"],
    )
    common.add_argument("--n", type=int, default=1)
    common.add_argument(
        "--size",
        type=str,
        choices=["1024x1024", "1536x1024", "1024x1536", "auto"],
    )
    common.add_argument(
        "--quality",
        "-q",
        type=str,
        choices=["low", "medium", "high", "auto"],
    )
    common.add_argument(
        "--format",
        "-f",
        type=str,
        choices=["png", "jpeg", "webp"],
        default="png",
    )
    common.add_argument(
        "--background",
        "-b",
        type=str,
        choices=["opaque", "transparent", "auto"],
    )
    common.add_argument("--compression", type=int)
    common.add_argument("--moderation", type=str, choices=["auto", "low"])
    common.add_argument(
        "--api-key-env-var",
        type=str,
        default="OPENAI_API_KEY",
        help="Name der Umgebungsvariable mit dem API-Key (Default: OPENAI_API_KEY)",
    )

    gen_parser = subparsers.add_parser("generate", parents=[common])
    gen_parser.add_argument("--prompt", "-p", type=str, required=True)

    edit_parser = subparsers.add_parser("edit", parents=[common])
    edit_parser.add_argument("--prompt", "-p", type=str, required=True)
    edit_parser.add_argument("--image", "-i", type=str, nargs="+", required=True)
    edit_parser.add_argument("--mask", type=str)
    edit_parser.add_argument(
        "--input-fidelity",
        type=str,
        choices=["low", "high"],
    )

    args = parser.parse_args()

    try:
        from openai import OpenAI
    except ImportError:
        print(
            "[ERROR] openai-Library nicht installiert. Install: pip install openai",
            file=sys.stderr,
        )
        return 1

    api_key = find_api_key(args.api_key_env_var)
    client = OpenAI(api_key=api_key)

    import openai

    try:
        if args.command == "generate":
            cmd_generate(args, client)
        elif args.command == "edit":
            cmd_edit(args, client)
    except openai.AuthenticationError:
        print("[ERROR] API Key ungültig oder nicht gefunden.", file=sys.stderr)
        return 1
    except openai.RateLimitError:
        print("[ERROR] Rate-Limit erreicht. Warten und erneut versuchen.", file=sys.stderr)
        return 1
    except openai.BadRequestError as e:
        print(f"[ERROR] Request abgelehnt: {e}", file=sys.stderr)
        return 1
    except openai.PermissionDeniedError:
        print("[ERROR] Permission denied. Organisation verifiziert?", file=sys.stderr)
        return 1
    except openai.APIConnectionError:
        print("[ERROR] Netzwerk-Fehler — OpenAI-API nicht erreichbar.", file=sys.stderr)
        return 1
    except openai.APITimeoutError:
        print("[ERROR] Request-Timeout. Qualität oder Größe reduzieren.", file=sys.stderr)
        return 1

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
