"""CLI interface for file-organizer.

This module defines the Typer-based command-line interface with commands for
organizing files, undoing operations, managing configuration, and managing
ignore patterns.
"""

from __future__ import annotations

import json
import time
from pathlib import Path
from typing import Annotated

import typer
from rich.console import Console
from rich.panel import Panel
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
from rich.table import Table
from rich.text import Text

from .config import Config, get_config_path, load_config
from .core import FileOrganizer, organize_directory
from .models import OrganizeMode, OrganizePlan, UndoLog

app = typer.Typer(
    name="file-organizer",
    help="Smart CLI tool to organize messy directories by file type, date, or size",
    add_completion=False,
    rich_markup_mode="rich",
    no_args_is_help=True,
)

console = Console()


def print_plan(plan: OrganizePlan, show_details: bool = False) -> None:
    """Print an organization plan in a nice table.

    Displays a Rich table with columns for index, source filename,
    destination filename, and file size. Shows a summary line with
    total file count and total size.

    Args:
        plan: OrganizePlan to display.
        show_details: Unused parameter for future expansion.
    """
    if plan.total_files() == 0:
        console.print("[yellow]No files to organize.[/yellow]")
        return

    table = Table(title=f"[bold]Organization Plan ({plan.mode.value})[/bold]")
    table.add_column("#", style="dim", width=4)
    table.add_column("Source", style="cyan")
    table.add_column("Destination", style="green")
    table.add_column("Size", style="magenta", justify="right")

    for i, action in enumerate(plan.actions, 1):
        src_name = action.source.name
        dst_name = action.destination.name
        size_str = format_size(action.size_bytes)
        table.add_row(str(i), src_name, dst_name, size_str)

    console.print(table)

    summary = Text()
    summary.append(f"Total: {plan.total_files()} files, ", style="bold")
    summary.append(f"{format_size(plan.total_size())}", style="bold magenta")
    if plan.dry_run:
        summary.append(" (DRY RUN)", style="bold yellow")
    console.print(summary)


def format_size(size_bytes: int) -> str:
    """Format bytes as human-readable string.

    Converts bytes to KB, MB, GB, or TB with one decimal place.

    Args:
        size_bytes: Size in bytes.

    Returns:
        Formatted string (e.g., "1.5 MB", "1024 B", "2.0 GB").
    """
    size_float = float(size_bytes)
    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if size_float < 1024:
            return f"{size_float:.1f} {unit}"
        size_float /= 1024
    return f"{size_float:.1f} PB"


def confirm_action(message: str) -> bool:
    """Ask for user confirmation via Typer's confirm prompt.

    Args:
        message: Prompt message to display.

    Returns:
        True if user confirms, False otherwise.
    """
    return typer.confirm(message)


def print_category_summary(plan: OrganizePlan) -> None:
    """Print a summary grouped by destination category.

    Aggregates file count and total size per destination folder
    (category/date/size bucket) and displays as a Rich table.
    Only shown if there are multiple categories.

    Args:
        plan: OrganizePlan to summarize.
    """
    from collections import defaultdict

    # Map category name -> (count, total_size)
    category_stats: dict[str, tuple[int, int]] = defaultdict(lambda: (0, 0))
    for action in plan.actions:
        cat = action.destination.parent.name
        count, size = category_stats[cat]
        category_stats[cat] = (count + 1, size + action.size_bytes)

    # Skip if only one category (not useful)
    if len(category_stats) <= 1:
        return

    table = Table(title="[bold]Summary by Category[/bold]")
    table.add_column("Category", style="cyan")
    table.add_column("Files", justify="right", style="green")
    table.add_column("Total Size", justify="right", style="magenta")

    for cat, (count, size) in sorted(category_stats.items()):
        table.add_row(cat, str(count), format_size(size))

    console.print(table)


def _extend_plan_recursive(organizer: FileOrganizer, source_dir: Path, plan: OrganizePlan) -> None:
    """Extend plan with files from subdirectories.

    For each subdirectory, creates a sub-plan and merges its actions
    into the main plan. Used when --recursive flag is set.

    Args:
        organizer: FileOrganizer instance to use for planning.
        source_dir: Root directory being organized.
        plan: Main plan to extend with subdirectory actions.
    """
    for subdir in source_dir.rglob("*"):
        if subdir.is_dir() and subdir != source_dir:
            sub_plan = organizer.plan_organize(subdir, plan.mode, True)
            for action in sub_plan.actions:
                plan.add_action(action.source, action.destination, action.size_bytes)


