"""
Image generation using Google Gemini 2.5 Flash Image Preview API.
Supports multi-image input, custom prompts, and automatic upscaling integration.
Provides both CLI interface and programmatic API for image generation workflows.
"""

__author__ = "Lene Preuss <lene.preuss@gmail.com>"

import argparse
import datetime
import logging
import shutil
import sys
from io import BytesIO
from pathlib import Path
from typing import Any, List, Optional, Tuple, TYPE_CHECKING

# Coloredlogs handled in setup_logging() from utils

from google.genai.types import GenerateContentResponse
from PIL import Image

from stable_delusion.exceptions import ImageGenerationError, FileOperationError
from stable_delusion import builders
from stable_delusion.client.gemini_client import GeminiClient
from stable_delusion.generate.generation_config import GenerationConfig
from stable_delusion.models.client_config import GeminiClientConfig
from stable_delusion.utils import (
    generate_timestamped_filename,
    setup_logging,
)

if TYPE_CHECKING:
    from stable_delusion.models.requests import GenerateImageRequest
    from stable_delusion.models.responses import GenerateImageResponse
    from stable_delusion.providers.image_provider import ImageProvider
    from stable_delusion.providers.types import ImageGenParams
    from stable_delusion.project.context import ProjectContext
    from stable_delusion.project.models import ProjectMetadata, SceneMetadata

DEFAULT_PROMPT = "A futuristic cityscape with flying cars at sunset"
DEFAULT_IMAGE_SIZE = "2K"

# Coloredlogs will be configured in main() after parsing arguments


def log_failure_reason(response: GenerateContentResponse) -> None:
    logging.error("No candidates returned from the API.")
    # Check prompt feedback for safety filtering
    if hasattr(response, "prompt_feedback") and response.prompt_feedback:
        feedback = response.prompt_feedback
        if hasattr(feedback, "block_reason"):
            logging.error("Prompt blocked: %s", feedback.block_reason)
        if hasattr(feedback, "safety_ratings") and feedback.safety_ratings:
            for rating in feedback.safety_ratings:
                logging.error("Safety rating: %s = %s", rating.category, rating.probability)
    # Log usage metadata if available
    if hasattr(response, "usage_metadata") and response.usage_metadata:
        logging.error("Usage metadata: %s", response.usage_metadata)
    # Log any other response properties that might give clues
    logging.error("Response type: %s", type(response))
    logging.error(
        "Response attributes: %s", [attr for attr in dir(response) if not attr.startswith("_")]
    )


def _validate_and_normalize_output_filename(filename: str) -> str:
    """
    Validate and normalize the output filename according to PNG requirements.

    Args:
        filename: The filename provided by user

    Returns:
        Normalized filename (basename without .png extension)

    Raises:
        SystemExit: If the file extension is not supported
    """
    if not filename:
        return filename

    # Convert to Path for easier extension handling
    path = Path(filename)

    # Get the extension (lowercase for comparison)
    extension = path.suffix.lower()

    if extension == ".png":
        # Strip .png extension but preserve directory path
        return str(path.with_suffix(""))
    if extension == "":
        # No extension - keep as is
        return filename

    # Any other extension is not supported
    print(
        f"Error: file type not supported for --output-filename: '{extension}'. "
        "Only PNG files are supported."
    )
    sys.exit(1)


def _add_common_args(parser: argparse.ArgumentParser) -> None:
    """Add arguments shared across subcommands."""
    parser.add_argument(
        "-q", "--quiet", action="store_true", help="Quiet mode - only show warnings and errors."
    )
    parser.add_argument(
        "-d",
        "--debug",
        action="store_true",
        help="Debug mode - show all log messages including debug details.",
    )
    parser.add_argument(
        "--output-dir",
        type=Path,
        default=Path("."),
        help="Directory where generated files will be saved (default: cwd).",
    )
    parser.add_argument(
        "--storage-type",
        type=str,
        choices=["local", "s3"],
        help="Storage backend: 'local' or 's3'.",
    )


