"""
CLI commands for sync operations.

This module contains the sync, doctor, rollback, and help commands.
"""

from pathlib import Path

import click
from rich.console import Console

from persuade.backup import BackupError, RestoreError

RICH_AVAILABLE = True


def get_git_root(path: Path) -> Path | None:
    """Get the root directory of the git repository containing the given path.

    Uses `git rev-parse --show-toplevel` to find the repository root.

    Args:
        path: Directory path within a git repository

    Returns:
        Path to the git repository root, or None if not in a git repository
    """
    import subprocess

    from persuade.constants import GIT_REV_PARSE_TIMEOUT

    try:
        result = subprocess.run(
            ["git", "rev-parse", "--show-toplevel"],
            cwd=path,
            capture_output=True,
            text=True,
            check=False,
            timeout=GIT_REV_PARSE_TIMEOUT,
        )
        if result.returncode == 0:
            return Path(result.stdout.strip())
        return None
    except (OSError, subprocess.SubprocessError, subprocess.TimeoutExpired):
        return None


def get_persuade_source_path() -> Path:
    """Get the path to the persuade template repository (source for sync).

    This function determines where to find the persuade template files that will
    be synced to target projects.

    Resolution strategy:
        1. If this package is checked out from git (development mode), use the
           git repository root. This ensures git operations (branches, commits, PRs)
           work correctly against the repository root.
        2. If installed via pip (no git repository), fall back to the parent of the
           package directory. The directory structure is:
           persuade/cli_sync.py -> persuade/ -> repo root (or site-packages parent)

    Returns:
        Path to the persuade source/template directory
    """
    package_dir = Path(__file__).parent
    git_root = get_git_root(package_dir)
    if git_root is not None:
        return git_root
    # Fallback for pip-installed package: go up one level from package dir
    return package_dir.parent


def resolve_project_root(
    path: Path,
    allow_non_git: bool,
    no_resolve_root: bool,
    console: Console,
    verbose: bool = False,
) -> Path:
    """Resolve the project root directory and validate git repository.

    By default, persuade expects to run at the git repository root.
    This function:
    1. Validates the path is inside a git repository (unless --allow-non-git)
    2. Resolves to the git root (unless --no-resolve-root)

    Args:
        path: Directory path to validate and resolve
        allow_non_git: If True, skip git validation
        no_resolve_root: If True, use the path as-is without resolving to git root
        console: Rich console for output
        verbose: If True, print informational messages

    Returns:
        The resolved project root path

    Raises:
        click.Abort: If path is not in a git repo and allow_non_git is False
    """
    # Check if we're in a git repository
    git_root = get_git_root(path)

    if git_root is None:
        if allow_non_git:
            # User explicitly allowed non-git, use path as-is
            if verbose:
                console.print(f"[yellow]⚠[/yellow] Running in non-git directory: {path}")
            return path
        console.print(
            f"[red]✗[/red] Error: {path} is not inside a git repository.\n"
            f"Persuade is designed to work with git repositories.\n"
            f"Use --allow-non-git flag to proceed anyway (not recommended)."
        )
        raise click.Abort()

    # We're in a git repository
    if no_resolve_root:
        # User explicitly wants to use the given path
        if verbose and path != git_root:
            console.print(f"[dim]Using specified path: {path} (git root: {git_root})[/dim]")
        return path

    # Resolve to git root
    if path != git_root:
        if verbose:
            console.print(f"[dim]Resolved to git root: {git_root} (from: {path})[/dim]")
        else:
            console.print(f"[dim]Using git root: {git_root}[/dim]")
    return git_root


