#!/usr/bin/env python3
"""
Video and Image Converter
Converts between video and image formats using FFmpeg and Pillow.
"""

import re
import subprocess
import argparse
import sys
import threading
import time
from pathlib import Path
from typing import Optional

try:
    from PIL import Image
except ImportError:
    Image = None

try:
    from rich.console import Console
    from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn
    from rich.panel import Panel
    from rich import box
    RICH_AVAILABLE = True
except ImportError:
    RICH_AVAILABLE = False


# Supported video formats
VIDEO_FORMATS = {'.avi', '.mp4', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
# Supported image formats
IMAGE_FORMATS = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.webp', '.gif'}

# Reuse one console (force_terminal=True so Rich shows full UI even when not a real TTY, e.g. IDE terminals)
console = Console(force_terminal=True) if RICH_AVAILABLE else None

# Regex to parse ffmpeg stderr time=HH:MM:SS.ms
FFMPEG_TIME_RE = re.compile(r"time=(\d{2}):(\d{2}):(\d{2})\.(\d{2})")


def _time_to_seconds(match) -> float:
    h, m, s, cs = match.groups()
    return int(h) * 3600 + int(m) * 60 + int(s) + int(cs) / 100.0


def _get_video_duration_seconds(path: Path) -> Optional[float]:
    """Return video duration in seconds via ffprobe, or None if unavailable."""
    try:
        out = subprocess.run(
            [
                "ffprobe", "-v", "error", "-show_entries", "format=duration",
                "-of", "default=noprint_wrappers=1:nokey=1", str(path)
            ],
            capture_output=True,
            text=True,
            timeout=10,
        )
        if out.returncode == 0 and out.stdout.strip():
            return float(out.stdout.strip())
    except Exception:
        pass
    return None


def convert_video(
    input_path: str,
    output_path: Optional[str] = None,
    quality: str = "medium",
    crf: int = 22,
    progress_callback=None,
) -> str:
    """Convert a video to another format using FFmpeg."""
    inp = Path(input_path)

    if not inp.exists():
        raise FileNotFoundError(f"File not found: {input_path}")

    if inp.suffix.lower() not in VIDEO_FORMATS:
        raise ValueError(f"Unsupported video format: {inp.suffix}")

    if output_path is None:
        out = inp.with_suffix(".mp4")
    else:
        out = Path(output_path)
        out.parent.mkdir(parents=True, exist_ok=True)

    duration_sec = _get_video_duration_seconds(inp)
    cmd = [
        "ffmpeg", "-y", "-i", str(inp),
        "-c:v", "libx264", "-preset", quality, "-crf", str(crf),
        "-c:a", "aac", "-b:a", "192k",
        "-movflags", "+faststart",
        str(out)
    ]

    if progress_callback and duration_sec and duration_sec > 0:
        proc = subprocess.Popen(
            cmd,
            stderr=subprocess.PIPE,
            text=True,
            bufsize=1,
        )
        current = [0.0]  # mutable for closure

        def read_stderr():
            if proc.stderr:
                for line in iter(proc.stderr.readline, ""):
                    m = FFMPEG_TIME_RE.search(line)
                    if m:
                        current[0] = _time_to_seconds(m)
                        progress_callback(min(1.0, current[0] / duration_sec))

        t = threading.Thread(target=read_stderr)
        t.start()
        proc.wait()
        t.join(timeout=1)
        if proc.returncode != 0:
            raise RuntimeError("FFmpeg failed.")
        progress_callback(1.0)
        return str(out)

    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(f"FFmpeg failed:\n{result.stderr}")
    if progress_callback:
        progress_callback(1.0)
    return str(out)


def convert_image(
    input_path: str,
    output_path: Optional[str] = None,
    format: Optional[str] = None,
) -> str:
    """Convert an image to another format using Pillow."""
    if Image is None:
        raise ImportError("Pillow is not installed. Install with: pip install Pillow")

    inp = Path(input_path)

    if not inp.exists():
        raise FileNotFoundError(f"File not found: {input_path}")

    if inp.suffix.lower() not in IMAGE_FORMATS:
        raise ValueError(f"Unsupported image format: {inp.suffix}")

    if output_path is None:
        if inp.suffix.lower() in {'.jpg', '.jpeg'}:
            out = inp.with_suffix(".png")
            output_format = "PNG"
        else:
            out = inp.with_suffix(".jpg")
            output_format = "JPEG"
    else:
        out = Path(output_path)
        out.parent.mkdir(parents=True, exist_ok=True)
        if format:
            output_format = format.upper()
        else:
            ext = out.suffix.lower()
            format_map = {
                '.png': 'PNG', '.jpg': 'JPEG', '.jpeg': 'JPEG',
                '.bmp': 'BMP', '.tiff': 'TIFF', '.tif': 'TIFF',
                '.webp': 'WEBP', '.gif': 'GIF'
            }
            output_format = format_map.get(ext, 'PNG')

    with Image.open(inp) as img:
        if output_format == 'JPEG' and img.mode in ('RGBA', 'LA', 'P'):
            rgb_img = Image.new('RGB', img.size, (255, 255, 255))
            if img.mode == 'P':
                img = img.convert('RGBA')
            rgb_img.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
            img = rgb_img
        elif output_format == 'JPEG' and img.mode != 'RGB':
            img = img.convert('RGB')
        img.save(out, format=output_format, quality=95)

    return str(out)


def detect_file_type(file_path: Path) -> str:
    """Detect whether a file is video or image by extension."""
    ext = file_path.suffix.lower()
    if ext in VIDEO_FORMATS:
        return "video"
    elif ext in IMAGE_FORMATS:
        return "image"
    return "unknown"


def _run_with_progress_rich(convert_fn, task_desc: str, total: Optional[float], **kwargs):
    """Run convert_fn inside Rich Progress. total=None for spinner only."""
    if not RICH_AVAILABLE:
        return convert_fn(**{k: v for k, v in kwargs.items() if k != "progress_callback"})

    columns = [
        SpinnerColumn("dots", style="bold cyan"),
        TextColumn("[bold blue]{task.description}"),
        BarColumn(complete_style="cyan", finished_style="green"),
        TaskProgressColumn(),
        TimeRemainingColumn(),
    ]
    with Progress(*columns, console=console) as progress:
        if total and total > 0:
            task = progress.add_task(task_desc, total=100)
            def cb(p: float):
                progress.update(task, completed=min(100, int(p * 100)))
            kwargs["progress_callback"] = cb
        else:
            task = progress.add_task(task_desc, total=None)  # indeterminate spinner
        result = [None]
        err = [None]

        def run():
            try:
                result[0] = convert_fn(**kwargs)
            except Exception as e:
                err[0] = e

        t = threading.Thread(target=run)
        t.start()
        while t.is_alive():
            progress.refresh()
            time.sleep(0.05)
        t.join()
        if total and total > 0:
            progress.update(task, completed=100)
        if err[0]:
            raise err[0]
        return result[0]


def main():
    """CLI entry point."""
    parser = argparse.ArgumentParser(
        prog="converter",
        description="Convert videos and images between formats. Videos require FFmpeg; images use Pillow (included). Developed by L3CHUGU1T4.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
INPUT FILES
  One or more video or image files. Formats are detected by extension.

  Video: .avi .mp4 .mov .mkv .webm .flv .wmv .m4v
  Image: .jpg .jpeg .png .bmp .tiff .tif .webp .gif

OUTPUT
  If -o/--output is omitted:
    • Videos are written as <name>.mp4 in the same directory.
    • JPEG/JPG → <name>.png; other images → <name>.jpg

  Use -o/--output to set an explicit output path (only when converting a single file).

VIDEO OPTIONS
  --quality   Encoding speed vs compression: ultrafast | fast | medium | slow | veryslow (default: medium).
  --crf       Quality (18–28). Lower = better quality, larger file (default: 22).

EXAMPLES
  converter video.avi
  converter video.avi -o output.mov
  converter image.jpg
  converter image.png -o output.jpg
  converter video.avi --quality slow --crf 18
  converter file1.avi file2.jpg file3.png

  Developed by L3CHUGU1T4
        """,
    )

    parser.add_argument("files", nargs="+", help="One or more files to convert")
    parser.add_argument("-o", "--output", metavar="PATH", help="Output file path (single file only)")
    parser.add_argument(
        "--quality",
        choices=["ultrafast", "fast", "medium", "slow", "veryslow"],
        default="medium",
        help="Video encoding preset (default: medium)",
    )
    parser.add_argument("--crf", type=int, default=22, metavar="N", help="Video quality 18–28 (default: 22)")
    parser.add_argument("--no-rich", action="store_true", help="Disable rich UI (plain text output)")

    args = parser.parse_args()
    use_rich = RICH_AVAILABLE and not args.no_rich

    if args.output and len(args.files) > 1:
        parser.error("--output may only be used when converting a single file")

    if use_rich and console:
        console.print()
        console.print(Panel(
            "[bold cyan]converter[/]  [dim]video & image converter[/]",
            box=box.ROUNDED,
            border_style="cyan",
            padding=(0, 1),
        ))
        console.print()

    success_count = 0
    error_count = 0
    errors_list = []

    for file_path in args.files:
        inp = Path(file_path)
        file_type = detect_file_type(inp)

        if file_type == "unknown":
            if use_rich and console:
                console.print(f"  [red]✗[/] Unsupported format: [bold]{inp.name}[/] (extension: {inp.suffix})")
            else:
                print(f"✗ Unsupported format: {inp.name} (extension: {inp.suffix})")
            error_count += 1
            continue

        try:
            if file_type == "video":
                duration = _get_video_duration_seconds(inp) if use_rich else None
                out_path = _run_with_progress_rich(
                    convert_video,
                    f"[cyan]Video[/] [bold]{inp.name}[/] → [green]{inp.with_suffix('.mp4').name if not args.output else Path(args.output).name}[/]",
                    duration,
                    input_path=str(inp),
                    output_path=args.output,
                    quality=args.quality,
                    crf=args.crf,
                ) if use_rich else convert_video(
                    str(inp), args.output, quality=args.quality, crf=args.crf
                )
                success_count += 1
                if use_rich and console:
                    console.print(f"  [green]✓[/] [dim]{out_path}[/]")
                else:
                    print(f"✓ Video converted: {out_path}")
            else:
                out_path = _run_with_progress_rich(
                    convert_image,
                    f"[cyan]Image[/] [bold]{inp.name}[/] → [green]{Path(args.output or '').name or (inp.with_suffix('.png').name if inp.suffix.lower() in {'.jpg','.jpeg'} else inp.with_suffix('.jpg').name)}[/]",
                    None,
                    input_path=str(inp),
                    output_path=args.output,
                ) if use_rich else convert_image(str(inp), args.output)
                success_count += 1
                if use_rich and console:
                    console.print(f"  [green]✓[/] [dim]{out_path}[/]")
                else:
                    print(f"✓ Image converted: {out_path}")
        except FileNotFoundError as e:
            error_count += 1
            errors_list.append((inp.name, str(e)))
            if use_rich and console:
                console.print(f"  [red]✗[/] [red]{e}[/]")
            else:
                print(f"✗ Error: {e}")
        except (ValueError, RuntimeError) as e:
            error_count += 1
            errors_list.append((inp.name, str(e)))
            if use_rich and console:
                console.print(f"  [red]✗[/] [red]{e}[/]")
            else:
                print(f"✗ Error: {e}")
        except Exception as e:
            error_count += 1
            errors_list.append((inp.name, str(e)))
            if use_rich and console:
                console.print(f"  [red]✗[/] Unexpected: [red]{e}[/]")
            else:
                print(f"✗ Unexpected error: {e}")

    total = success_count + error_count
    if use_rich and console:
        console.print()
        style = "green" if error_count == 0 else "yellow"
        console.print(Panel(
            f"[bold]Processed[/] [white]{total}[/]  [green]Ok[/] [white]{success_count}[/]  [red]Errors[/] [white]{error_count}[/]",
            title="[bold]Summary[/]",
            title_align="left",
            border_style=style,
            box=box.ROUNDED,
            padding=(0, 1),
        ))
        console.print()
    else:
        print(f"\n{'='*50}")
        print(f"Processed: {total} | Ok: {success_count} | Errors: {error_count}")

    sys.exit(0 if error_count == 0 else 1)


if __name__ == "__main__":
    main()