def _add_image_generate_args(parser: argparse.ArgumentParser) -> None:
    """Add image-generate-specific arguments."""
    parser.add_argument("--prompt", type=str, help="The prompt text for image generation.")
    parser.add_argument(
        "--output-filename",
        type=Path,
        default=None,
        help="Output filename base (without timestamp/extension).",
    )
    parser.add_argument(
        "--image", type=Path, action="append", help="Path to a reference image. Can be repeated."
    )
    parser.add_argument("--gcp-project-id", type=str, help="Google Cloud Project ID.")
    parser.add_argument("--gcp-location", type=str, help="Google Cloud region.")
    parser.add_argument(
        "--scale", type=int, choices=[2, 4], help="Upscale factor: 2 or 4 (Gemini only)."
    )
    parser.add_argument(
        "--size", type=str, help="Image size (Seedream only): '1K', '2K', '4K', or 'WIDTHxHEIGHT'."
    )
    parser.add_argument(
        "--model",
        type=str,
        choices=["gemini", "seedream"],
        help="AI model for image generation (default: gemini).",
    )
    parser.add_argument("--gemini-api-key", type=str, help="Gemini API key.")
    parser.add_argument("--aws-s3-bucket", type=str, help="AWS S3 bucket name.")
    parser.add_argument("--aws-s3-region", type=str, help="AWS S3 region.")
    parser.add_argument("--aws-access-key-id", type=str, help="AWS access key ID.")
    parser.add_argument("--aws-secret-access-key", type=str, help="AWS secret access key.")
    parser.add_argument("--upload-folder", type=Path, help="Directory for uploaded files.")
    parser.add_argument("--default-output-dir", type=Path, help="Default output directory.")
    parser.add_argument("--flask-debug", action="store_true", help="Enable Flask debug mode.")
    parser.add_argument(
        "--scene",
        type=str,
        default=None,
        help="Scene name to target (activates project mode).",
    )
    parser.add_argument(
        "--count",
        type=int,
        default=1,
        help="Number of images to generate (project mode, default: 1).",
    )
    parser.add_argument(
        "--group-name",
        type=str,
        default=None,
        help="Name for the keyframe group (auto-generated if omitted).",
    )
    parser.add_argument(
        "--provider",
        type=str,
        default=None,
        help="Image provider: 'gemini' or 'seedream' (default: project default).",
    )


def _add_video_generate_args(parser: argparse.ArgumentParser) -> None:
    """Add video-generate-specific arguments."""
    parser.add_argument("--prompt", type=str, help="The prompt text for video generation.")
    parser.add_argument(
        "--keyframes",
        type=str,
        nargs="+",
        help="Paths to keyframe images (first and optionally last frame).",
    )
    parser.add_argument(
        "--duration",
        type=int,
        default=8,
        choices=[4, 8, 12],
        help="Video duration in seconds (default: 8).",
    )
    parser.add_argument(
        "--provider", type=str, default="seedance", help="Video provider (default: seedance)."
    )
    parser.add_argument("--ratio", type=str, default="16:9", help="Aspect ratio (default: 16:9).")
    parser.add_argument("--audio", action="store_true", help="Generate audio along with video.")
    parser.add_argument(
        "--no-last-frame", action="store_true", help="Don't return last frame for chaining."
    )


def _add_video_status_args(parser: argparse.ArgumentParser) -> None:
    """Add video-status-specific arguments."""
    parser.add_argument(
        "--task-id",
        type=str,
        default=None,
        help="Specific task ID to check (default: check all pending).",
    )
    parser.add_argument(
        "--watch", action="store_true", help="Continuously poll until all tasks complete."
    )