def register_sync_commands(cli: click.Group) -> None:
    """Register sync-related commands with the CLI group."""

    @cli.command()
    @click.argument(
        "target_path",
        type=click.Path(exists=True, file_okay=False, path_type=Path),
        default=None,
        required=False,
    )
    @click.option("--dry-run", is_flag=True, help="Show changes without applying")
    @click.option("--strict", is_flag=True, help="Remove non-template permissions")
    @click.option("--force", is_flag=True, help="Skip confirmation prompts")
    @click.option("--no-backup", is_flag=True, help="Skip backup creation (NOT RECOMMENDED)")
    @click.option("--exclude", multiple=True, help="Exclude files matching pattern")
    @click.option(
        "--shadow", is_flag=True, help="Sync files but exclude from git (add to .gitignore)"
    )
    @click.option(
        "-e",
        "--editable",
        is_flag=True,
        help="Editable mode: symlink files instead of copying for live development",
    )
    @click.option(
        "--allow-non-git",
        is_flag=True,
        help="Allow syncing to non-git directories (not recommended)",
    )
    @click.option(
        "--no-resolve-root",
        is_flag=True,
        help="Don't auto-resolve to git repository root",
    )
    @click.option(
        "--force-update",
        is_flag=True,
        help="Force update COPY_IF_MISSING files even if they exist (overwrites customizations)",
    )
    @click.option(
        "--git-commit",
        is_flag=True,
        help="Create atomic git commits for synced files (requires --branch or "
        "--allow-current-branch)",
    )
    @click.option(
        "--branch",
        is_flag=True,
        help="Create a new branch for commits (persuade/sync-YYYY-MM-DD). Implies --git-commit",
    )
    @click.option(
        "--allow-current-branch",
        is_flag=True,
        help="Allow committing on current branch instead of requiring --branch",
    )
    @click.option(
        "--pr",
        is_flag=True,
        help="Create a PR after commits (requires --branch and gh CLI). "
        "Implies --branch and --git-commit",
    )
    @click.pass_context
    def sync(
        ctx: click.Context,
        target_path: Path | None,
        dry_run: bool,
        strict: bool,
        force: bool,
        no_backup: bool,
        exclude: tuple[str, ...],
        shadow: bool,
        editable: bool,
        allow_non_git: bool,
        no_resolve_root: bool,
        force_update: bool,
        git_commit: bool,
        branch: bool,
        allow_current_branch: bool,
        pr: bool,
    ) -> None:
        """Sync automation to target project (defaults to git repository root)."""
        console = Console()
        verbose = ctx.obj["verbose"]
        quiet = ctx.obj["quiet"]

        # Handle flag implications: --pr implies --branch, --branch implies --git-commit
        if pr:
            branch = True
        if branch:
            git_commit = True

        # Validate git commit flags
        if git_commit and not branch and not allow_current_branch:
            console.print(
                "[red]✗[/red] --git-commit requires either --branch or --allow-current-branch"
            )
            console.print("  [dim]--branch:[/dim] Create a new branch (recommended, safer)")
            console.print(
                "  [dim]--allow-current-branch:[/dim] Commit on current branch (use with caution)"
            )
            raise click.Abort()

        # Default to current directory if not provided, then resolve to git root
        initial_path = target_path or Path.cwd()
        resolved_path = resolve_project_root(
            initial_path, allow_non_git, no_resolve_root, console, verbose
        )

        try:
            # Import here to avoid circular dependencies
            from persuade.core import SyncOptions, sync_automation
            from persuade.reporter import SyncReporter

            reporter = SyncReporter(verbose=verbose)

            source_path = get_persuade_source_path()

            options = SyncOptions(
                dry_run=dry_run,
                mode="strict" if strict else "union",
                force=force,
                no_backup=no_backup,
                exclude=list(exclude),
                verbose=verbose,
                quiet=quiet,
                shadow=shadow,
                editable=editable,
                force_update=force_update,
                git_commit=git_commit,
                create_branch=branch,
                create_pr=pr,
            )

            result = sync_automation(source_path, resolved_path, options)

            if result.status == "success":
                reporter.print_success(
                    "Sync completed successfully",
                    details={
                        "Backup": result.backup_path,
                        "Files synced": result.files_synced,
                        "Permissions merged": result.permissions_merged,
                    }
                    if verbose
                    else None,
                )

                # Report git workflow results
                if result.git_workflow_result:
                    gwr = result.git_workflow_result
                    if gwr.commits_created > 0:
                        console.print(
                            f"\n[green]✓[/green] Created {gwr.commits_created} atomic commit(s)"
                        )
                    if gwr.branch_name:
                        console.print(f"  Branch: [cyan]{gwr.branch_name}[/cyan]")
                    if gwr.pr_url:
                        console.print(f"  PR: [cyan]{gwr.pr_url}[/cyan]")

            elif result.status == "partial":
                # Files synced but git workflow had issues
                console.print(
                    f"\n[yellow]⚠[/yellow] Sync completed with warnings: {result.message}"
                )
                if result.git_workflow_result:
                    gwr = result.git_workflow_result
                    if gwr.error:
                        console.print(f"  [yellow]Git error:[/yellow] {gwr.error}")
                    if gwr.branch_name:
                        console.print(f"  Branch: [cyan]{gwr.branch_name}[/cyan]")
                # Exit with non-zero status for partial success when git workflow was requested
                raise click.Abort()

            elif result.status == "dry_run":
                click.echo("\nDry run complete. Run without --dry-run to apply changes.")

        except (OSError, ValueError, BackupError) as e:
            if RICH_AVAILABLE:
                console.print(f"\n[red]✗[/red] Sync failed: {e}")
                if verbose:
                    console.print_exception()
            else:
                click.echo(f"\n✗ Sync failed: {e}", err=True)
            raise click.Abort() from e

    @cli.command()
    @click.argument(
        "target_path",
        type=click.Path(exists=True, file_okay=False, path_type=Path),
        default=None,
        required=False,
    )
    @click.option("--fix", is_flag=True, help="Automatically fix issues")
    @click.option("--check-only", is_flag=True, help="Only report issues, no fixes")
    @click.option(
        "--allow-non-git",
        is_flag=True,
        help="Allow checking non-git directories (not recommended)",
    )
    @click.option(
        "--no-resolve-root",
        is_flag=True,
        help="Don't auto-resolve to git repository root",
    )
    @click.pass_context
    def doctor(
        ctx: click.Context,
        target_path: Path | None,
        fix: bool,
        check_only: bool,
        allow_non_git: bool,
        no_resolve_root: bool,
    ) -> None:
        """Validate and fix misconfigurations (defaults to git repository root)."""
        console = Console()
        verbose = ctx.obj["verbose"]

        # Default to current directory if not provided, then resolve to git root
        initial_path = target_path or Path.cwd()
        resolved_path = resolve_project_root(
            initial_path, allow_non_git, no_resolve_root, console, verbose
        )

        try:
            from persuade.validators import doctor_command

            report = doctor_command(resolved_path, fix=fix, check_only=check_only)

            if report.all_passed:
                click.echo("\n✓ All checks passed!")
            else:
                click.echo(f"\n⚠ {report.failed} checks failed")
                raise click.Abort()

        except (OSError, ValueError) as e:
            click.echo(f"\nError: {e}", err=True)
            raise click.Abort() from e

    @cli.command("help-command")
    @click.argument(
        "target_path",
        type=click.Path(exists=True, file_okay=False, path_type=Path),
        default=None,
        required=False,
    )
    @click.option(
        "--allow-non-git",
        is_flag=True,
        help="Allow checking non-git directories (not recommended)",
    )
    @click.option(
        "--no-resolve-root",
        is_flag=True,
        help="Don't auto-resolve to git repository root",
    )
    @click.pass_context
    def help_command(
        ctx: click.Context,
        target_path: Path | None,
        allow_non_git: bool,
        no_resolve_root: bool,
    ) -> None:
        """Show installation state and help (defaults to git repository root)."""
        console = Console()
        verbose = ctx.obj["verbose"]

        # Default to current directory if not provided, then resolve to git root
        initial_path = target_path or Path.cwd()
        resolved_path = resolve_project_root(
            initial_path, allow_non_git, no_resolve_root, console, verbose
        )

        try:
            from persuade.reporter import show_help

            show_help(resolved_path, verbose=verbose)
        except (OSError, ValueError) as e:
            click.echo(f"\nError: {e}", err=True)
            raise click.Abort() from e

    @cli.command()
    @click.argument(
        "target_path",
        type=click.Path(exists=True, file_okay=False, path_type=Path),
        default=None,
        required=False,
    )
    @click.option("--list", "list_backups", is_flag=True, help="List available backups")
    @click.option("--timestamp", help="Restore specific backup by timestamp")
    @click.option("--latest", is_flag=True, help="Restore most recent backup")
    @click.option(
        "--allow-non-git",
        is_flag=True,
        help="Allow restoring to non-git directories (not recommended)",
    )
    @click.option(
        "--no-resolve-root",
        is_flag=True,
        help="Don't auto-resolve to git repository root",
    )
    @click.pass_context
    def rollback(
        ctx: click.Context,
        target_path: Path | None,
        list_backups: bool,
        timestamp: str | None,
        latest: bool,
        allow_non_git: bool,
        no_resolve_root: bool,
    ) -> None:
        """Restore from backup (defaults to git repository root)."""
        console = Console()  # Initialize at top to ensure availability in exception handler
        verbose = ctx.obj["verbose"]

        # Default to current directory if not provided, then resolve to git root
        initial_path = target_path or Path.cwd()
        resolved_path = resolve_project_root(
            initial_path, allow_non_git, no_resolve_root, console, verbose
        )

        try:
            from persuade.backup import BackupManager

            backup_manager = BackupManager(resolved_path)

            if list_backups:
                backups = backup_manager.list_backups()

                if not backups:
                    if RICH_AVAILABLE:
                        console.print("[yellow]No backups available[/yellow]")
                    else:
                        click.echo("No backups available")
                    return

                if RICH_AVAILABLE:
                    console.print("\n[bold]Available Backups:[/bold]\n")
                    for backup in backups:
                        console.print(f"  [cyan]{backup.timestamp}[/cyan]")
                        console.print(f"    Created: {backup.created_at}")
                        console.print(f"    Files: {backup.file_count}")
                        console.print()
                else:
                    click.echo("\nAvailable Backups:\n")
                    for backup in backups:
                        click.echo(f"  {backup.timestamp}")
                        click.echo(f"    Created: {backup.created_at}")
                        click.echo(f"    Files: {backup.file_count}")
                        click.echo()
            else:
                # Restore backup
                restored = backup_manager.restore_backup(timestamp=timestamp if timestamp else None)
                if RICH_AVAILABLE:
                    console.print(f"\n[green]✓[/green] Restored from backup: {restored.name}")
                else:
                    click.echo(f"\n✓ Restored from backup: {restored.name}")

        except (OSError, ValueError, RestoreError) as e:
            if RICH_AVAILABLE:
                console.print(f"\n[red]✗[/red] Rollback failed: {e}")
            else:
                click.echo(f"\n✗ Rollback failed: {e}", err=True)
            raise click.Abort() from e
