"""
djb deploy CLI - Heroku deployment commands.

Provides commands for deploying and reverting Django applications to Heroku.
"""

from __future__ import annotations

import subprocess
import sys
from pathlib import Path

import click

from djb.cli.editable import (
    find_djb_dir,
    install_editable_djb,
    is_djb_editable,
    uninstall_editable_djb,
)
from djb.secrets import AgeKey, SecretsManager, get_default_key_path


def _run(
    cmd: list[str],
    cwd: Path | None = None,
    label: str | None = None,
    halt_on_fail: bool = True,
) -> int:
    """Run a shell command with optional error handling."""
    if label:
        click.echo(f"Running {label} ({' '.join(cmd)})")
    result = subprocess.run(cmd, cwd=cwd)
    if result.returncode != 0 and halt_on_fail:
        raise click.ClickException(
            f"{label or 'Command'} failed with exit code {result.returncode}"
        )
    return result.returncode


@click.group()
def deploy():
    """Deploy applications to Heroku."""
    pass


@deploy.command("heroku")
@click.option(
    "--app",
    required=True,
    help="Heroku app name",
)
@click.option(
    "--skip-build",
    is_flag=True,
    help="Skip rebuilding frontend assets (use existing dist files).",
)
@click.option(
    "--skip-migrate",
    is_flag=True,
    help="Skip running database migrations on Heroku.",
)
@click.option(
    "--skip-secrets",
    is_flag=True,
    help="Skip syncing secrets to Heroku config vars.",
)
@click.option(
    "-y",
    "--yes",
    is_flag=True,
    help="Auto-confirm prompts (e.g., uncommitted changes warning).",
)
@click.option(
    "--frontend-dir",
    type=click.Path(path_type=Path),
    default=None,
    help="Frontend directory containing package.json (default: ./frontend)",
)
@click.option(
    "--secrets-dir",
    type=click.Path(path_type=Path),
    default=None,
    help="Secrets directory (default: ./secrets)",
)
@click.option(
    "--key-path",
    type=click.Path(path_type=Path),
    default=None,
    help="Path to age key file (default: ~/.age/keys.txt)",
)
def heroku(
    app: str,
    skip_build: bool,
    skip_migrate: bool,
    skip_secrets: bool,
    yes: bool,
    frontend_dir: Path | None,
    secrets_dir: Path | None,
    key_path: Path | None,
):
    """Deploy the application to Heroku.

    Complete deployment workflow:

    \b
    • Syncs production secrets to Heroku config vars
    • Builds frontend assets with version hashes
    • Collects Django static files
    • Pushes code to Heroku
    • Runs database migrations
    • Tags the deployment for tracking

    If djb is installed in editable mode (local development), it will be
    temporarily switched to the PyPI version for deployment, then restored.

    Checks for uncommitted changes and confirms before proceeding.

    \b
    Examples:
      djb deploy heroku --app myapp          # Full deployment
      djb deploy heroku --app myapp --skip-build  # Skip frontend build
      djb deploy heroku --app myapp --skip-migrate  # Skip migrations
    """
    repo_root = Path.cwd()

    if frontend_dir is None:
        frontend_dir = repo_root / "frontend"
    if secrets_dir is None:
        secrets_dir = repo_root / "secrets"
    if key_path is None:
        key_path = get_default_key_path()

    # Check if we need to temporarily switch from editable djb
    was_editable = find_djb_dir(repo_root) is not None and is_djb_editable(repo_root)

    if was_editable:
        click.echo("Temporarily switching djb from editable to PyPI version for deploy...")
        if not uninstall_editable_djb(repo_root):
            raise click.ClickException("Failed to switch djb from editable mode")

    try:
        _deploy_heroku_impl(
            app=app,
            skip_build=skip_build,
            skip_migrate=skip_migrate,
            skip_secrets=skip_secrets,
            yes=yes,
            repo_root=repo_root,
            frontend_dir=frontend_dir,
            secrets_dir=secrets_dir,
            key_path=key_path,
        )
    finally:
        # Restore editable djb if it was installed before
        if was_editable:
            click.echo("Restoring editable djb installation...")
            if not install_editable_djb(repo_root):
                click.secho(
                    "Failed to restore editable djb. Run `djb editable-djb` manually.",
                    fg="yellow",
                )