def _setup_subcommand_parser() -> argparse.ArgumentParser:
    """Create the CLI parser with image/video subcommands."""
    parser = argparse.ArgumentParser(
        description="stable-delusion: AI image and video generation.", prog="hallucinate"
    )
    subparsers = parser.add_subparsers(dest="command")

    image_parser = subparsers.add_parser("image", help="Image generation commands")
    image_subs = image_parser.add_subparsers(dest="subcommand")
    image_gen = image_subs.add_parser("generate", help="Generate images from prompts")
    _add_common_args(image_gen)
    _add_image_generate_args(image_gen)

    video_parser = subparsers.add_parser("video", help="Video generation commands")
    video_subs = video_parser.add_subparsers(dest="subcommand")
    video_gen = video_subs.add_parser("generate", help="Generate video from keyframes")
    _add_common_args(video_gen)
    _add_video_generate_args(video_gen)
    video_status = video_subs.add_parser("status", help="Check video generation task status")
    _add_common_args(video_status)
    _add_video_status_args(video_status)
    video_download = video_subs.add_parser("download", help="Download completed video")
    _add_common_args(video_download)
    video_download.add_argument("task_id", type=str, help="Task ID to download")

    # project commands
    project_parser = subparsers.add_parser("project", help="Project management commands")
    project_subs = project_parser.add_subparsers(dest="subcommand")

    project_new = project_subs.add_parser("new", help="Create a new project")
    project_new.add_argument("name", type=str, help="Project name")
    project_new.add_argument(
        "--aspect-ratio", type=str, default="16:9", help="Aspect ratio (default: 16:9)"
    )
    project_new.add_argument("--description", type=str, default="", help="Project description")
    project_new.add_argument(
        "--dir", type=Path, default=None, help="Projects root directory override"
    )

    project_list = project_subs.add_parser("list", help="List all projects")
    project_list.add_argument(
        "--dir", type=Path, default=None, help="Projects root directory override"
    )

    project_info = project_subs.add_parser("info", help="Show project details")
    project_info.add_argument("name", type=str, nargs="?", default=None, help="Project name")
    project_info.add_argument(
        "--dir", type=Path, default=None, help="Projects root directory override"
    )

    project_use = project_subs.add_parser("use", help="Set active project")
    project_use.add_argument("name", type=str, help="Project name to activate")
    project_use.add_argument(
        "--dir", type=Path, default=None, help="Projects root directory override"
    )

    # scene commands
    scene_parser = subparsers.add_parser("scene", help="Scene management commands")
    scene_subs = scene_parser.add_subparsers(dest="subcommand")

    scene_new = scene_subs.add_parser("new", help="Create a new scene in the active project")
    scene_new.add_argument("name", type=str, help="Scene name")
    scene_new.add_argument("--description", type=str, default="", help="Scene description")
    scene_new.add_argument(
        "--project", type=str, default=None, help="Project name (default: active project)"
    )
    scene_new.add_argument(
        "--dir", type=Path, default=None, help="Projects root directory override"
    )

    scene_list = scene_subs.add_parser("list", help="List scenes in the active project")
    scene_list.add_argument(
        "--project", type=str, default=None, help="Project name (default: active project)"
    )
    scene_list.add_argument(
        "--dir", type=Path, default=None, help="Projects root directory override"
    )

    scene_info = scene_subs.add_parser("info", help="Show scene details")
    scene_info.add_argument("name", type=str, help="Scene name")
    scene_info.add_argument(
        "--project", type=str, default=None, help="Project name (default: active project)"
    )
    scene_info.add_argument(
        "--dir", type=Path, default=None, help="Projects root directory override"
    )

    return parser


def _is_legacy_invocation(argv: List[str]) -> bool:
    """Return True if argv looks like a legacy (pre-subcommand) invocation."""
    if not argv:
        return True
    return argv[0].startswith("-")


