"""
CLI commands for agent operations.

This module contains agent-related commands like iterate-pr, serve, and validate-docs.
"""

from pathlib import Path

import click
from rich.console import Console

from persuade.cli_sync import resolve_project_root
from persuade.logging import get_logger

logger = get_logger()


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

    @cli.command("iterate-pr")
    @click.argument("pr_number", type=int)
    @click.option("--auto-approve", "-y", is_flag=True, help="Skip approval prompt")
    @click.option(
        "--project-dir",
        type=click.Path(exists=True, file_okay=False, path_type=Path),
        default=None,
        help="Project directory (defaults to git repository root)",
    )
    @click.option(
        "--allow-non-git",
        is_flag=True,
        help="Allow running in 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 iterate_pr(
        ctx: click.Context,
        pr_number: int,
        auto_approve: bool,
        project_dir: Path | None,
        allow_non_git: bool,
        no_resolve_root: bool,
    ) -> None:
        """Iterate on PR feedback with deterministic two-phase execution.

        This command runs the PR iteration workflow in two phases:

        \b
        Phase 1 (Plan): Read-only analysis and classification of PR feedback
        Phase 2 (Execute): Apply changes after user approval

        The two-phase pattern ensures deterministic behavior - you always
        see what will happen before any changes are made.

        Example:
            persuade iterate-pr 67
            persuade iterate-pr 67 --auto-approve  # For CI/CD
        """
        console = Console()
        verbose = ctx.obj.get("verbose", False)

        try:
            from persuade.agents.iterate_pr import IteratePRAgent
            from persuade.agents.runner import AgentRunner

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

            agent = IteratePRAgent(pr_number, project_dir=resolved_path)
            result = AgentRunner.run_sync(agent, auto_approve=auto_approve)

            if not result.success:
                raise click.Abort()

        except ImportError as e:
            console.print(f"\n[red]✗[/red] Missing dependency: {e}")
            console.print("[dim]Install with: pip install persuade[agents][/dim]")
            raise click.Abort() from e
        except (RuntimeError, ValueError, TimeoutError, ConnectionError, OSError) as e:
            # Handle specific agent/execution errors:
            # - RuntimeError: General agent failures
            # - ValueError: Invalid configuration or state
            # - TimeoutError: Agent SDK timeouts
            # - ConnectionError: Network issues with SDK
            # - OSError: File system or subprocess issues
            console.print(f"\n[red]✗[/red] iterate-pr failed: {e}")
            if ctx.obj.get("verbose"):
                console.print_exception()
            raise click.Abort() from e
        except Exception as e:
            # Fallback catch-all for unexpected exceptions to prevent crashes
            # Log for debugging even when not in verbose mode
            logger.error("iterate_pr_unexpected_error", error=str(e), error_type=type(e).__name__)
            console.print(f"\n[red]✗[/red] Unexpected error: {e}")
            if ctx.obj.get("verbose"):
                console.print_exception()
            raise click.Abort() from e

    @cli.command("validate-docs")
    @click.argument(
        "docs_path",
        type=click.Path(exists=True, file_okay=False, path_type=Path),
        default=None,
        required=False,
    )
    @click.option("--strict", is_flag=True, help="Enable strict type checking with mypy")
    @click.pass_context
    def validate_docs(ctx: click.Context, docs_path: Path | None, strict: bool) -> None:
        """Validate Python code examples in documentation.

        Checks that all Python code blocks in markdown files are syntactically
        valid and optionally validates type hints with mypy in strict mode.

        \b
        Examples:
            persuade validate-docs                     # Default path
            persuade validate-docs docs/               # Custom path
            persuade validate-docs --strict            # Enable mypy type checking
            persuade validate-docs docs/ --strict      # Both
        """
        console = Console()

        try:
            from persuade.docs.validate import validate_documentation

            # Default to infrastructure docs if not specified
            target_path = docs_path or Path(".claude/knowledge/infrastructure")

            if not target_path.exists():
                console.print(f"\n[red]✗[/red] Path not found: {target_path}")
                raise click.Abort()

            console.print(f"Validating Python examples in {target_path}")
            if strict:
                console.print("Mode: [yellow]Strict type checking with mypy[/yellow]")
            else:
                console.print("Mode: [cyan]Syntax validation only[/cyan]")
            console.print()

            total, failed = validate_documentation(target_path, strict=strict)

            console.print(f"\n{'=' * 60}")
            console.print(f"Total code blocks: {total}")
            console.print(f"Failed: {failed}")
            if total > 0:
                success_rate = (total - failed) / total * 100
                console.print(f"Success rate: {success_rate:.1f}%")

            if failed > 0:
                console.print("\n[red]✗[/red] Validation failed")
                raise click.Abort()

            console.print("\n[green]✓[/green] All code examples are valid")

        except FileNotFoundError as e:
            console.print(f"\n[red]✗[/red] {e}")
            raise click.Abort() from e
        except (SyntaxError, OSError, ValueError) as e:
            # Handle specific validation errors:
            # - SyntaxError: Invalid Python code in documentation
            # - OSError: File system issues during validation
            # - ValueError: Invalid configuration or parameters
            console.print(f"\n[red]✗[/red] Validation error: {e}")
            if ctx.obj.get("verbose"):
                console.print_exception()
            raise click.Abort() from e
        except Exception as e:
            # Fallback catch-all for unexpected exceptions to prevent crashes
            # Log for debugging even when not in verbose mode
            logger.error(
                "validate_docs_unexpected_error", error=str(e), error_type=type(e).__name__
            )
            console.print(f"\n[red]✗[/red] Unexpected error: {e}")
            if ctx.obj.get("verbose"):
                console.print_exception()
            raise click.Abort() from e

    @cli.command()
    @click.option("--host", default="127.0.0.1", help="Host to bind to")
    @click.option("--port", default=8765, type=int, help="Port to bind to")
    @click.option("--reload", is_flag=True, help="Enable auto-reload for development")
    @click.pass_context
    def serve(ctx: click.Context, host: str, port: int, reload: bool) -> None:
        """Start the agent HTTP server.

        The server provides a REST API for running agents programmatically
        from external tools, CI/CD pipelines, or other services.

        \b
        Endpoints:
          POST /agent/run              - Run an agent (plan or execute)
          POST /agent/{id}/execute     - Execute a pending plan
          GET  /agent/{id}             - Get pending plan details
          DELETE /agent/{id}           - Cancel a pending plan
          GET  /health                 - Health check

        Example:
            persuade serve --port 8765

            # Then call:
            curl -X POST http://localhost:8765/agent/run \\
              -H "Content-Type: application/json" \\
              -d '{"agent_type": "iterate-pr", "params": {"pr_number": 67}}'
        """
        console = Console()

        try:
            import uvicorn

            from persuade.agents.server import create_app
        except ImportError as e:
            console.print(f"\n[red]✗[/red] Missing dependency: {e}")
            console.print("[dim]Install with: pip install persuade[server][/dim]")
            raise click.Abort() from e

        console.print(f"\n[bold blue]Starting agent server on {host}:{port}[/bold blue]\n")
        console.print("Endpoints:")
        console.print(f"  [cyan]POST[/cyan] http://{host}:{port}/agent/run")
        console.print(f"  [cyan]GET[/cyan]  http://{host}:{port}/health")
        console.print()

        # Create app and run server
        app = create_app()
        uvicorn.run(app, host=host, port=port, reload=reload)

    @cli.command("check-docstrings")
    @click.argument("files", nargs=-1, type=click.Path(exists=True, path_type=Path))
    @click.pass_context
    def check_docstrings(ctx: click.Context, files: tuple[Path, ...]) -> None:
        """Check Python files for missing docstrings.

        Uses AST parsing to accurately detect functions and classes missing docstrings.
        Handles edge cases like decorated functions, async functions, and nested definitions.

        Example:
            persuade check-docstrings file1.py file2.py
            persuade check-docstrings src/**/*.py
        """
        from persuade.docstring_checker import check_files

        exit_code = check_files(list(files))
        if exit_code != 0:
            raise click.Abort()

    @cli.command("strip-attribution")
    @click.option(
        "--commits",
        "-n",
        type=int,
        default=None,
        help="Number of recent commits to process",
    )
    @click.option(
        "--range",
        "-r",
        "commit_range",
        type=str,
        default=None,
        help="Git revision range (e.g., HEAD~10..HEAD)",
    )
    @click.option(
        "--branch",
        "-b",
        "branch_only",
        is_flag=True,
        help="Process all commits since diverging from main/master",
    )
    @click.option(
        "--preserve-human-coauthors/--strip-all-coauthors",
        default=True,
        help="Keep Co-Authored-By lines for human contributors (default: keep)",
    )
    @click.option(
        "--dry-run",
        is_flag=True,
        help="Preview changes without making them",
    )
    @click.option(
        "--project-dir",
        type=click.Path(exists=True, file_okay=False, path_type=Path),
        default=None,
        help="Project directory (defaults to current directory)",
    )
    @click.pass_context
    def strip_attribution(
        ctx: click.Context,
        commits: int | None,
        commit_range: str | None,
        branch_only: bool,
        preserve_human_coauthors: bool,
        dry_run: bool,
        project_dir: Path | None,
    ) -> None:
        """Strip Claude attribution from git commit history.

        Removes Co-Authored-By lines for Claude/Anthropic and Generated-with
        signatures from commit messages. Creates a backup ref before any changes.

        \b
        Examples:
            persuade strip-attribution --commits 5
            persuade strip-attribution --range HEAD~10..HEAD
            persuade strip-attribution --branch
            persuade strip-attribution --commits 5 --dry-run

        \b
        After stripping, you may need to force-push:
            git push --force-with-lease
        """
        console = Console()
        repo_path = project_dir or Path.cwd()

        from persuade.git_attribution import (
            preview_changes,
            strip_attribution_from_commits,
        )

        # Validate that at least one commit selection method is specified
        if not any([commits, commit_range, branch_only]):
            console.print("\n[red]✗[/red] Must specify --commits, --range, or --branch")
            raise click.Abort()

        # Preview changes first
        console.print("\n[bold]Scanning commits for Claude attribution...[/bold]\n")
        changes = preview_changes(
            repo_path,
            commit_count=commits,
            commit_range=commit_range,
            branch_only=branch_only,
            preserve_human_coauthors=preserve_human_coauthors,
        )

        if not changes:
            console.print("[green]✓[/green] No Claude attribution found in commits")
            return

        # Show preview
        console.print(f"[yellow]Found {len(changes)} commits with Claude attribution:[/yellow]\n")
        for sha, original, _new in changes[:5]:  # Show first 5
            console.print(f"  [dim]{sha}[/dim] {original.splitlines()[0][:60]}...")

        if len(changes) > 5:
            console.print(f"  [dim]... and {len(changes) - 5} more[/dim]")

        console.print()

        if dry_run:
            console.print("[cyan]Dry run - no changes made[/cyan]")
            return

        # Require confirmation
        console.print("[bold yellow]WARNING:[/bold yellow] This will rewrite git history.")
        console.print("A backup ref will be created before any changes.")
        console.print()

        if not click.confirm("Do you want to proceed?"):
            console.print("\n[dim]Aborted[/dim]")
            raise click.Abort()

        # Perform the strip
        result = strip_attribution_from_commits(
            repo_path,
            commit_count=commits,
            commit_range=commit_range,
            branch_only=branch_only,
            preserve_human_coauthors=preserve_human_coauthors,
        )

        if result.success:
            console.print(f"\n[green]✓[/green] {result.message}")
            if result.backup_ref:
                console.print(f"\n[dim]Backup ref: {result.backup_ref}[/dim]")
                console.print(
                    "[dim]To restore: persuade restore-attribution "
                    f"--backup {result.backup_ref}[/dim]"
                )
            console.print("\n[yellow]Note:[/yellow] You may need to force-push:")
            console.print("  git push --force-with-lease")
        else:
            console.print(f"\n[red]✗[/red] {result.message}")
            if result.error:
                console.print(f"[dim]{result.error}[/dim]")
            if result.backup_ref:
                console.print(f"\n[dim]Backup ref created: {result.backup_ref}[/dim]")
            raise click.Abort()

    @cli.command("restore-attribution")
    @click.option(
        "--backup",
        "-b",
        "backup_ref",
        type=str,
        default=None,
        help="Backup ref to restore from",
    )
    @click.option(
        "--list",
        "-l",
        "list_backups",
        is_flag=True,
        help="List available backup refs",
    )
    @click.option(
        "--project-dir",
        type=click.Path(exists=True, file_okay=False, path_type=Path),
        default=None,
        help="Project directory (defaults to current directory)",
    )
    @click.pass_context
    def restore_attribution(
        ctx: click.Context,
        backup_ref: str | None,
        list_backups: bool,
        project_dir: Path | None,
    ) -> None:
        """Restore git history from a backup ref created by strip-attribution.

        \b
        Examples:
            persuade restore-attribution --list
            persuade restore-attribution --backup refs/persuade/backup/.../20260123
        """
        console = Console()
        repo_path = project_dir or Path.cwd()

        from persuade.git_attribution import list_backup_refs, restore_from_backup

        if list_backups:
            refs = list_backup_refs(repo_path)
            if not refs:
                console.print("[dim]No backup refs found[/dim]")
                return

            console.print("[bold]Available backup refs:[/bold]\n")
            for ref, sha in refs:
                console.print(f"  {ref}")
                console.print(f"    [dim]commit: {sha[:8]}[/dim]")
            return

        if not backup_ref:
            console.print("[red]✗[/red] Must specify --backup or --list")
            raise click.Abort()

        # Require confirmation
        console.print(
            "[bold yellow]WARNING:[/bold yellow] This will reset your branch to the backup state."
        )
        console.print()

        if not click.confirm("Do you want to proceed?"):
            console.print("\n[dim]Aborted[/dim]")
            raise click.Abort()

        result = restore_from_backup(repo_path, backup_ref)

        if result.success:
            console.print(f"\n[green]✓[/green] {result.message}")
        else:
            console.print(f"\n[red]✗[/red] {result.message}")
            if result.error:
                console.print(f"[dim]{result.error}[/dim]")
            raise click.Abort()