def _deploy_heroku_impl(
    app: str,
    skip_build: bool,
    skip_migrate: bool,
    skip_secrets: bool,
    yes: bool,
    repo_root: Path,
    frontend_dir: Path,
    secrets_dir: Path,
    key_path: Path,
):
    """Internal implementation of Heroku deployment."""
    # Check if logged into Heroku
    try:
        _run(["heroku", "auth:whoami"], label="Checking Heroku auth", halt_on_fail=True)
    except click.ClickException:
        raise click.ClickException(
            "Not logged into Heroku. Run 'heroku login' first."
        )

    # Verify we're in a git repository
    if not (repo_root / ".git").exists():
        raise click.ClickException("Not in a git repository")

    # Sync secrets to Heroku config vars
    if not skip_secrets:
        click.echo("Syncing production secrets to Heroku...")
        try:
            if not key_path.exists():
                click.secho("Warning: Age key not found, skipping secrets sync", fg="yellow")
            else:
                key = AgeKey.from_private_string(key_path.read_text().strip())
                manager = SecretsManager(secrets_dir=secrets_dir, age_key=key)
                secrets = manager.load_secrets("heroku_prod", decrypt=True)

                # Flatten nested dicts for Heroku config
                def flatten_dict(d, parent_key=""):
                    items = []
                    for k, v in d.items():
                        new_key = f"{parent_key}_{k}".upper() if parent_key else k.upper()
                        if isinstance(v, dict):
                            items.extend(flatten_dict(v, new_key).items())
                        else:
                            items.append((new_key, str(v)))
                    return dict(items)

                flat_secrets = flatten_dict(secrets)

                # Heroku-managed config vars that should not be overwritten
                heroku_managed_keys = {
                    "DATABASE_URL",  # Managed by Heroku Postgres addon
                    "DB_CREDENTIALS_USERNAME",
                    "DB_CREDENTIALS_PASSWORD",
                    "DB_CREDENTIALS_DATABASE",
                    "DB_CREDENTIALS_HOST",
                    "DB_CREDENTIALS_PORT",
                }

                # Set config vars on Heroku
                for key_name, value in flat_secrets.items():
                    # Skip Heroku-managed database config vars
                    if key_name in heroku_managed_keys:
                        click.secho(
                            f"Skipping {key_name} (managed by Heroku)", fg="yellow"
                        )
                        continue

                    # Skip if it's a complex value
                    if len(value) > 500:
                        click.secho(f"Skipping {key_name} (value too large)", fg="yellow")
                        continue

                    subprocess.run(
                        ["heroku", "config:set", f"{key_name}={value}", "--app", app],
                        capture_output=True,
                        check=True,
                    )

                click.secho(
                    f"✓ Synced {len(flat_secrets)} secrets to Heroku config", fg="green"
                )
        except Exception as e:
            click.secho(f"Warning: Failed to sync secrets: {e}", fg="yellow")
            if not yes and not click.confirm("Continue deployment without secrets?", default=False):
                raise click.ClickException("Deployment cancelled")
    else:
        click.secho("Skipping secrets sync.", fg="yellow")

    # Check for uncommitted changes
    result = subprocess.run(
        ["git", "status", "--porcelain"],
        cwd=repo_root,
        capture_output=True,
        text=True,
    )
    if result.stdout.strip():
        click.secho("Warning: You have uncommitted changes:", fg="yellow")
        click.echo(result.stdout)
        if not yes and not click.confirm("Continue with deployment?", default=False):
            raise click.ClickException("Deployment cancelled")

    # Rebuild frontend assets with unique version hashes
    if not skip_build and frontend_dir.exists():
        click.echo("Building frontend assets with version hashes...")
        _run(
            ["bun", "run", "build"],
            cwd=frontend_dir,
            label="Frontend build",
        )
        click.secho("Frontend build complete.", fg="green")
    elif not skip_build:
        click.secho(f"Frontend directory not found at {frontend_dir}, skipping build.", fg="yellow")
    else:
        click.secho("Skipping frontend build (using existing assets).", fg="yellow")

    # Collect static files
    click.echo("Collecting Django static files...")
    _run(
        ["python", "manage.py", "collectstatic", "--noinput", "--clear"],
        cwd=repo_root,
        label="collectstatic",
    )
    click.secho("Static files collected.", fg="green")

    # Get current git commit hash for tracking
    result = subprocess.run(
        ["git", "rev-parse", "HEAD"],
        cwd=repo_root,
        capture_output=True,
        text=True,
    )
    commit_hash = result.stdout.strip()[:7]

    # Check current branch
    result = subprocess.run(
        ["git", "branch", "--show-current"],
        cwd=repo_root,
        capture_output=True,
        text=True,
    )
    current_branch = result.stdout.strip()

    click.echo(f"Deploying from branch '{current_branch}' (commit {commit_hash})...")

    # Push to Heroku
    click.echo(f"Pushing to Heroku ({app})...")
    _run(
        ["git", "push", "heroku", f"{current_branch}:main", "--force"],
        label="git push heroku",
    )
    click.secho("Code pushed to Heroku.", fg="green")

    # Run migrations
    if not skip_migrate:
        click.echo("Running database migrations on Heroku...")
        _run(
            ["heroku", "run", "python manage.py migrate", "--app", app],
            label="heroku migrate",
        )
        click.secho("Migrations complete.", fg="green")
    else:
        click.secho("Skipping database migrations.", fg="yellow")

    # Tag the deployment
    tag_name = f"deploy-{commit_hash}"
    subprocess.run(
        ["git", "tag", "-f", tag_name],
        cwd=repo_root,
        capture_output=True,
    )
    subprocess.run(
        ["git", "push", "--tags", "--force"],
        cwd=repo_root,
        capture_output=True,
    )

    click.secho(f"\n✓ Deployment successful! (commit: {commit_hash})", fg="green", bold=True)
    click.echo(f"App URL: https://{app}.herokuapp.com/")
    click.echo(f"Logs: heroku logs --tail --app {app}")