def _setup_cli_argument_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Generate an image using the Gemini API.")
    parser.add_argument(
        "-q", "--quiet", action="store_true", help="Quiet mode - only show warnings and errors."
    )
    parser.add_argument(
        "-d",
        "--debug",
        action="store_true",
        help="Debug mode - show all log messages including debug details.",
    )
    parser.add_argument("--prompt", type=str, help="The prompt text for image generation.")
    parser.add_argument(
        "--output-filename",
        type=Path,
        default=None,
        help="The output filename base (without timestamp/extension). "
        "If not specified, model-specific defaults are used "
        "(gemini: 'generated', seedream: 'seedream_image').",
    )
    parser.add_argument(
        "--image",
        type=Path,
        action="append",
        help="Path to a reference image. Can be repeated.",
    )
    parser.add_argument(
        "--gcp-project-id",
        type=str,
        help="Google Cloud Project ID (defaults to value in conf.py).",
    )
    parser.add_argument(
        "--gcp-location",
        type=str,
        help="Google Cloud region (defaults to value in conf.py).",
    )
    parser.add_argument(
        "--scale",
        type=int,
        choices=[2, 4],
        help="Upscale factor: 2 or 4 (optional, Gemini only).",
    )
    parser.add_argument(
        "--size",
        type=str,
        help="Image size for generation (optional, Seedream only). "
        "Can be '1K', '2K', '4K', or '{width}x{height}' "
        "where width is 1280-4096 and height is 720-4096. Examples: '2K', '1920x1080'.",
    )
    parser.add_argument(
        "--output-dir",
        type=Path,
        default=Path("."),
        help="Directory where generated files will be saved " "(default: current directory).",
    )
    parser.add_argument(
        "--storage-type",
        type=str,
        choices=["local", "s3"],
        help="Storage backend: 'local' for local filesystem or 's3' for AWS S3 "
        "(overrides configuration file setting).",
    )
    parser.add_argument(
        "--model",
        type=str,
        choices=["gemini", "seedream"],
        help="AI model to use for image generation: 'gemini' for Gemini 2.5 Flash "
        "or 'seedream' for SeeEdit Seedream 4.0 (defaults to 'gemini').",
    )

    # Authentication parameters
    parser.add_argument(
        "--gemini-api-key",
        type=str,
        help="Gemini API key (WARNING: visible in process list - prefer environment variable).",
    )

    # AWS S3 parameters
    parser.add_argument(
        "--aws-s3-bucket",
        type=str,
        help="AWS S3 bucket name (required when using S3 storage).",
    )
    parser.add_argument(
        "--aws-s3-region",
        type=str,
        help="AWS S3 region (required when using S3 storage).",
    )
    parser.add_argument(
        "--aws-access-key-id",
        type=str,
        help="AWS access key ID (WARNING: visible in process list - prefer environment variable).",
    )
    parser.add_argument(
        "--aws-secret-access-key",
        type=str,
        help="AWS secret access key (WARNING: visible in process list - "
        "prefer environment variable).",
    )

    # Flask/Application parameters
    parser.add_argument(
        "--upload-folder",
        type=Path,
        help="Directory for uploaded files (used by Flask API).",
    )
    parser.add_argument(
        "--default-output-dir",
        type=Path,
        help="Default output directory for generated images.",
    )
    parser.add_argument(
        "--flask-debug",
        action="store_true",
        help="Enable Flask debug mode.",
    )
    return parser


def _validate_s3_storage_arguments(
    args: argparse.Namespace, parser: argparse.ArgumentParser
) -> None:
    if args.storage_type == "s3":
        import os

        bucket = args.aws_s3_bucket or os.getenv("AWS_S3_BUCKET")
        region = args.aws_s3_region or os.getenv("AWS_S3_REGION")

        if not bucket or not region:
            parser.error(
                "When using --storage-type s3, bucket and region are required: "
                "--aws-s3-bucket and --aws-s3-region or set AWS_S3_BUCKET and "
                "AWS_S3_REGION environment variables. "
                "Credentials will be read from AWS credential chain "
                "(~/.aws/credentials, environment variables, IAM roles)."
            )


def _warn_about_sensitive_cli_parameters(args: argparse.Namespace) -> None:
    if args.gemini_api_key:
        logging.warning(
            "API key passed via command line is visible in process list. "
            "Consider using GEMINI_API_KEY environment variable instead."
        )

    if args.aws_access_key_id or args.aws_secret_access_key:
        logging.warning(
            "AWS credentials passed via command line are visible in process list. "
            "Consider using AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY "
            "environment variables instead."
        )


def parse_command_line(argv: Optional[List[str]] = None) -> argparse.Namespace:
    """Parse CLI arguments, supporting both subcommands and legacy flat syntax."""
    if argv is None:
        argv = sys.argv[1:]

    if _is_legacy_invocation(argv):
        parser = _setup_cli_argument_parser()
        args = parser.parse_args(argv)

        # Load .env file before validation to ensure environment variables are available
        from dotenv import load_dotenv

        load_dotenv(override=False)

        _validate_s3_storage_arguments(args, parser)
        _warn_about_sensitive_cli_parameters(args)

        args.command = "image"
        args.subcommand = "generate"
        return args

    parser = _setup_subcommand_parser()
    args = parser.parse_args(argv)

    if args.command is None:
        parser.print_help()
        sys.exit(1)

    return args


