#!/usr/bin/env python3
"""Thin CLI wrapper around api_sdk library functions.

Implements a layered design:
  - Core/SDK lives in api_sdk.* modules with pure functions.
  - CLI performs parsing, logging, I/O, exit code mapping, and JSON output.

Global flags:
  --json      Emit machine-readable JSON result on stdout
  --verbose   Verbose logging (DEBUG)
  --quiet     Suppress non-error logs (ERROR)
  --no-color  Disable decorative icons/emojis
"""

import argparse
import json
import logging
import os
import sys
from pathlib import Path
from typing import Any, Optional
from typing import Any, Dict


EXIT_OK = 0
EXIT_INVALID_INPUT = 2
EXIT_NOT_FOUND = 3
EXIT_DEPENDENCY_ERROR = 4
EXIT_UNKNOWN_ERROR = 5


def setup_logging(verbose: bool = False, quiet: bool = False) -> None:
    """Setup logging configuration.

    - verbose -> DEBUG
    - quiet   -> ERROR
    - default -> INFO
    """
    if quiet:
        level = logging.ERROR
    else:
        level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(level=level, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')


def emit_json(data: Dict[str, Any]) -> None:
    """Print JSON to stdout with stable formatting."""
    print(json.dumps(data, ensure_ascii=False, separators=(",", ":")))


def decorate(msg: str, *, use_color: bool, icon: str) -> str:
    return (f"{icon} " if use_color else "") + msg


def main():
    """Main CLI entry point"""
    parser = argparse.ArgumentParser(
        description='API SDK CLI - Generate images and detect objects using Google Gemini',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Generate an image with a simple prompt (saves to current directory)
  api-sdk generate-image "A beautiful sunset over mountains"

  # Save to specific folder (creates my_image.png and my_image_description.md)
  api-sdk generate-image "A cat playing piano" -o ./images/ -n my_image

  # Save to specific file path
  api-sdk generate-image "Abstract art" -o ./output/artwork.png

  # Generate with reference image
  api-sdk generate-image "Make it sunset" -i reference.jpg

  # Multiple reference images
  api-sdk generate-image "Combine these styles" -i style1.jpg -i style2.jpg

  # Skip saving description file
  api-sdk generate-image "Quick test" --no-description

  # Detect objects in an image
  api-sdk detect-objects /path/to/image.jpg "Find all objects"

  # Detect specific objects
  api-sdk detect-objects /path/to/image.jpg "Find all animals and food items"

  # Save detection result to specific location
  api-sdk detect-objects /path/to/image.jpg "Detect vehicles" -o ./detected/result.png

  # Use a different model
  api-sdk generate-image "Abstract art" --model gemini-3-pro-image-preview
        """
    )

    # Global flags (apply to all subcommands)
    parser.add_argument('--json', action='store_true', help='Emit machine-readable JSON to stdout')
    parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output (DEBUG)')
    parser.add_argument('-q', '--quiet', action='store_true', help='Suppress non-error logs (ERROR)')
    parser.add_argument('--no-color', action='store_true', help='Disable decorative icons/emojis in stdout')

    # Add subparsers for different commands
    subparsers = parser.add_subparsers(dest='command', help='Available commands')

    # Generate text command
    text_parser = subparsers.add_parser(
        'generate-text',
        help='Generate text from a prompt (Gemini or Codex)'
    )
    text_parser.add_argument('prompt', type=str, help='Text prompt')
    text_parser.add_argument('--model', type=str, default=None, help='Model to use (default: gemini-2.5-pro)')
    # Credentials are resolved by the google-genai client (no API key flag)
    text_parser.add_argument('-o', '--output', type=str, default=None, help='Optional path to save text output')
    text_parser.add_argument('--temperature', type=float, default=None, help='Sampling temperature')
    text_parser.add_argument('--max-tokens', type=int, default=None, help='Maximum output tokens')
    text_parser.add_argument('--top-p', type=float, default=None, help='Top-p sampling')
    text_parser.add_argument('--top-k', type=int, default=None, help='Top-k sampling')
    text_parser.add_argument('--seed', type=int, default=None, help='Deterministic seed (if supported)')
    text_parser.add_argument('--stop', action='append', dest='stop_sequences', help='Stop sequence (repeatable)')
    text_parser.add_argument('--provider', type=str, choices=['gemini','codex'], default=os.environ.get('TEXT_PROVIDER','gemini'), help='Text generation provider: gemini (default) or codex')
    text_parser.add_argument('--response-mime-type', type=str, default=None, help='Optional response mime type (e.g., application/json for structured output)')
    text_parser.add_argument('--response-schema-file', type=str, default=None, help='Optional JSON schema file for Gemini Structured Output')

    # Generate image command
    image_parser = subparsers.add_parser(
        'generate-image',
        help='Generate an image from a text prompt with optional reference images'
    )
    image_parser.add_argument(
        'prompt',
        type=str,
        help='Text prompt for image generation'
    )
    image_parser.add_argument(
        '--provider',
        type=str,
        choices=['gemini', 'jimeng'],
        default=os.environ.get('IMAGE_PROVIDER', 'gemini'),
        help='Image provider: gemini (default) or jimeng (Volcengine 即梦)'
    )
    image_parser.add_argument(
        '-i', '--image',
        action='append',
        dest='images',
        help='Input/reference image(s) - can be used multiple times for multiple images'
    )
    image_parser.add_argument(
        '-o', '--output',
        type=str,
        default=None,
        help='Output folder or file path. If folder, saves as generated_image.png (or use -n for custom name)'
    )
    image_parser.add_argument(
        '-n', '--name',
        type=str,
        default='generated_image',
        help='Base name for output files when -o is a folder (default: generated_image)'
    )
    image_parser.add_argument(
        '--no-description',
        action='store_true',
        help='Skip saving the description markdown file'
    )
    image_parser.add_argument(
        '--model',
        type=str,
        default=None,
        help='Model to use. Gemini default: gemini-3-pro-image-preview; Jimeng default (req_key): jimeng_t2i_v40'
    )
    image_parser.add_argument(
        '--aspect-ratio',
        type=str,
        default=None,
        help='Gemini only: aspect ratio via image_config (e.g., 1:1, 16:9)'
    )
    image_parser.add_argument(
        '--seed',
        type=int,
        default=None,
        help='Jimeng only: deterministic seed'
    )
    image_parser.add_argument(
        '--width',
        type=int,
        default=None,
        help='Jimeng only: output width (if supported by model)'
    )
    image_parser.add_argument(
        '--height',
        type=int,
        default=None,
        help='Jimeng only: output height (if supported by model)'
    )
    image_parser.add_argument(
        '--extra-config-file',
        type=str,
        default=None,
        help='Optional JSON file merged into provider config/body (advanced)'
    )
    # Credentials are resolved by the google-genai client (no API key flag)
    # Subcommand-specific verbosity is removed in favor of global flags

    # Generate video command (Veo / Jimeng)
    video_parser = subparsers.add_parser(
        'generate-video',
        help='Generate a video from a text prompt (Gemini Veo or Volcengine Jimeng)'
    )
    video_parser.add_argument(
        'prompt',
        type=str,
        help='Text prompt for video generation'
    )
    video_parser.add_argument(
        '--provider',
        type=str,
        choices=['gemini', 'jimeng'],
        default=os.environ.get('VIDEO_PROVIDER', 'gemini'),
        help='Video provider: gemini (Veo, default) or jimeng (Volcengine 即梦)'
    )
    video_parser.add_argument(
        '-i', '--image',
        type=str,
        default=None,
        help='Optional input image path (used as first frame / reference)'
    )
    video_parser.add_argument(
        '-o', '--output',
        type=str,
        default=None,
        help='Output folder or file path. If folder/no suffix, saves as generated_video.mp4 (or use -n for custom name)'
    )
    video_parser.add_argument(
        '-n', '--name',
        type=str,
        default='generated_video',
        help='Base name for output files when -o is a folder (default: generated_video)'
    )
    video_parser.add_argument(
        '--no-description',
        action='store_true',
        help='Skip saving the description markdown file'
    )
    video_parser.add_argument(
        '--model',
        type=str,
        default=None,
        help='Model to use. Gemini default: veo-3.1-generate-preview; Jimeng default (req_key): jimeng_t2v_v30'
    )
    video_parser.add_argument(
        '--negative-prompt',
        type=str,
        default=None,
        help='Optional negative prompt (discourage unwanted elements)'
    )
    video_parser.add_argument(
        '--reference-image',
        action='append',
        dest='reference_images',
        default=None,
        help='Reference image path (max 3). Use multiple times for multiple images.'
    )
    video_parser.add_argument(
        '--reference-type',
        action='append',
        dest='reference_types',
        default=None,
        help='Reference type for each --reference-image (ASSET or STYLE). Must match count if provided.'
    )
    video_parser.add_argument(
        '--last-frame',
        type=str,
        default=None,
        help='Optional last frame image path (for interpolation)'
    )
    video_parser.add_argument(
        '--extend-video',
        type=str,
        default=None,
        help='Optional input video path/URI to extend'
    )
    video_parser.add_argument(
        '--aspect-ratio',
        type=str,
        default=None,
        help='Optional aspect ratio (e.g., 16:9, 9:16, 1:1)'
    )
    video_parser.add_argument(
        '--resolution',
        type=str,
        default=None,
        help='Optional resolution (e.g., 720p, 1080p)'
    )
    video_parser.add_argument(
        '--duration-seconds',
        type=int,
        default=None,
        help='Optional duration in seconds'
    )
    video_parser.add_argument(
        '--frames',
        type=int,
        default=None,
        help='Jimeng only: frames=121 (5s) or 241 (10s). Overrides --duration-seconds mapping.'
    )
    video_parser.add_argument(
        '--person-generation',
        type=str,
        default=None,
        help='Optional person generation policy (provider-specific)'
    )
    video_parser.add_argument(
        '--seed',
        type=int,
        default=None,
        help='Optional deterministic seed'
    )
    video_parser.add_argument(
        '--fps',
        type=int,
        default=None,
        help='Optional frames per second'
    )
    video_parser.add_argument(
        '--num-videos',
        type=int,
        default=1,
        help='Number of videos to generate (default: 1)'
    )
    video_parser.add_argument(
        '--enhance-prompt',
        action='store_true',
        default=None,
        help='Enable prompt enhancement (if supported)'
    )
    video_parser.add_argument(
        '--generate-audio',
        action='store_true',
        default=None,
        help='Enable audio generation (if supported)'
    )
    video_parser.add_argument(
        '--compression-quality',
        type=str,
        default=None,
        help='Optional compression quality (LOSSLESS or OPTIMIZED)'
    )
    video_parser.add_argument(
        '--poll-interval',
        type=float,
        default=5.0,
        help='Poll interval seconds for long-running operations (default: 5.0)'
    )
    video_parser.add_argument(
        '--timeout',
        type=float,
        default=600.0,
        help='Overall timeout seconds for generation (default: 600.0)'
    )

    # Detect objects command
    detect_parser = subparsers.add_parser(
        'detect-objects',
        help='Detect objects in an image and draw bounding boxes'
    )
    detect_parser.add_argument(
        'image',
        type=str,
        help='Path to the input image'
    )
    detect_parser.add_argument(
        'prompt',
        type=str,
        help='Detection prompt (required), e.g., "Find all people", "Detect vehicles", "Identify food items"'
    )
    detect_parser.add_argument(
        '-o', '--output',
        type=str,
        default=None,
        help='Output path for the image with bounding boxes (default: <input>_detected.<ext>)'
    )
    detect_parser.add_argument(
        '--model',
        type=str,
        default='gemini-2.0-flash-exp',
        help='Model to use for detection (default: gemini-2.0-flash-exp)'
    )
    # Credentials are resolved by the google-genai client (no API key flag)
    detect_parser.add_argument(
        '--box-color',
        type=str,
        default='red',
        help='Color of the bounding boxes (default: red)'
    )
    detect_parser.add_argument(
        '--box-width',
        type=int,
        default=1,
        help='Width of the bounding box lines in pixels (default: 1)'
    )
    # Subcommand-specific verbosity is removed in favor of global flags

    # Synthesize speech (TTS) command
    tts_parser = subparsers.add_parser(
        'synthesize-speech',
        aliases=['generate-speech'],
        help='Synthesize speech audio from text (Chinese supported) via iFlytek or Google Gemini TTS'
    )
    tts_parser.add_argument(
        'text',
        type=str,
        help='Input text to synthesize (UTF-8, Chinese supported)'
    )
    tts_parser.add_argument(
        '-o', '--output',
        type=str,
        default=None,
        help='Output file path (.wav or .pcm). Default: speech.wav'
    )
    tts_parser.add_argument(
        '--voice',
        type=str,
        default=None,
        help='Voice name/code. For iFlytek: vcn (e.g., x_xiaoyang_story). For Google: voice name (optional)'
    )
    tts_parser.add_argument(
        '--speed',
        type=int,
        default=None,
        help='Optional speed parameter (provider-specific range)'
    )
    tts_parser.add_argument(
        '--volume',
        type=int,
        default=None,
        help='Optional volume parameter (provider-specific range)'
    )
    tts_parser.add_argument(
        '--pitch',
        type=int,
        default=None,
        help='Optional pitch parameter (provider-specific range)'
    )
    tts_parser.add_argument(
        '--format',
        type=str,
        choices=['wav', 'pcm', 'mp3'],
        default='wav',
        help='Output audio format: wav (default), pcm, or mp3 (Google only)'
    )
    tts_parser.add_argument(
        '--sample-rate',
        type=int,
        default=16000,
        help='Sample rate for PCM/WAV (default: 16000)'
    )
    tts_parser.add_argument(
        '--provider',
        type=str,
        choices=['iflytek', 'google', 'gemini', 'algieba', 'elevenlabs'],
        default=os.environ.get('TTS_PROVIDER', 'algieba'),
        help='TTS provider to use: algieba (default, Gemini) or iflytek/google/gemini/elevenlabs'
    )
    tts_parser.add_argument(
        '--model',
        type=str,
        default=None,
        help='TTS model for provider. For Google: gemini-2.5-pro-preview-tts; For ElevenLabs: eleven_multilingual_v2'
    )
    tts_parser.add_argument(
        '--output-format',
        type=str,
        default=None,
        help='ElevenLabs only: output_format like mp3_44100_128 (defaults to env ELEVENLABS_OUTPUT_FORMAT or mp3_44100_128)'
    )
    tts_parser.add_argument(
        '--endpoint',
        type=str,
        default=None,
        help='Override iFlytek WebSocket endpoint (default: wss://tts-api-sg.xf-yun.com/v2/tts) (iflytek only)'
    )
    # Credentials resolved from env by default; advanced users may pass flags here in the future

    # Design report command
    report_parser = subparsers.add_parser(
        'design-report',
        help='Generate a design report composed of one or more image grids'
    )
    report_parser.add_argument(
        '-c', '--config',
        type=str,
        required=True,
        help='Path to JSON config describing grids and outputs'
    )
    report_parser.add_argument(
        '-o', '--output',
        type=str,
        default=None,
        help='Optional PNG output path override'
    )
    report_parser.add_argument(
        '--pdf',
        type=str,
        default=None,
        help='Optional PDF output path override'
    )
    # Subcommand-specific verbosity is removed in favor of global flags

    # Remove background command (RMBG or Trimap via scenario routing)
    rb_parser = subparsers.add_parser(
        'remove-background',
        help='Remove image background using either RMBG (subject) or Trimap Matting (white-bg-multi)'
    )
    rb_parser.add_argument(
        '--scenario',
        type=str,
        choices=['auto', 'subject', 'white-bg-multi'],
        default='auto',
        help='Routing scenario: auto (analyze), subject (RMBG), white-bg-multi (Trimap)'
    )
    # Backend selection: rmbg or trimap (may be overridden by --scenario)
    rb_parser.add_argument(
        '--backend',
        type=str,
        choices=['rmbg', 'trimap'],
        default='rmbg',
        help='Backend to use: rmbg (default) or trimap (trimap requires keep/bg masks or auto background)'
    )

    rb_parser.add_argument(
        'image',
        type=str,
        help='Path to the input image'
    )
    rb_parser.add_argument(
        '-o', '--output',
        type=str,
        default=None,
        help='Output path for the PNG with transparent background (default: <input>_transparent.png)'
    )
    rb_parser.add_argument(
        '--alpha-matting',
        action='store_true',
        help='Enable alpha matting refinement for cleaner edges'
    )
    rb_parser.add_argument(
        '--alpha-matting-foreground-threshold',
        type=int,
        default=240,
        help='Foreground threshold for alpha matting (0-255, default: 240)'
    )
    rb_parser.add_argument(
        '--alpha-matting-background-threshold',
        type=int,
        default=10,
        help='Background threshold for alpha matting (0-255, default: 10)'
    )
    rb_parser.add_argument(
        '--alpha-matting-erosion-size',
        type=int,
        default=10,
        help='Erosion size for trimap refinement (default: 10)'
    )
    rb_parser.add_argument(
        '--model',
        type=str,
        default=None,
        help='Optional rembg model name (e.g., isnet-general-use)'
    )
    # Guided preservation options
    rb_parser.add_argument(
        '--keep-mask',
        type=str,
        default=None,
        help='Optional binary mask to force-keep regions (white=keep)'
    )
    rb_parser.add_argument(
        '--keep-mask-dilate',
        type=int,
        default=0,
        help='Dilate keep-mask by N iterations before reconstruction'
    )
    rb_parser.add_argument(
        '--alpha-support-threshold',
        type=int,
        default=40,
        help='Pixels with model alpha >= threshold are valid support for reconstruction'
    )
    rb_parser.add_argument(
        '--nonwhite-keep-threshold',
        type=int,
        default=None,
        help='Also treat non-white pixels (distance to white > T) as support'
    )
    rb_parser.add_argument(
        '--auto-keep-from-nonwhite',
        action='store_true',
        help='Derive sparse seeds from non-white pixels, then reconstruct to keep entire connected objects'
    )
    rb_parser.add_argument(
        '--auto-seed-erosion',
        type=int,
        default=2,
        help='Erode auto seeds by N to make them sparse (default: 2)'
    )
    # Trimap-specific options
    rb_parser.add_argument(
        '--bg-mask',
        type=str,
        default=None,
        help='Optional binary mask to force background (white=background) for trimap backend'
    )
    rb_parser.add_argument(
        '--bg-corners',
        action='store_true',
        help='Auto-generate a background mask with white patches at four corners (for center-subject photos)'
    )
    rb_parser.add_argument(
        '--corner-size',
        type=float,
        default=0.12,
        help='Relative corner patch size (0-0.5), used with --bg-corners (default: 0.12)'
    )
    rb_parser.add_argument(
        '--trimap-keep-dilate',
        type=int,
        default=2,
        help='Dilate keep-mask (foreground seeds) when building trimap (default: 2)'
    )
    rb_parser.add_argument(
        '--trimap-bg-dilate',
        type=int,
        default=2,
        help='Dilate bg-mask (background seeds) when building trimap (default: 2)'
    )
    rb_parser.add_argument(
        '--trimap-bg-mode',
        type=str,
        choices=['dilate','graphcut'],
        default='graphcut',
        help='How to expand background seeds into trimap: simple dilation or graphcut propagation (default: graphcut)'
    )
    rb_parser.add_argument(
        '--grabcut-iters',
        type=int,
        default=5,
        help='Iterations for GrabCut when --trimap-bg-mode graphcut (default: 5)'
    )
    rb_parser.add_argument(
        '--auto-bg-from-edges',
        action='store_true',
        help='Auto-detect white background connected to edges for trimap backend'
    )
    rb_parser.add_argument(
        '--white-bg-threshold',
        type=int,
        default=240,
        help='Threshold for near-white background when auto-detecting (default: 240)'
    )

    # Subcommand-specific verbosity is removed in favor of global flags

    args = parser.parse_args()

    # Show help if no command provided
    if not args.command:
        parser.print_help()
        return EXIT_OK

    # Configure logging and UI toggles
    setup_logging(getattr(args, 'verbose', False), getattr(args, 'quiet', False))
    use_color = not getattr(args, 'no_color', False)

    # Handle generate-text command
    if args.command == 'generate-text':
        from api_sdk.text_generator import generate_text, generate_text_structured
        try:
            response_json_schema: Optional[Any] = None
            if getattr(args, "response_schema_file", None):
                schema_path = Path(args.response_schema_file)
                response_json_schema = json.loads(schema_path.read_text(encoding="utf-8"))

            use_response_mime_type = args.response_mime_type
            if response_json_schema is not None and not use_response_mime_type:
                use_response_mime_type = "application/json"

            provider = getattr(args, "provider", "gemini")
            if response_json_schema is not None:
                obj = generate_text_structured(
                    args.prompt,
                    model=args.model,
                    schema=response_json_schema,
                    response_mime_type=use_response_mime_type or "application/json",
                    provider=provider,
                )
                text = json.dumps(obj, ensure_ascii=False, indent=2)
            else:
                text = generate_text(
                    prompt=args.prompt,
                    model=args.model,
                    temperature=args.temperature,
                    max_output_tokens=args.max_tokens,
                    top_p=args.top_p,
                    top_k=args.top_k,
                    seed=args.seed,
                    stop_sequences=args.stop_sequences,
                    response_mime_type=use_response_mime_type,
                    provider=provider,
                )

            saved: Optional[Path] = None
            if args.output:
                out_path = Path(args.output)
                if out_path.exists() and out_path.is_dir():
                    out_path = out_path / "generated_text.txt"
                out_path.parent.mkdir(parents=True, exist_ok=True)
                out_path.write_text(text, encoding="utf-8")
                saved = out_path
            if args.json:
                emit_json({
                    "ok": True,
                    "command": "generate-text",
                    "model": args.model,
                    "provider": getattr(args, 'provider', 'gemini'),
                    "output_path": str(saved) if saved else None,
                    "response_mime_type": use_response_mime_type,
                    "schema_path": args.response_schema_file,
                    "text": text,
                })
            elif not args.quiet:
                print(text)
                if saved:
                    print(decorate(f"Saved to: {saved}", use_color=use_color, icon="💾"))
            return EXIT_OK
        except ValueError as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "InvalidInput"})
            else:
                print(f"Invalid input: {e}", file=sys.stderr)
            return EXIT_INVALID_INPUT
        except Exception as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "UnknownError"})
            else:
                print(f"Failed to generate text: {e}", file=sys.stderr)
                if getattr(args, 'verbose', False):
                    import traceback
                    traceback.print_exc()
            return EXIT_UNKNOWN_ERROR

    # Handle generate-image command
    if args.command == 'generate-image':
        # Delay import of heavy modules
        from api_sdk.image_generator import generate_image

        try:
            provider = (getattr(args, "provider", "gemini") or "gemini").lower().strip()
            extra_config: Optional[Dict[str, Any]] = None
            if getattr(args, "extra_config_file", None):
                cfg_path = Path(args.extra_config_file)
                extra_config = json.loads(cfg_path.read_text(encoding="utf-8"))

            if not args.json and not args.quiet:
                if args.images:
                    print(decorate(f"Generating image with prompt: \"{args.prompt}\"", use_color=use_color, icon="🎨"))
                    print("   Using %d reference image(s)" % len(args.images))
                else:
                    print(decorate(f"Generating image with prompt: \"{args.prompt}\"", use_color=use_color, icon="🎨"))
                if provider != "gemini":
                    print(f"   Provider: {provider}")

            # Handle output path - combine directory and name if needed
            output_path = args.output
            if output_path:
                output_path = Path(output_path)
                # If it's a directory or has no extension, treat as directory
                if output_path.exists() and output_path.is_dir():
                    # Existing directory
                    output_path = output_path / args.name
                elif not output_path.suffix:
                    # No extension, treat as directory
                    output_path = output_path / args.name
                # Otherwise it's a full file path with extension

            if provider == "jimeng":
                if args.images:
                    raise ValueError("Jimeng provider does not support -i/--image reference images")
                if args.aspect_ratio is not None:
                    raise ValueError("Jimeng provider does not support --aspect-ratio in this CLI; pass provider params via --extra-config-file instead")
            else:
                if any(v is not None for v in (args.seed, args.width, args.height)):
                    raise ValueError("--seed/--width/--height are only supported with --provider jimeng")

            image_path, description_path = generate_image(
                prompt=args.prompt,
                output_path=output_path,
                provider=provider,
                model=args.model,
                input_images=args.images,
                aspect_ratio=args.aspect_ratio,
                seed=args.seed,
                width=args.width,
                height=args.height,
                extra_config=extra_config,
                save_description=not args.no_description
            )

            if args.json:
                emit_json({
                    "ok": True,
                    "command": "generate-image",
                    "provider": provider,
                    "image_path": str(image_path),
                    "description_path": str(description_path) if description_path else None,
                    "model": args.model,
                    "input_images": args.images or [],
                    "aspect_ratio": args.aspect_ratio,
                    "seed": args.seed,
                    "width": args.width,
                    "height": args.height,
                })
            elif not args.quiet:
                print(decorate(f"Image saved to: {image_path}", use_color=use_color, icon="✅"))
                if description_path:
                    print(decorate(f"Description saved to: {description_path}", use_color=use_color, icon="📝"))
            return EXIT_OK

        except ValueError as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "InvalidInput"})
            else:
                print(f"Invalid input: {e}", file=sys.stderr)
            return EXIT_INVALID_INPUT
        except Exception as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "UnknownError"})
            else:
                print(f"Failed to generate image: {e}", file=sys.stderr)
                if getattr(args, 'verbose', False):
                    import traceback
                    traceback.print_exc()
            return EXIT_UNKNOWN_ERROR

    # Handle generate-video command
    if args.command == 'generate-video':
        from api_sdk.video_generator import generate_video, generate_videos
        provider = (args.provider or 'gemini').lower().strip()

        try:
            if not args.json and not args.quiet:
                print(decorate(f"Generating video with prompt: \"{args.prompt}\"", use_color=use_color, icon="🎬"))
                if args.image:
                    print(f"   Using input image: {args.image}")
                if args.extend_video:
                    print(f"   Extending from video: {args.extend_video}")
                if provider != "gemini":
                    print(f"   Provider: {provider}")

            output_path = args.output
            if output_path:
                output_path = Path(output_path)
                if output_path.exists() and output_path.is_dir():
                    output_path = output_path / args.name
                elif not output_path.suffix:
                    output_path = output_path / args.name

            if args.reference_types and args.reference_images and len(args.reference_types) != len(args.reference_images):
                raise ValueError("--reference-type count must match --reference-image count")
            if args.reference_images and len(args.reference_images) > 3:
                raise ValueError("--reference-image supports at most 3 images")

            extra_config = {"frames": args.frames} if args.frames is not None else None

            if provider == "jimeng":
                if args.num_videos and args.num_videos != 1:
                    raise ValueError("Jimeng provider does not support --num-videos != 1")
                if args.frames is None and args.duration_seconds is not None and args.duration_seconds not in (5, 10):
                    raise ValueError("Jimeng provider requires --duration-seconds 5 or 10 (or pass --frames 121/241)")
                if args.frames is not None and args.frames not in (121, 241):
                    raise ValueError("Jimeng provider requires --frames 121 or 241")

                unsupported = {
                    "--image": args.image,
                    "--last-frame": args.last_frame,
                    "--reference-image": args.reference_images,
                    "--reference-type": args.reference_types,
                    "--extend-video": args.extend_video,
                    "--negative-prompt": args.negative_prompt,
                    "--resolution": args.resolution,
                    "--person-generation": args.person_generation,
                    "--fps": args.fps,
                }
                unsupported_flags = [k for k, v in unsupported.items() if v]
                if args.enhance_prompt:
                    unsupported_flags.append("--enhance-prompt")
                if args.generate_audio:
                    unsupported_flags.append("--generate-audio")
                if args.compression_quality:
                    unsupported_flags.append("--compression-quality")
                if unsupported_flags:
                    raise ValueError(f"Jimeng provider does not support: {', '.join(unsupported_flags)}")

                compression_quality = None
            else:
                if args.frames is not None:
                    raise ValueError("--frames is only supported with --provider jimeng")
                from google.genai import types as genai_types

                compression_quality = None
                if args.compression_quality:
                    key = args.compression_quality.strip().upper()
                    if key not in ("LOSSLESS", "OPTIMIZED"):
                        raise ValueError("--compression-quality must be LOSSLESS or OPTIMIZED")
                    compression_quality = getattr(genai_types.VideoCompressionQuality, key)

            if args.num_videos and args.num_videos > 1:
                video_paths, description_path = generate_videos(
                    prompt=args.prompt,
                    output_path=output_path,
                    provider=provider,
                    model=args.model,
                    input_image=args.image,
                    last_frame=args.last_frame,
                    reference_images=args.reference_images,
                    reference_types=args.reference_types,
                    input_video=args.extend_video,
                    negative_prompt=args.negative_prompt,
                    aspect_ratio=args.aspect_ratio,
                    resolution=args.resolution,
                    duration_seconds=args.duration_seconds,
                    person_generation=args.person_generation,
                    seed=args.seed,
                    fps=args.fps,
                    number_of_videos=args.num_videos,
                    enhance_prompt=args.enhance_prompt,
                    generate_audio=args.generate_audio,
                    compression_quality=compression_quality,
                    extra_config=extra_config,
                    poll_interval_s=args.poll_interval,
                    timeout_s=args.timeout,
                    save_description=not args.no_description,
                )
                if args.json:
                    emit_json({
                        "ok": True,
                        "command": "generate-video",
                        "provider": provider,
                        "video_paths": [str(p) for p in video_paths],
                        "description_path": str(description_path) if description_path else None,
                        "model": args.model,
                        "input_image": args.image,
                        "last_frame": args.last_frame,
                        "reference_images": args.reference_images or [],
                        "reference_types": args.reference_types or [],
                        "input_video": args.extend_video,
                        "negative_prompt": args.negative_prompt,
                        "aspect_ratio": args.aspect_ratio,
                        "resolution": args.resolution,
                        "duration_seconds": args.duration_seconds,
                        "person_generation": args.person_generation,
                        "seed": args.seed,
                        "fps": args.fps,
                        "num_videos": args.num_videos,
                        "frames": args.frames,
                    })
                elif not args.quiet:
                    for p in video_paths:
                        print(decorate(f"Video saved to: {p}", use_color=use_color, icon="✅"))
                    if description_path:
                        print(decorate(f"Description saved to: {description_path}", use_color=use_color, icon="📝"))
                return EXIT_OK

            video_path, description_path = generate_video(
                prompt=args.prompt,
                output_path=output_path,
                provider=provider,
                model=args.model,
                input_image=args.image,
                last_frame=args.last_frame,
                reference_images=args.reference_images,
                reference_types=args.reference_types,
                input_video=args.extend_video,
                negative_prompt=args.negative_prompt,
                aspect_ratio=args.aspect_ratio,
                resolution=args.resolution,
                duration_seconds=args.duration_seconds,
                person_generation=args.person_generation,
                seed=args.seed,
                fps=args.fps,
                enhance_prompt=args.enhance_prompt,
                generate_audio=args.generate_audio,
                compression_quality=compression_quality,
                extra_config=extra_config,
                poll_interval_s=args.poll_interval,
                timeout_s=args.timeout,
                save_description=not args.no_description,
            )

            if args.json:
                emit_json({
                    "ok": True,
                    "command": "generate-video",
                    "provider": provider,
                    "video_path": str(video_path),
                    "description_path": str(description_path) if description_path else None,
                    "model": args.model,
                    "input_image": args.image,
                    "last_frame": args.last_frame,
                    "reference_images": args.reference_images or [],
                    "reference_types": args.reference_types or [],
                    "input_video": args.extend_video,
                    "negative_prompt": args.negative_prompt,
                    "aspect_ratio": args.aspect_ratio,
                    "resolution": args.resolution,
                    "duration_seconds": args.duration_seconds,
                    "person_generation": args.person_generation,
                    "seed": args.seed,
                    "fps": args.fps,
                    "num_videos": 1,
                    "frames": args.frames,
                })
            elif not args.quiet:
                print(decorate(f"Video saved to: {video_path}", use_color=use_color, icon="✅"))
                if description_path:
                    print(decorate(f"Description saved to: {description_path}", use_color=use_color, icon="📝"))
            return EXIT_OK

        except ValueError as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "InvalidInput"})
            else:
                print(f"Invalid input: {e}", file=sys.stderr)
            return EXIT_INVALID_INPUT
        except Exception as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "UnknownError"})
            else:
                print(f"Failed to generate video: {e}", file=sys.stderr)
                if getattr(args, 'verbose', False):
                    import traceback
                    traceback.print_exc()
            return EXIT_UNKNOWN_ERROR

    # Handle detect-objects command
    elif args.command == 'detect-objects':
        # Delay import
        from api_sdk.object_detector import detect_and_draw

        try:
            if not args.json and not args.quiet:
                print(decorate(f"Detecting objects in image: {args.image}", use_color=use_color, icon="🔍"))
                print(f"   Using prompt: \"{args.prompt}\"")

            output_path, detected_objects = detect_and_draw(
                image_path=args.image,
                prompt=args.prompt,
                output_path=args.output,
                model=args.model,
                box_color=args.box_color,
                box_width=args.box_width
            )

            if args.json:
                emit_json({
                    "ok": True,
                    "command": "detect-objects",
                    "output_path": str(output_path),
                    "objects": detected_objects,
                    "model": args.model,
                })
            elif not args.quiet:
                print(decorate(f"Detected {len(detected_objects)} objects", use_color=use_color, icon="✅"))
                for obj in detected_objects:
                    name = obj.get('name', 'unknown')
                    confidence = obj.get('confidence')
                    if confidence is not None:
                        print(f"   - {name} (confidence: {confidence:.2f})")
                    else:
                        print(f"   - {name}")
                print(decorate(f"Output saved to: {output_path}", use_color=use_color, icon="📁"))
            return EXIT_OK

        except FileNotFoundError as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "NotFound"})
            else:
                print(f"File not found: {e}", file=sys.stderr)
            return EXIT_NOT_FOUND
        except ValueError as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "InvalidInput"})
            else:
                print(f"Invalid input: {e}", file=sys.stderr)
            return EXIT_INVALID_INPUT
        except Exception as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "UnknownError"})
            else:
                print(f"Failed to detect objects: {e}", file=sys.stderr)
                if getattr(args, 'verbose', False):
                    import traceback
                    traceback.print_exc()
            return EXIT_UNKNOWN_ERROR

    # Handle synthesize-speech command
    elif args.command in ('synthesize-speech', 'generate-speech'):
        from api_sdk.tts_google import synthesize_speech_google_bytes, DEFAULT_GOOGLE_TTS_MODEL

        try:
            if not args.json and not args.quiet:
                print(decorate("Synthesizing speech...", use_color=use_color, icon="🔊"))

            if args.provider in ('google', 'gemini', 'algieba'):
                audio_bytes = synthesize_speech_google_bytes(
                    text=args.text,
                    voice=(args.voice or os.environ.get('GEMINI_TTS_VOICE') or 'Algieba'),
                    sample_rate=args.sample_rate,
                    model=(args.model or DEFAULT_GOOGLE_TTS_MODEL),
                    audio_format=args.format,
                )
            elif args.provider == 'elevenlabs':
                if args.format != 'mp3':
                    raise ValueError("ElevenLabs provider currently only supports --format mp3")
                if any(v is not None for v in (args.speed, args.volume, args.pitch, args.endpoint)):
                    raise ValueError("ElevenLabs provider does not support --speed/--volume/--pitch/--endpoint")

                # Allow per-invocation override through env for simplicity.
                if args.output_format:
                    os.environ["ELEVENLABS_OUTPUT_FORMAT"] = args.output_format

                from api_sdk.tts_elevenlabs import synthesize_speech_elevenlabs_bytes, DEFAULT_ELEVENLABS_TTS_MODEL

                audio_bytes = synthesize_speech_elevenlabs_bytes(
                    text=args.text,
                    voice_id=(args.voice or os.environ.get("ELEVENLABS_VOICE_ID")),
                    model_id=(args.model or os.environ.get("ELEVENLABS_MODEL_ID") or DEFAULT_ELEVENLABS_TTS_MODEL),
                )
            else:
                from api_sdk.tts import synthesize_speech_bytes
                audio_bytes = synthesize_speech_bytes(
                    text=args.text,
                    voice=args.voice,
                    speed=args.speed,
                    volume=args.volume,
                    pitch=args.pitch,
                    sample_rate=args.sample_rate,
                    endpoint=(args.endpoint or os.environ.get('FLYTEK_URL') or 'wss://tts-api-sg.xf-yun.com/v2/tts'),
                    audio_format=args.format,
                    provider=args.provider,
                    model=args.model,
                )

            # Decide output path
            if args.format == 'wav':
                default_name = 'speech.wav'
            elif args.format == 'pcm':
                default_name = 'speech.pcm'
            else:
                default_name = 'speech.mp3'
            out_path = Path(args.output) if args.output else Path(default_name)
            out_path.parent.mkdir(parents=True, exist_ok=True)
            out_path.write_bytes(audio_bytes)

            info = {
                "bytes": len(audio_bytes) if args.format == 'pcm' else len(audio_bytes) - 44 if out_path.suffix.lower() == '.wav' else len(audio_bytes),
                "sample_rate": args.sample_rate,
                "format": args.format,
                "voice": args.voice or ('x_xiaoyang_story' if args.provider == 'iflytek' else ''),
                "provider": args.provider,
            }
            if args.provider == 'google':
                info["model"] = args.model or DEFAULT_GOOGLE_TTS_MODEL

            if args.json:
                emit_json({
                    "ok": True,
                    "command": "synthesize-speech",
                    "output_path": str(out_path),
                    "info": info,
                })
            elif not args.quiet:
                print(decorate(f"Audio saved to: {out_path}", use_color=use_color, icon="✅"))
            return EXIT_OK

        except ValueError as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "InvalidInput"})
            else:
                print(f"Invalid input: {e}", file=sys.stderr)
            return EXIT_INVALID_INPUT
        except Exception as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "UnknownError"})
            else:
                print(f"Failed to synthesize speech: {e}", file=sys.stderr)
                if getattr(args, 'verbose', False):
                    import traceback
                    traceback.print_exc()
            return EXIT_UNKNOWN_ERROR

    # Handle design-report command
    elif args.command == 'design-report':
        from api_sdk.design_report import (
            build_design_report_from_config,
            save_design_report,
        )

        try:
            if not args.json and not args.quiet:
                print(decorate(f"Building design report from config: {args.config}", use_color=use_color, icon="🧩"))
            image, png_path, pdf_path = build_design_report_from_config(args.config)

            # Overrides if provided
            if args.output or args.pdf:
                print("✏️ Applying output overrides")
                png_path, pdf_path = save_design_report(
                    image,
                    output_png=args.output or png_path,
                    output_pdf=args.pdf or pdf_path,
                )

            if args.json:
                emit_json({
                    "ok": True,
                    "command": "design-report",
                    "png_path": str(png_path) if png_path else None,
                    "pdf_path": str(pdf_path) if pdf_path else None,
                })
            elif not args.quiet:
                if png_path:
                    print(decorate(f"PNG saved to: {png_path}", use_color=use_color, icon="✅"))
                if pdf_path:
                    print(decorate(f"PDF saved to: {pdf_path}", use_color=use_color, icon="✅"))
                if not png_path and not pdf_path:
                    print("No outputs were saved (no output paths specified)")
            return EXIT_OK

        except FileNotFoundError as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "NotFound"})
            else:
                print(f"File not found: {e}", file=sys.stderr)
            return EXIT_NOT_FOUND

    # Handle remove-background command (ML-based)
    elif args.command == 'remove-background':
        from api_sdk.ml_background_remover import remove_background_ml

        try:
            if not args.json and not args.quiet:
                print(decorate(f"Removing background from: {args.image}", use_color=use_color, icon="🧼"))

            # Scenario routing
            selected_backend = args.backend
            if args.scenario != 'auto':
                selected_backend = 'trimap' if args.scenario == 'white-bg-multi' else 'rmbg'
            else:
                # Auto: analyze background edges; if near-white and uniform -> trimap
                try:
                    from api_sdk.background_analyzer import analyze_background_color
                    info = analyze_background_color(args.image)
                    dom = info.get('dominant_color', (255, 255, 255))
                    uniformity = float(info.get('uniformity', 0.0))
                    is_near_white = all(int(c) >= 245 for c in dom)
                    selected_backend = 'trimap' if (uniformity >= 0.7 and is_near_white) else 'rmbg'
                    if not args.json and not args.quiet and getattr(args, 'verbose', False):
                        print(f"   Auto route -> {selected_backend} (dominant={dom}, uniformity={uniformity:.2f})")
                except Exception:
                    selected_backend = 'rmbg'

            if selected_backend == 'trimap':
                from api_sdk.trimap_matting import build_trimap, apply_trimap_matting
                if not args.json and not args.quiet:
                    print("   Backend: trimap (KNN matting)")
                # Optionally synthesize bg mask from corners
                bg_mask_path = args.bg_mask
                if args.bg_corners:
                    from PIL import Image, ImageDraw
                    src_img = Image.open(args.image).convert('RGB')
                    w, h = src_img.size
                    mask = Image.new('L', (w, h), 0)
                    draw = ImageDraw.Draw(mask)
                    cw = max(1, int(w * max(0.0, min(args.corner_size, 0.5))))
                    ch = max(1, int(h * max(0.0, min(args.corner_size, 0.5))))
                    draw.rectangle([0, 0, cw, ch], fill=255)
                    draw.rectangle([w - cw, 0, w - 1, ch], fill=255)
                    draw.rectangle([0, h - ch, cw, h - 1], fill=255)
                    draw.rectangle([w - cw, h - ch, w - 1, h - 1], fill=255)
                    bg_mask_path = str(Path(args.image).with_suffix('').name + '_bg_corners.png')
                    mask.save(bg_mask_path)
                    if not args.json and not args.quiet:
                        print(f"   Auto bg-corners mask saved: {bg_mask_path}")

                # Build trimap with gray-rule defaults for white-bg-multi
                trimap = build_trimap(
                    args.image,
                    keep_mask_path=args.keep_mask,
                    bg_mask_path=bg_mask_path,
                    keep_dilate=args.trimap_keep_dilate,
                    bg_dilate=args.trimap_bg_dilate,
                    auto_bg_from_edges=False if args.bg_corners else args.auto_bg_from_edges,
                    white_bg_threshold=args.white_bg_threshold,
                    auto_fg_from_nonwhite=True,
                    nonwhite_threshold=20,
                    bg_mode=args.trimap_bg_mode,
                    grabcut_iters=args.grabcut_iters,
                )
                out = args.output or (Path(args.image).stem + "_transparent.png")
                output_path = apply_trimap_matting(args.image, trimap, out)
            else:
                if not args.json and not args.quiet:
                    print("   Backend: rmbg (ISNet/U2Net via rembg)")
                output_path = remove_background_ml(
                    image_path=args.image,
                    output_path=args.output,
                    # for subject scenario, default to soft matting if user didn't set flags
                    alpha_matting=True if args.scenario == 'subject' and not args.alpha_matting else args.alpha_matting,
                    alpha_matting_foreground_threshold=args.alpha_matting_foreground_threshold,
                    alpha_matting_background_threshold=args.alpha_matting_background_threshold,
                    alpha_matting_erosion_size=0 if args.scenario == 'subject' else args.alpha_matting_erosion_size,
                    model=args.model,
                    keep_mask_path=args.keep_mask,
                    keep_mask_dilate=args.keep_mask_dilate,
                    alpha_support_threshold=args.alpha_support_threshold,
                    nonwhite_keep_threshold=args.nonwhite_keep_threshold,
                    auto_keep_from_nonwhite=args.auto_keep_from_nonwhite,
                    auto_seed_erosion=args.auto_seed_erosion,
                )

            if args.json:
                emit_json({
                    "ok": True,
                    "command": "remove-background",
                    "output_path": str(output_path),
                    "backend": selected_backend,
                    "scenario": args.scenario,
                })
            elif not args.quiet:
                print(decorate(f"Transparent PNG saved to: {output_path}", use_color=use_color, icon="✅"))
            return EXIT_OK

        except FileNotFoundError as e:
            if args.json:
                emit_json({"ok": False, "error": str(e), "code": "NotFound"})
            else:
                print(f"File not found: {e}", file=sys.stderr)
            return EXIT_NOT_FOUND

    return EXIT_OK


if __name__ == '__main__':
    sys.exit(main())