def execute_with_progress(organizer: FileOrganizer, plan: OrganizePlan) -> UndoLog:
    """Execute plan with a Rich progress bar.

    Iterates through all actions, moves files using os.replace (atomic on
    same filesystem), updates progress bar, and builds an UndoLog for
    potential reversal.

    Args:
        organizer: FileOrganizer instance (used for config access).
        plan: OrganizePlan with actions to execute.

    Returns:
        UndoLog with success status and any error message.
    """
    undo_log = UndoLog(plan=plan)

    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        BarColumn(),
        TaskProgressColumn(),
        console=console,
    ) as progress:
        task = progress.add_task("[cyan]Moving files...", total=len(plan.actions))

        for action in plan.actions:
            try:
                src = action.source.resolve()
                dst = action.destination.resolve()

                if not src.exists():
                    raise FileNotFoundError(f"Source file not found: {src}")

                dst.parent.mkdir(parents=True, exist_ok=True)

                import os

                os.replace(src, dst)

                progress.update(task, advance=1, description=f"[cyan]Moving {src.name}")

            except Exception as e:
                undo_log.success = False
                undo_log.error = str(e)
                break

    return undo_log


@app.command()
def organize(
    path: Annotated[
        Path,
        typer.Argument(
            help="Directory to organize",
            exists=True,
            file_okay=False,
            dir_okay=True,
            readable=True,
            resolve_path=True,
        ),
    ] = Path("."),
    by: Annotated[
        OrganizeMode,
        typer.Option(
            "--by",
            "-b",
            help="Organization mode (type, date, size)",
            case_sensitive=False,
        ),
    ] = OrganizeMode.TYPE,
    apply: Annotated[
        bool,
        typer.Option(
            "--apply",
            "-a",
            help="Actually move files (default is dry-run)",
        ),
    ] = False,
    dry_run: Annotated[
        bool,
        typer.Option(
            "--dry-run/--no-dry-run",
            help="Show what would be done without making changes",
        ),
    ] = True,
    yes: Annotated[
        bool,
        typer.Option(
            "--yes",
            "-y",
            help="Skip confirmation prompt",
        ),
    ] = False,
    recursive: Annotated[
        bool,
        typer.Option(
            "--recursive",
            "-r",
            help="Organize files in subdirectories recursively",
        ),
    ] = False,
    export_plan: Annotated[
        Path | None,
        typer.Option(
            "--export-plan",
            help="Export organization plan to JSON file",
            file_okay=True,
            dir_okay=False,
            writable=True,
        ),
    ] = None,
) -> None:
    """Organize files in a directory by type, date, or size.

    By default, runs in dry-run mode showing what would happen.
    Use --apply to actually move files.
    Use --recursive to include files in subdirectories.
    Use --export-plan to save the plan as JSON for review.
    """
    # Determine dry_run: --apply overrides --dry-run, config default applies if neither
    config = Config(load_config())
    effective_dry_run = dry_run and not apply
    if not dry_run and not apply:
        effective_dry_run = config.dry_run_by_default

    console.print(
        Panel.fit(
            f"[bold]Organizing:[/bold] {path}\n"
            f"[bold]Mode:[/bold] {by.value}\n"
            f"[bold]Recursive:[/bold] {'Yes' if recursive else 'No'}\n"
            f"[bold]Dry run:[/bold] {'Yes' if effective_dry_run else 'No'}",
            title="File Organizer",
        )
    )

    # Build plan (supports recursive via config)
    plan, _ = organize_directory(path, by, True, config)

    if recursive:
        # Extend plan with files from subdirectories
        organizer = FileOrganizer(config)
        config._data["organize"]["create_subdirs"] = False  # Keep flat in category dirs
        _extend_plan_recursive(organizer, path, plan)

    if plan.total_files() == 0:
        console.print("[green]No files to organize![/green]")
        return

    print_plan(plan)
    print_category_summary(plan)

    if export_plan:
        with export_plan.open("w", encoding="utf-8") as f:
            f.write(json.dumps(plan.to_dict(), indent=2))
        console.print(f"\n[green]Plan exported to {export_plan}[/green]")

    if effective_dry_run:
        console.print("\n[yellow]This was a dry run. Use --apply to execute.[/yellow]")
        return

    if not yes and not confirm_action("\nProceed with moving files?"):
        console.print("[yellow]Cancelled.[/yellow]")
        return

    # Execute the plan (real move) with progress bar
    plan.dry_run = False
    organizer = FileOrganizer(config)
    undo_log = execute_with_progress(organizer, plan)

    if undo_log.success:
        console.print(f"\n[green]✓ Successfully organized {plan.total_files()} files![/green]")
        console.print("[dim]Undo log saved. Run 'file-organizer undo' to revert.[/dim]")
    else:
        console.print(f"\n[red]✗ Organization failed: {undo_log.error}[/red]")
        raise typer.Exit(1)