@deploy.command("revert")
@click.option(
    "--app",
    required=True,
    help="Heroku app name",
)
@click.argument("git_hash", required=False)
@click.option(
    "--skip-migrate",
    is_flag=True,
    help="Skip running database migrations on Heroku.",
)
def revert(app: str, git_hash: str | None, skip_migrate: bool):
    """Revert to a previous deployment.

    Pushes a previous git commit to Heroku, effectively rolling back
    your deployment. By default reverts to the previous commit (HEAD~1).

    Confirms before executing the revert. Tags the revert for tracking.

    \b
    Examples:
      djb deploy revert --app myapp          # Revert to previous commit
      djb deploy revert --app myapp abc123   # Revert to specific commit
      djb deploy revert --app myapp --skip-migrate  # Revert without migrations
    """
    repo_root = Path.cwd()

    # Check if logged into Heroku
    try:
        _run(["heroku", "auth:whoami"], label="Checking Heroku auth", halt_on_fail=True)
    except click.ClickException:
        raise click.ClickException(
            "Not logged into Heroku. Run 'heroku login' first."
        )

    # Verify we're in a git repository
    if not (repo_root / ".git").exists():
        raise click.ClickException("Not in a git repository")

    # If no git hash provided, use the previous commit
    if git_hash is None:
        result = subprocess.run(
            ["git", "rev-parse", "HEAD~1"],
            cwd=repo_root,
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            raise click.ClickException("Could not determine previous commit")
        git_hash = result.stdout.strip()
        click.echo(f"No git hash provided, using previous commit: {git_hash[:7]}")

    # Verify the git hash exists
    result = subprocess.run(
        ["git", "cat-file", "-t", git_hash],
        cwd=repo_root,
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        raise click.ClickException(f"Git hash '{git_hash}' not found in repository")

    # Get full commit info
    result = subprocess.run(
        ["git", "log", "-1", "--oneline", git_hash],
        cwd=repo_root,
        capture_output=True,
        text=True,
    )
    commit_info = result.stdout.strip()

    click.echo(f"Reverting to: {commit_info}")
    if not click.confirm("Continue with revert?", default=False):
        raise click.ClickException("Revert cancelled")

    # Push the specified commit to Heroku
    click.echo(f"Pushing commit {git_hash[:7]} to Heroku ({app})...")
    _run(
        ["git", "push", "heroku", f"{git_hash}:main", "--force"],
        label="git push heroku (revert)",
    )
    click.secho("Code pushed to Heroku.", fg="green")

    # Run migrations
    if not skip_migrate:
        click.echo("Running database migrations on Heroku...")
        _run(
            ["heroku", "run", "python manage.py migrate", "--app", app],
            label="heroku migrate",
        )
        click.secho("Migrations complete.", fg="green")
    else:
        click.secho("Skipping database migrations.", fg="yellow")

    # Tag the revert
    short_hash = git_hash[:7]
    tag_name = f"revert-to-{short_hash}"
    subprocess.run(
        ["git", "tag", "-f", tag_name],
        cwd=repo_root,
        capture_output=True,
    )
    subprocess.run(
        ["git", "push", "--tags", "--force"],
        cwd=repo_root,
        capture_output=True,
    )

    click.secho(f"\n✓ Revert successful! (commit: {short_hash})", fg="green", bold=True)
    click.echo(f"App URL: https://{app}.herokuapp.com/")
    click.echo(f"Logs: heroku logs --tail --app {app}")