def generate_from_images(
    prompt_text: str, image_paths: List[Path], config: Optional[GenerationConfig] = None
) -> Optional[Path]:
    if config is None:
        config = GenerationConfig()

    from stable_delusion.models.client_config import GCPConfig, StorageConfig

    client_config = GeminiClientConfig(
        gcp=GCPConfig(project_id=config.project_id, location=config.location),
        storage=StorageConfig(output_dir=config.output_dir, storage_type=config.storage_type),
    )
    client = GeminiClient(client_config)
    return client.generate_from_images(prompt_text, image_paths)


def save_response_image(
    response: GenerateContentResponse, output_dir: Path = Path(".")
) -> Optional[Path]:
    if not response.candidates:
        logging.warning("No candidates found in the API response.")
        raise ImageGenerationError(
            "No candidates returned from the API", api_response=str(response)
        )

    candidate = response.candidates[0]
    if not candidate.content or not candidate.content.parts:
        logging.warning("No content parts found in the API response.")
        raise ImageGenerationError("No content parts in the candidate", api_response=str(candidate))

    for part in candidate.content.parts:
        if part.text is not None:
            logging.debug("Response text: %s", part.text)
        elif part.inline_data is not None and part.inline_data.data is not None:
            image = Image.open(BytesIO(part.inline_data.data))
            filename = generate_timestamped_filename("generated")
            filepath = output_dir / filename
            image.save(str(filepath))
            return filepath
    logging.warning("No image found in the API response.")
    return None


def _process_cli_arguments() -> Tuple[str, List[Path], argparse.Namespace]:
    args = parse_command_line()
    prompt = args.prompt if args.prompt else DEFAULT_PROMPT
    images = args.image if args.image else []
    return prompt, images, args


def _create_cli_request_dto(
    prompt: str, images: List[Path], args: argparse.Namespace
) -> "GenerateImageRequest":
    from stable_delusion.models.requests import GenerateImageRequest

    # Validate and normalize output filename if provided (None means use model defaults)
    output_filename = getattr(args, "output_filename")
    if output_filename is not None:
        output_filename = Path(_validate_and_normalize_output_filename(str(output_filename)))

    return GenerateImageRequest(
        prompt=prompt,
        images=images,
        project_id=getattr(args, "gcp_project_id"),
        location=getattr(args, "gcp_location"),
        output_dir=getattr(args, "output_dir"),
        output_filename=output_filename,
        scale=getattr(args, "scale"),
        image_size=getattr(args, "size"),
        storage_type=getattr(args, "storage_type"),
        model=getattr(args, "model"),
    )


def _execute_image_generation(
    request_dto: "GenerateImageRequest",
) -> "GenerateImageResponse":
    service = builders.create_image_generation_service(
        project_id=request_dto.project_id,
        location=request_dto.location,
        output_dir=request_dto.output_dir,
        storage_type=request_dto.storage_type,
        model=request_dto.model,
    )
    return service.generate_image(request_dto)


def _handle_cli_custom_output(
    response: "GenerateImageResponse", request_dto: "GenerateImageRequest"
) -> None:
    if response.generated_file and request_dto.output_filename:
        # Generate timestamped filename with .png extension
        custom_filename = generate_timestamped_filename(
            str(request_dto.output_filename), extension="png"
        )

        logging.debug(
            "Custom output: attempting to rename %s to %s",
            response.generated_file,
            custom_filename,
        )
        logging.debug("Source file exists: %s", response.generated_file.exists())

        # If output_dir is specified, use it; otherwise use the same directory as the source file
        if request_dto.output_dir:
            custom_path = request_dto.output_dir / custom_filename
        else:
            custom_path = response.generated_file.parent / custom_filename

        logging.debug("Target path: %s", custom_path)

        try:
            # Ensure target directory exists
            custom_path.parent.mkdir(parents=True, exist_ok=True)

            # Move the file (handles cross-device links)
            shutil.move(str(response.generated_file), str(custom_path))
            response.image_config.generated_file = custom_path
            logging.debug("Successfully renamed to: %s", custom_path)
        except (OSError, FileNotFoundError, PermissionError, shutil.Error) as e:
            logging.error("Failed to rename file: %s", e)
            logging.debug(
                "Source path: %s (exists: %s)",
                response.generated_file,
                response.generated_file.exists(),
            )
            logging.debug("Target path: %s", custom_path)


