"""Change-url command: rewrite git remote URLs to a target protocol."""

from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from pathlib import Path
from typing import Annotated

import typer
from rich.console import Console
from rich.progress import (
    BarColumn,
    Progress,
    SpinnerColumn,
    TaskProgressColumn,
    TextColumn,
)
from rich.table import Table

from repo_sync_kitty.config.loader import (
    ManifestError,
    ResolvedProject,
    load_manifest,
    resolve_all_projects,
)
from repo_sync_kitty.exitcodes import ExitCode
from repo_sync_kitty.git.operations import GitError, RepoManager
from repo_sync_kitty.util.git import (
    convert_url_to_https,
    convert_url_to_ssh,
    is_git_repo,
)

console = Console()
error_console = Console(stderr=True)


@dataclass
class ChangeResult:
    """Result of changing a single repository's remote URL."""

    project: ResolvedProject
    action: str  # "changed", "skipped", "error"
    old_url: str = ""
    new_url: str = ""
    message: str = ""


def _change_repo_url(
    project: ResolvedProject,
    root_path: Path,
    protocol: str,
    dry_run: bool = False,
) -> ChangeResult:
    repo_path = root_path / project.path

    if not is_git_repo(repo_path):
        return ChangeResult(
            project=project,
            action="skipped",
            message="not cloned",
        )

    mgr = RepoManager(repo_path)
    remote_urls = mgr.get_remote_urls()

    if "origin" not in remote_urls:
        return ChangeResult(
            project=project,
            action="skipped",
            message="no origin remote",
        )

    old_url = remote_urls["origin"]
    converter = convert_url_to_https if protocol == "https" else convert_url_to_ssh

    try:
        new_url = converter(old_url)
    except ValueError as e:
        return ChangeResult(
            project=project,
            action="error",
            old_url=old_url,
            message=str(e),
        )

    if old_url == new_url:
        return ChangeResult(
            project=project,
            action="skipped",
            old_url=old_url,
            message="already using target protocol",
        )

    if dry_run:
        return ChangeResult(
            project=project,
            action="changed",
            old_url=old_url,
            new_url=new_url,
            message="would change (dry run)",
        )

    try:
        mgr.set_remote_url("origin", new_url)
    except GitError as e:
        return ChangeResult(
            project=project,
            action="error",
            old_url=old_url,
            new_url=new_url,
            message=str(e),
        )

    return ChangeResult(
        project=project,
        action="changed",
        old_url=old_url,
        new_url=new_url,
    )


def change_url(
    ctx: typer.Context,
    protocol: Annotated[
        str,
        typer.Option("--protocol", "-p", help="Target protocol: https or ssh"),
    ],
    remote: Annotated[
        str | None,
        typer.Option("--remote", "-r", help="Only change repos using this remote"),
    ] = None,
    manifest: Annotated[
        Path | None,
        typer.Option("--manifest", "-m", help="Path to manifest.toml file"),
    ] = None,
    dry_run: Annotated[
        bool,
        typer.Option("--dry-run", "-n", help="Show what would be changed"),
    ] = False,
    path: Annotated[
        str | None,
        typer.Argument(help="Specific repo path to change"),
    ] = None,
) -> None:
    """Rewrite git remote URLs to use a different protocol (https or ssh)."""
    if protocol not in ("https", "ssh"):
        error_console.print(
            f"[red]Error:[/red] --protocol must be 'https' or 'ssh', got '{protocol}'"
        )
        raise typer.Exit(ExitCode.USAGE_ERROR)

    if not remote and not path:
        error_console.print(
            "[red]Error:[/red] Must specify --remote or a repo path (or both)"
        )
        raise typer.Exit(ExitCode.USAGE_ERROR)

    # Find manifest
    global_manifest = ctx.obj.get("manifest") if ctx.obj else None
    manifest_path = manifest or global_manifest or Path("manifest.toml")
    if not manifest_path.exists():
        error_console.print(f"[red]Error:[/red] Manifest not found: {manifest_path}")
        raise typer.Exit(ExitCode.USAGE_ERROR)

    try:
        mf = load_manifest(manifest_path)
    except ManifestError as e:
        error_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(ExitCode.USAGE_ERROR) from e

    root_path = mf.common.root_path
    projects = resolve_all_projects(mf)

    # Filter by remote name
    if remote:
        projects = [p for p in projects if p.remote_name == remote]
        if not projects:
            error_console.print(
                f"[yellow]No projects found using remote '{remote}'[/yellow]"
            )
            return

    # Filter by specific path
    if path:
        projects = [p for p in projects if str(p.path) == path]
        if not projects:
            error_console.print(f"[yellow]No project found with path '{path}'[/yellow]")
            return

    # Process repos
    results: list[ChangeResult] = []
    num_workers = mf.common.parallelism

    with Progress(
        SpinnerColumn(),
        BarColumn(),
        TaskProgressColumn(),
        TextColumn("[progress.description]{task.description}"),
        console=console,
    ) as progress:
        task = progress.add_task("Changing URLs...", total=len(projects))

        with ThreadPoolExecutor(max_workers=num_workers) as executor:
            futures = {
                executor.submit(
                    _change_repo_url, project, root_path, protocol, dry_run
                ): project
                for project in projects
            }

            for future in as_completed(futures):
                result = future.result()
                results.append(result)
                progress.update(
                    task, advance=1, description=f"Processed {result.project.path}"
                )

    # Sort by original project order
    project_order = {str(p.path): i for i, p in enumerate(projects)}
    results.sort(key=lambda r: project_order.get(str(r.project.path), 0))

    # Display results table (only changed and errors)
    changed = [r for r in results if r.action == "changed"]
    errors = [r for r in results if r.action == "error"]
    skipped = [r for r in results if r.action == "skipped"]

    if changed:
        table = Table(title="URL Changes" + (" (dry run)" if dry_run else ""))
        table.add_column("Path", style="cyan")
        table.add_column("Old URL", style="red")
        table.add_column("New URL", style="green")

        for r in changed:
            table.add_row(str(r.project.path), r.old_url, r.new_url)

        console.print(table)

    if errors:
        console.print(f"\n[red]Errors ({len(errors)}):[/red]")
        for r in errors:
            console.print(f"  [red]✗[/red] {r.project.path}: {r.message}")

    # Summary
    parts = []
    if changed:
        verb = "would change" if dry_run else "changed"
        parts.append(f"[green]{len(changed)} {verb}[/green]")
    if skipped:
        parts.append(f"[dim]{len(skipped)} skipped[/dim]")
    if errors:
        parts.append(f"[red]{len(errors)} failed[/red]")

    if parts:
        console.print("\n" + ", ".join(parts))

    if errors:
        raise typer.Exit(ExitCode.GENERAL_ERROR)
