#!/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

try:
    from rich_argparse import RawDescriptionRichHelpFormatter
    RICH_HELP_AVAILABLE = True
except ImportError:
    RawDescriptionRichHelpFormatter = None
    RICH_HELP_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]


_HELP_EPILOG_PLAIN = """
SUPPORTED FORMATS
  Video (input):  AVI, MP4, MOV, MKV, WebM, FLV, WMV, M4V
  Image (input):  JPEG, JPG, PNG, BMP, TIFF, TIF, WebP, GIF

CONVERSION LIST – How to convert each type
  Without -o, output is in the same directory with the default extension below.

  VIDEO (any → any; default output: MP4)
    AVI  → MP4 (default)    converter clip.avi
    AVI  → MOV             converter clip.avi -o clip.mov
    MKV  → MP4             converter file.mkv
    WebM → MP4             converter file.webm
    FLV  → MP4             converter file.flv
    WMV  → MP4             converter file.wmv
    M4V  → MP4             converter file.m4v
    Video quality:         converter video.avi --quality slow --crf 18

  IMAGE (defaults: JPEG→PNG, others→JPEG)
    JPEG/JPG → PNG (default)   converter photo.jpg
    PNG      → JPEG (default)  converter image.png
    PNG      → WebP            converter image.png -o image.webp
    BMP      → JPEG            converter art.bmp
    TIFF/TIF → JPEG            converter scan.tiff
    WebP     → JPEG            converter pic.webp
    GIF      → JPEG            converter anim.gif
    PNG      → BMP             converter image.png -o image.bmp
    JPEG     → TIFF            converter photo.jpg -o photo.tiff

OUTPUT
  -o, --output PATH   Set output path (allowed only when converting one file).
  Omit -o             Videos → <name>.mp4; JPEG/JPG → <name>.png; other images → <name>.jpg.

VIDEO OPTIONS (only for video conversion)
  --quality   ultrafast | fast | medium | slow | veryslow  (default: medium)
  --crf       Quality 18–28; lower = better, 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
"""

_HELP_EPILOG_RICH = """
[bold cyan]SUPPORTED FORMATS[/]
  [yellow]Video[/] (input):  AVI, MP4, MOV, MKV, WebM, FLV, WMV, M4V
  [yellow]Image[/] (input):  JPEG, JPG, PNG, BMP, TIFF, TIF, WebP, GIF

[bold cyan]CONVERSION LIST[/] – How to convert each type
  Without [bold]-o[/], output is in the same directory with the default extension below.

  [bold yellow]VIDEO[/] (any → any; default output: MP4)
    AVI  → MP4 (default)    [green]converter clip.avi[/]
    AVI  → MOV              [green]converter clip.avi -o clip.mov[/]
    MKV  → MP4              [green]converter file.mkv[/]
    WebM → MP4              [green]converter file.webm[/]
    FLV  → MP4              [green]converter file.flv[/]
    WMV  → MP4              [green]converter file.wmv[/]
    M4V  → MP4              [green]converter file.m4v[/]
    Video quality:          [green]converter video.avi --quality slow --crf 18[/]

  [bold yellow]IMAGE[/] (defaults: JPEG→PNG, others→JPEG)
    JPEG/JPG → PNG (default)   [green]converter photo.jpg[/]
    PNG      → JPEG (default)  [green]converter image.png[/]
    PNG      → WebP            [green]converter image.png -o image.webp[/]
    BMP      → JPEG            [green]converter art.bmp[/]
    TIFF/TIF → JPEG            [green]converter scan.tiff[/]
    WebP     → JPEG            [green]converter pic.webp[/]
    GIF      → JPEG            [green]converter anim.gif[/]
    PNG      → BMP             [green]converter image.png -o image.bmp[/]
    JPEG     → TIFF            [green]converter photo.jpg -o photo.tiff[/]

[bold cyan]OUTPUT[/]
  [bold]-o, --output PATH[/]   Set output path (allowed only when converting one file).
  Omit [bold]-o[/]             Videos → <name>.mp4; JPEG/JPG → <name>.png; other images → <name>.jpg.

[bold cyan]VIDEO OPTIONS[/] (only for video conversion)
  [bold]--quality[/]   ultrafast | fast | medium | slow | veryslow  (default: medium)
  [bold]--crf[/]       Quality 18–28; lower = better, larger file     (default: 22)

[bold cyan]EXAMPLES[/]
  [green]converter video.avi[/]
  [green]converter video.avi -o output.mov[/]
  [green]converter image.jpg[/]
  [green]converter image.png -o output.jpg[/]
  [green]converter video.avi --quality slow --crf 18[/]
  [green]converter file1.avi file2.jpg file3.png[/]

  [dim]Developed by L3CHUGU1T4[/]
"""


def main():
    """CLI entry point."""
    use_rich_help = RICH_HELP_AVAILABLE and RawDescriptionRichHelpFormatter is not None
    help_formatter = RawDescriptionRichHelpFormatter if use_rich_help else argparse.RawDescriptionHelpFormatter
    description_plain = "Convert videos and images between formats. Videos require FFmpeg; images use Pillow (included). Developed by L3CHUGU1T4."
    description_rich = "[bold]Convert videos and images[/] between formats. Videos require [cyan]FFmpeg[/]; images use [cyan]Pillow[/] (included). [dim]Developed by L3CHUGU1T4.[/]"

    parser = argparse.ArgumentParser(
        prog="converter",
        description=description_rich if use_rich_help else description_plain,
        formatter_class=help_formatter,
        epilog=_HELP_EPILOG_RICH if use_rich_help else _HELP_EPILOG_PLAIN,
    )

    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()