def _log_generation_result(response: "GenerateImageResponse", args: argparse.Namespace) -> None:
    if response.generated_file:
        if args.scale:
            logging.info("High Res Image saved to %s", response.generated_file)
        else:
            logging.debug("Image saved to %s", response.generated_file)
    else:
        logging.error("Image generation failed.")


def _make_image_provider(provider_name: str, output_dir: Path) -> "ImageProvider":
    from stable_delusion.providers.gemini_provider import GeminiProvider
    from stable_delusion.providers.seedream_provider import SeedreamProvider

    if provider_name == "gemini":
        return GeminiProvider(output_dir=output_dir)
    if provider_name == "seedream":
        return SeedreamProvider(output_dir=output_dir)
    print(f"Error: unknown provider '{provider_name}'. Use 'gemini' or 'seedream'.")
    sys.exit(1)


class _KeyframeWriteConfig:
    """Groups write-destination parameters for keyframe generation."""

    def __init__(self, count: int, group_dir: Path) -> None:
        self.count = count
        self.group_dir = group_dir


def _generate_and_write_keyframes(
    provider: "ImageProvider",
    prompt: str,
    source_images: list,
    params: "ImageGenParams",
    write_config: _KeyframeWriteConfig,
) -> tuple:
    """Returns (generated_count, total_cost, model_name)."""
    import hashlib

    from stable_delusion.project.keyframe_sidecar_repository import KeyframeSidecarRepository
    from stable_delusion.project.models import GroupCost, KeyframeSidecar

    sidecar_repo = KeyframeSidecarRepository()
    total_cost = 0.0
    generated_count = 0
    last_result = None

    for i in range(1, write_config.count + 1):
        last_result = provider.generate(prompt, source_images, params)
        for generated_path in last_result.image_paths:
            suffix = generated_path.suffix or ".png"
            dest = write_config.group_dir / f"{i:02d}{suffix}"
            if generated_path.exists() and generated_path != dest:
                generated_path.rename(dest)
            content_hash = hashlib.sha256(dest.read_bytes()).hexdigest()
            with Image.open(dest) as img:
                width, height = img.size
            sidecar = KeyframeSidecar(
                created_at=datetime.datetime.now(datetime.timezone.utc),
                index=i,
                cost=GroupCost(amount=last_result.cost.amount, currency=last_result.cost.currency),
                content_hash=content_hash,
                aspect_ratio=params.aspect_ratio or "16:9",
                resolution=[width, height],
            )
            sidecar_repo.write(dest, sidecar)
            total_cost += last_result.cost.amount
            generated_count += 1

    model_name = last_result.cost.model if last_result else ""
    return generated_count, total_cost, model_name


def _update_scene_after_generation(
    scene_meta: "SceneMetadata",
    scene_dir: Path,
    generated_count: int,
    total_cost: float,
) -> None:
    from stable_delusion.project.scene_repository import SceneRepository

    scene_meta.keyframe_count += generated_count
    if scene_meta.status == "empty":
        scene_meta.status = "has-keyframes"
    scene_meta.updated_at = datetime.datetime.now(datetime.timezone.utc)
    scene_meta.cost.amount += total_cost
    scene_meta.cost.image_generation += total_cost
    SceneRepository().update(scene_dir, scene_meta)


def _update_project_after_generation(
    project_meta: "ProjectMetadata",
    project_dir: Path,
    total_cost: float,
) -> None:
    from stable_delusion.project.project_repository import ProjectRepository

    project_meta.updated_at = datetime.datetime.now(datetime.timezone.utc)
    project_meta.total_cost.amount += total_cost
    project_meta.total_cost.image_generation += total_cost
    ProjectRepository().update(project_dir, project_meta)