@app.command()
def undo() -> None:
    """Undo the last organization operation.

    Reads the undo log, shows what will be reverted, asks for confirmation,
    then moves files back to their original locations. Removes the undo log
    and cleans up empty directories on success.
    """
    organizer = FileOrganizer(Config(load_config()))
    undo_log = organizer.get_last_undo_log()

    if not undo_log:
        console.print("[yellow]No previous organization to undo.[/yellow]")
        return

    console.print(
        Panel.fit(
            f"[bold]Last operation:[/bold] {undo_log.plan.mode.value}\n"
            f"[bold]Files:[/bold] {undo_log.plan.total_files()}\n"
            f"[bold]Time:[/bold] {time_str(undo_log.completed_at)}",
            title="Undo Last Operation",
        )
    )

    if not confirm_action("Undo this operation?"):
        console.print("[yellow]Cancelled.[/yellow]")
        return

    result = organizer.undo_last()

    if result and result.success:
        console.print("[green]✓ Successfully undone![/green]")
    else:
        error = result.error if result else "Unknown error"
        console.print(f"[red]✗ Undo failed: {error}[/red]")
        raise typer.Exit(1)


@app.command(name="config")
def config_cmd(
    show: Annotated[
        bool,
        typer.Option("--show", "-s", help="Show current configuration"),
    ] = False,
    set_key: Annotated[
        str | None,
        typer.Option("--set", help="Set a config value (format: key=value)"),
    ] = None,
    reset: Annotated[
        bool,
        typer.Option("--reset", help="Reset to default configuration"),
    ] = False,
) -> None:
    """Manage configuration.

    --show: Display current config with file path.
    --set KEY=VALUE: Set a config value (e.g., organize.default_mode=date).
    --reset: Delete config file to restore defaults.
    """
    config_path = get_config_path()

    if reset:
        if confirm_action("Reset configuration to defaults?"):
            config_path.unlink(missing_ok=True)
            console.print("[green]Configuration reset to defaults.[/green]")
        return

    if show or (not set_key and not reset):
        config = Config(load_config())
        table = Table(title="Current Configuration")
        table.add_column("Key", style="cyan")
        table.add_column("Value", style="green")

        def add_rows(d: dict, prefix: str = "") -> None:
            """Recursively add config rows to table with dot notation keys."""
            for k, v in d.items():
                key = f"{prefix}{k}"
                if isinstance(v, dict):
                    add_rows(v, f"{key}.")
                else:
                    table.add_row(key, str(v))

        add_rows(config.to_dict())
        console.print(table)
        console.print(f"\n[dim]Config file: {config_path}[/dim]")
        return

    if set_key:
        if "=" not in set_key:
            console.print("[red]Format: key=value[/red]")
            raise typer.Exit(1)

        key, value = set_key.split("=", 1)
        # Try to parse value as JSON for proper types (bool, int, list, etc.)
        import json

        try:
            parsed_value = json.loads(value)
        except json.JSONDecodeError:
            # Try boolean
            if value.lower() in ("true", "false"):
                parsed_value = value.lower() == "true"
            # Try number
            elif value.isdigit():
                parsed_value = int(value)
            else:
                parsed_value = value

        config = Config(load_config())
        config.set(key, parsed_value)
        console.print(f"[green]Set {key} = {parsed_value}[/green]")


@app.command()
def ignore(
    add: Annotated[
        str | None,
        typer.Option("--add", "-a", help="Add ignore pattern"),
    ] = None,
    remove: Annotated[
        str | None,
        typer.Option("--remove", "-r", help="Remove ignore pattern"),
    ] = None,
    list_patterns: Annotated[
        bool,
        typer.Option("--list", "-l", help="List ignore patterns"),
    ] = False,
) -> None:
    """Manage ignore patterns.

    --list: Show all current ignore patterns.
    --add PATTERN: Add a glob pattern to ignore (e.g., '*.bak').
    --remove PATTERN: Remove a previously added pattern.
    """
    config = Config(load_config())

    if list_patterns or (not add and not remove):
        table = Table(title="Ignore Patterns")
        table.add_column("Pattern", style="cyan")
        for pattern in config.ignore_patterns:
            table.add_row(pattern)
        console.print(table)
        return

    if add:
        patterns = config.ignore_patterns
        if add not in patterns:
            patterns.append(add)
            config.set("ignore.patterns", patterns)
            console.print(f"[green]Added ignore pattern: {add}[/green]")
        else:
            console.print(f"[yellow]Pattern already exists: {add}[/yellow]")

    if remove:
        patterns = config.ignore_patterns
        if remove in patterns:
            patterns.remove(remove)
            config.set("ignore.patterns", patterns)
            console.print(f"[green]Removed ignore pattern: {remove}[/green]")
        else:
            console.print(f"[yellow]Pattern not found: {remove}[/yellow]")


@app.command()
def version() -> None:
    """Show version information."""
    from . import __author__, __license__, __version__

    console.print(f"file-organizer v{__version__}")
    console.print(f"Author: {__author__}")
    console.print(f"License: {__license__}")


def time_str(timestamp: float) -> str:
    """Format timestamp as readable string.

    Args:
        timestamp: Unix timestamp (seconds since epoch).

    Returns:
        Formatted string like '2024-01-15 14:30:00'.
    """
    return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp))


def main() -> None:
    """Entry point for the CLI."""
    app()


if __name__ == "__main__":
    main()