def _handle_image_generate_project(args: argparse.Namespace, context: "ProjectContext") -> None:
    from stable_delusion.project.keyframe_group_repository import KeyframeGroupRepository
    from stable_delusion.project.models import GroupCost, KeyframeGroupMetadata
    from stable_delusion.providers.types import ImageGenParams

    prompt = args.prompt or ""
    if not prompt:
        print("Error: --prompt is required in project mode.")
        sys.exit(1)

    count = getattr(args, "count", 1)
    provider_name = getattr(args, "provider", None) or context.project_meta.default_provider.image
    source_images: list[Path] = getattr(args, "image", None) or []
    group_name_arg = getattr(args, "group_name", None)

    group_repo = KeyframeGroupRepository()
    group_index = group_repo.next_index(context.scene_dir)
    group_name = group_name_arg or f"group-{group_index}"
    now = datetime.datetime.now(datetime.timezone.utc)

    initial_group_meta = KeyframeGroupMetadata(
        name=group_name,
        index=group_index,
        created_at=now,
        prompt=prompt,
        source_images=[str(p) for p in source_images],
        provider=provider_name,
    )
    group_dir = group_repo.create(context.scene_dir, group_name, initial_group_meta)

    provider = _make_image_provider(provider_name, group_dir)
    params = ImageGenParams(
        image_size=getattr(args, "size", None) or DEFAULT_IMAGE_SIZE,
        aspect_ratio=context.project_meta.aspect_ratio,
    )

    write_config = _KeyframeWriteConfig(count=count, group_dir=group_dir)
    generated_count, total_cost, model_name = _generate_and_write_keyframes(
        provider, prompt, source_images, params, write_config
    )

    updated_group_meta = KeyframeGroupMetadata(
        name=group_name,
        index=group_index,
        created_at=now,
        prompt=prompt,
        source_images=[str(p) for p in source_images],
        provider=provider_name,
        model=model_name or provider_name,
        total_generated=generated_count,
        selected_count=generated_count,
        cost=GroupCost(amount=total_cost, currency="USD"),
    )
    group_repo.update(group_dir, updated_group_meta)

    _update_scene_after_generation(
        context.scene_meta, context.scene_dir, generated_count, total_cost
    )
    _update_project_after_generation(context.project_meta, context.project_dir, total_cost)

    print(
        f"Generated {generated_count} keyframe(s) in group '{group_name}' "
        f"(scene '{context.scene_meta.name}')"
    )
    print(f"Cost: ${total_cost:.4f} USD")


def _handle_project_command(args: argparse.Namespace) -> None:
    """Dispatch project subcommands."""
    from stable_delusion.project.xdg_paths import resolve_projects_dir
    from stable_delusion.project.models import ProjectMetadata
    from stable_delusion.project.project_repository import ProjectRepository
    from stable_delusion.project.active_project import get_active_project, set_active_project

    projects_dir = resolve_projects_dir(getattr(args, "dir", None))
    repo = ProjectRepository()

    if args.subcommand == "new":
        metadata = ProjectMetadata.create_new(
            name=args.name,
            aspect_ratio=args.aspect_ratio,
            description=args.description,
        )
        project_dir = repo.create(projects_dir, args.name, metadata)
        print(f"Created project '{args.name}' at {project_dir}")

    elif args.subcommand == "list":
        projects = repo.list_projects(projects_dir)
        if not projects:
            print("No projects found.")
        else:
            active = get_active_project()
            active_name = active[0] if active else None
            for path, meta in projects:
                marker = " (active)" if meta.name == active_name else ""
                print(f"  {meta.name}{marker} — {path}")

    elif args.subcommand == "info":
        name = args.name
        if name is None:
            active = get_active_project()
            if active is None:
                print("No active project. Use 'hallucinate project use <name>' to set one.")
                sys.exit(1)
            project_dir, meta = active[1], repo.get(active[1])
        else:
            project_dir, meta = repo.find_by_name(projects_dir, name)
        print(f"Name:        {meta.name}")
        print(f"Path:        {project_dir}")
        print(f"Aspect:      {meta.aspect_ratio}")
        print(f"Description: {meta.description or '(none)'}")
        print(f"Scenes:      {len(meta.scenes)}")
        print(f"Total cost:  {meta.total_cost.amount:.4f} {meta.total_cost.currency}")
        print(f"Created:     {meta.created_at.strftime('%Y-%m-%d %H:%M')}")

    elif args.subcommand == "use":
        project_dir, meta = repo.find_by_name(projects_dir, args.name)
        set_active_project(meta.name, project_dir)
        print(f"Active project set to '{meta.name}'")

    else:
        print("Usage: hallucinate project [new|list|info|use]")
        sys.exit(1)


def _handle_scene_info(
    args: argparse.Namespace,
    project_meta: "ProjectMetadata",
    scene_repo: Any,
    project_dir: Path,
) -> None:
    scenes = scene_repo.list_scenes(project_dir)
    matching = [(p, m) for p, m in scenes if m.name == args.name]
    if not matching:
        print(f"Scene '{args.name}' not found in project '{project_meta.name}'.")
        sys.exit(1)
    scene_path, scene_meta = matching[0]
    print(f"Name:        {scene_meta.name}")
    print(f"Index:       {scene_meta.index}")
    print(f"Path:        {scene_path}")
    print(f"Status:      {scene_meta.status}")
    print(f"Aspect:      {scene_meta.aspect_ratio}")
    print(f"Description: {scene_meta.description or '(none)'}")
    print(f"Keyframes:   {scene_meta.keyframe_count}")
    print(f"Clips:       {scene_meta.clip_count}")
    print(f"Duration:    {scene_meta.total_duration_seconds:.1f}s")
    print(f"Cost:        {scene_meta.cost.amount:.4f} {scene_meta.cost.currency}")
    print(f"Created:     {scene_meta.created_at.strftime('%Y-%m-%d %H:%M')}")


def _handle_scene_command(args: argparse.Namespace) -> None:
    """Dispatch scene subcommands."""
    from stable_delusion.project.xdg_paths import resolve_projects_dir
    from stable_delusion.project.models import SceneMetadata
    from stable_delusion.project.project_repository import ProjectRepository
    from stable_delusion.project.scene_repository import SceneRepository
    from stable_delusion.project.active_project import get_active_project

    projects_dir = resolve_projects_dir(getattr(args, "dir", None))
    project_repo = ProjectRepository()
    scene_repo = SceneRepository()

    # Resolve the project
    project_name = getattr(args, "project", None)
    if project_name:
        project_dir, project_meta = project_repo.find_by_name(projects_dir, project_name)
    else:
        active = get_active_project()
        if active is None:
            print("No active project. Use 'hallucinate project use <name>' to set one.")
            sys.exit(1)
        project_dir = active[1]
        project_meta = project_repo.get(project_dir)

    if args.subcommand == "new":
        index = scene_repo.next_index(project_dir)
        metadata = SceneMetadata.create_new(
            name=args.name,
            index=index,
            aspect_ratio=project_meta.aspect_ratio,
            description=args.description,
        )
        scene_dir = scene_repo.create(project_dir, args.name, metadata)
        project_meta.scenes.append(scene_dir.name)
        project_meta.updated_at = datetime.datetime.now(datetime.timezone.utc)
        project_repo.update(project_dir, project_meta)
        print(f"Created scene '{args.name}' at {scene_dir}")

    elif args.subcommand == "list":
        scenes = scene_repo.list_scenes(project_dir)
        if not scenes:
            print(f"No scenes in project '{project_meta.name}'.")
        else:
            for _, meta in scenes:
                print(f"  [{meta.index:02d}] {meta.name} — {meta.status}")

    elif args.subcommand == "info":
        _handle_scene_info(args, project_meta, scene_repo, project_dir)

    else:
        print("Usage: hallucinate scene [new|list|info]")
        sys.exit(1)


def main():
    try:
        prompt, images, args = _process_cli_arguments()

        # Configure coloredlogs based on quiet/debug flags
        setup_logging(quiet=args.quiet, debug=args.debug)

        if args.command == "video":
            print("Video generation commands are not yet implemented. Coming in Phase 2.")
            sys.exit(0)

        if args.command == "project":
            _handle_project_command(args)
            return

        if args.command == "scene":
            _handle_scene_command(args)
            return

        if args.command == "image" and args.subcommand == "generate":
            from stable_delusion.project.context import resolve_context
            from stable_delusion.exceptions.project_error import ProjectError as _ProjectError

            _ctx = resolve_context(
                scene_name=getattr(args, "scene", None),
                project_name=getattr(args, "project", None),
            )
            if _ctx is not None:
                _handle_image_generate_project(args, _ctx)
                return

        request_dto = _create_cli_request_dto(prompt, images, args)
        response = _execute_image_generation(request_dto)
        _handle_cli_custom_output(response, request_dto)
        _log_generation_result(response, args)

    except (ImageGenerationError, FileOperationError) as e:
        logging.error("Image generation failed: %s", e)
    except Exception as e:  # pylint: disable=broad-exception-caught
        logging.error("Unexpected error during image generation: %s", e)


if __name__ == "__main__":
    main()
