"""
CLI layer for user interface.

This module provides the Typer-based CLI with error formatting,
signal handling, and user-friendly messages.
"""

from __future__ import annotations

from pathlib import Path

import typer

from kilit.crypto import generate_master_key
from kilit.exceptions import (
    KilitCredentialsError,
    KilitError,
    KilitFileError,
    KilitFormatError,
    KilitKeyError,
)
from kilit.service import CredentialsService

# Create Typer app
app = typer.Typer(
    name="kilit",
    help="Encrypted secrets management for Python, inspired by Rails credentials",
    add_completion=False,
)

# Shared service instance
_service = CredentialsService()


def format_error(error: KilitError) -> str:
    """
    Format KilitError into user-friendly message.

    Args:
        error: KilitError instance

    Returns:
        Formatted error message

    Security:
        Never includes secret values or master keys in messages
    """
    message = str(error)

    # Add helpful context based on error type
    if isinstance(error, KilitKeyError):
        return f"❌ Master Key Error: {message}"
    elif isinstance(error, KilitFileError):
        return f"❌ File Error: {message}"
    elif isinstance(error, KilitFormatError):
        return f"❌ Format Error: {message}"
    elif isinstance(error, KilitCredentialsError):
        return f"❌ Credentials Error: {message}"
    else:
        return f"❌ Error: {message}"


def handle_error(error: Exception) -> None:
    """
    Handle error and exit with appropriate message.

    Args:
        error: Exception to handle
    """
    if isinstance(error, KilitError):
        typer.echo(format_error(error), err=True)
        raise typer.Exit(code=1)
    else:
        # Unexpected error
        typer.echo(f"❌ Unexpected error: {error}", err=True)
        raise typer.Exit(code=1)


@app.command()
def init(
    path: str = typer.Option(
        "config/credentials.json.enc",
        "--path",
        "-p",
        help="Path to encrypted credentials file",
    ),
    format: str = typer.Option("json", "--format", "-f", help="Format for secrets (json or yaml)"),
    master_key_path: str = typer.Option(
        "config/master.key",
        "--master-key-path",
        help="Path to save master key",
    ),
    print_master_key: bool = typer.Option(
        False,
        "--print-master-key/--no-print-master-key",
        help="Print master key to stdout (default: no)",
    ),
    force: bool = typer.Option(False, "--force", help="Overwrite existing file"),
):
    """
    Initialize new encrypted credentials file.

    This command:
    1. Generates a new master key
    2. Saves master key to file with secure permissions
    3. Creates empty encrypted credentials file

    Security:
    - Master key is NOT printed by default (prevents accidental leakage)
    - Use --print-master-key to explicitly print it
    - Master key file has 0o600 permissions on POSIX systems

    Examples:
        kilit init
        kilit init --path config/secrets.yaml.enc --format yaml
        kilit init --print-master-key
    """
    try:
        creds_path = Path(path)
        key_path = Path(master_key_path)

        # Check if file already exists
        if creds_path.exists() and not force:
            raise KilitFileError(
                f"Encrypted file already exists: {creds_path}\nUse --force to overwrite"
            )

        # Generate new master key
        master_key_str = generate_master_key()

        # Decode for encryption
        import base64

        padding = "=" * (4 - len(master_key_str) % 4) if len(master_key_str) % 4 else ""
        master_key_bytes = base64.urlsafe_b64decode(master_key_str + padding)

        # Save master key to file
        key_path.parent.mkdir(parents=True, exist_ok=True)
        _service.storage.save_key(master_key_str, key_path)

        # Get codec
        if format:
            codec = _service.codec_registry.get_codec_by_name(format)
        else:
            codec = _service.codec_registry.get_codec_for_path(creds_path)

        # Create empty secrets
        empty_secrets = {}

        # Encrypt and save
        _service.encrypt_credentials(empty_secrets, creds_path, master_key_bytes, codec)

        # Success message
        typer.echo(f"✅ Initialized encrypted credentials at: {creds_path}")
        typer.echo(f"✅ Master key saved to: {key_path}")

        # Optionally print master key
        if print_master_key:
            typer.echo(f"\n🔑 Master key: {master_key_str}")
            typer.echo("\n⚠️  Save this key securely! Never commit it to version control.")
        else:
            typer.echo(f"\n💡 Master key saved to {key_path} (not printed for security)")
            typer.echo("   Use --print-master-key to print it if needed")

        typer.echo(f"\n📝 Add to .gitignore: {key_path}")
        typer.echo(f"📝 Commit to git: {creds_path}")

    except KilitError as e:
        handle_error(e)
    except Exception as e:
        handle_error(e)


@app.command()
def edit(
    path: str = typer.Option(
        "config/credentials.json.enc",
        "--path",
        "-p",
        help="Path to encrypted credentials file",
    ),
    master_key: str | None = typer.Option(None, "--master-key", help="Master key (base64 encoded)"),
    master_key_path: str | None = typer.Option(
        None, "--master-key-path", help="Path to master key file"
    ),
):
    """
    Edit encrypted credentials in your default editor.

    This command:
    1. Decrypts credentials to a secure temp file
    2. Opens temp file in your editor (EDITOR env var or vi/notepad)
    3. Re-encrypts after you save and close the editor
    4. Cleans up temp file

    Security:
    - Temp file has 0o600 permissions on POSIX systems
    - Temp file is always cleaned up (even on Ctrl+C)
    - Signal handlers ensure cleanup on interruption

    Examples:
        kilit edit
        kilit edit --path config/secrets.yaml.enc
        kilit edit --master-key-path /secure/master.key
    """
    import os
    import signal
    import subprocess
    import sys
    import tempfile
    from pathlib import Path as PathlibPath

    temp_file_path = None

    def cleanup_temp_file():
        """Clean up temp file if it exists."""
        if temp_file_path and PathlibPath(temp_file_path).exists():
            try:
                PathlibPath(temp_file_path).unlink()
            except Exception:
                pass  # Best effort cleanup

    def signal_handler(signum, frame):
        """Handle signals to clean up temp file."""
        cleanup_temp_file()
        typer.echo("\n⚠️  Edit cancelled", err=True)
        raise typer.Exit(code=130)

    # Register signal handlers
    signal.signal(signal.SIGINT, signal_handler)
    if hasattr(signal, "SIGTERM"):
        signal.signal(signal.SIGTERM, signal_handler)

    try:
        creds_path = PathlibPath(path)

        # Check if file exists
        if not creds_path.exists():
            raise KilitFileError(f"Encrypted file not found: {creds_path}")

        # Load master key
        key_path_obj = PathlibPath(master_key_path) if master_key_path else None
        master_key_bytes = _service.load_master_key(
            env_var="KILIT_MASTER_KEY" if not master_key else None,
            cli_key=master_key,
            cli_key_path=key_path_obj,
            default_path=PathlibPath("config/master.key"),
        )

        # Decrypt credentials
        codec = _service.codec_registry.get_codec_for_path(creds_path)
        secrets = _service.decrypt_credentials(creds_path, master_key_bytes, codec)

        # Serialize to string for editing
        content = codec.dumps(secrets)

        # Create secure temp file
        suffix = codec.default_extension
        fd, temp_file_path = tempfile.mkstemp(suffix=suffix, text=True)

        try:
            # Set secure permissions (0o600 on POSIX)
            _service.storage.set_secure_permissions(PathlibPath(temp_file_path))

            # Write content to temp file
            with os.fdopen(fd, "w") as f:
                f.write(content)

            # Get editor from environment or use platform default
            editor = os.environ.get("EDITOR")
            if not editor:
                editor = "notepad" if sys.platform == "win32" else "vi"

            # Open editor
            result = subprocess.run([editor, temp_file_path])

            # Check if editor exited successfully
            if result.returncode != 0:
                typer.echo(f"⚠️  Editor exited with code {result.returncode}", err=True)

            # Read modified content
            with open(temp_file_path) as f:
                modified_content = f.read()

            # Parse modified content
            modified_secrets = codec.loads(modified_content)

            # Re-encrypt and save
            _service.encrypt_credentials(modified_secrets, creds_path, master_key_bytes, codec)

            typer.echo(f"✅ Credentials updated: {creds_path}")

        finally:
            # Always clean up temp file
            cleanup_temp_file()

    except KilitError as e:
        cleanup_temp_file()
        handle_error(e)
    except Exception as e:
        cleanup_temp_file()
        handle_error(e)


@app.command()
def show(
    path: str = typer.Option(
        "config/credentials.json.enc",
        "--path",
        "-p",
        help="Path to encrypted credentials file",
    ),
    master_key: str | None = typer.Option(None, "--master-key", help="Master key (base64 encoded)"),
    master_key_path: str | None = typer.Option(
        None, "--master-key-path", help="Path to master key file"
    ),
    redact: bool = typer.Option(False, "--redact", help="Redact all values (show structure only)"),
):
    """
    Display decrypted credentials.

    This command decrypts and displays credentials in their original format.
    Use --redact to show only the structure without revealing values.

    Security:
    - Use --redact in untrusted environments to verify structure
    - Never pipe output to logs without redaction

    Examples:
        kilit show
        kilit show --redact
        kilit show --path config/secrets.yaml.enc
    """
    try:
        creds_path = Path(path)

        # Check if file exists
        if not creds_path.exists():
            raise KilitFileError(f"Encrypted file not found: {creds_path}")

        # Load master key
        key_path_obj = Path(master_key_path) if master_key_path else None
        master_key_bytes = _service.load_master_key(
            env_var="KILIT_MASTER_KEY" if not master_key else None,
            cli_key=master_key,
            cli_key_path=key_path_obj,
            default_path=Path("config/master.key"),
        )

        # Decrypt credentials
        codec = _service.codec_registry.get_codec_for_path(creds_path)
        secrets = _service.decrypt_credentials(creds_path, master_key_bytes, codec)

        # Redact if requested
        if redact:
            secrets = _redact_values(secrets)

        # Format and display
        output = codec.dumps(secrets)
        typer.echo(output)

    except KilitError as e:
        handle_error(e)
    except Exception as e:
        handle_error(e)


def _redact_values(data):
    """
    Recursively redact all leaf values in a nested structure.

    Args:
        data: Dictionary or other data structure

    Returns:
        Data structure with all leaf values replaced with "[REDACTED]"
    """
    if isinstance(data, dict):
        return {key: _redact_values(value) for key, value in data.items()}
    elif isinstance(data, list):
        return [_redact_values(item) for item in data]
    else:
        return "[REDACTED]"


@app.command()
def export(
    path: str = typer.Option(
        "config/credentials.json.enc",
        "--path",
        "-p",
        help="Path to encrypted credentials file",
    ),
    master_key: str | None = typer.Option(None, "--master-key", help="Master key (base64 encoded)"),
    master_key_path: str | None = typer.Option(
        None, "--master-key-path", help="Path to master key file"
    ),
    prefix: str = typer.Option("", "--prefix", help="Prefix for environment variable names"),
):
    """
    Export credentials as environment variables.

    This command flattens nested credentials into UPPER_SNAKE_CASE
    environment variable format suitable for shell export.

    Usage:
        eval $(kilit export)
        kilit export > .env
        kilit export --prefix APP_

    Examples:
        kilit export
        kilit export --prefix MYAPP_
        eval $(kilit export)
    """
    try:
        creds_path = Path(path)

        # Check if file exists
        if not creds_path.exists():
            raise KilitFileError(f"Encrypted file not found: {creds_path}")

        # Load master key
        key_path_obj = Path(master_key_path) if master_key_path else None
        master_key_bytes = _service.load_master_key(
            env_var="KILIT_MASTER_KEY" if not master_key else None,
            cli_key=master_key,
            cli_key_path=key_path_obj,
            default_path=Path("config/master.key"),
        )

        # Decrypt credentials
        codec = _service.codec_registry.get_codec_for_path(creds_path)
        secrets = _service.decrypt_credentials(creds_path, master_key_bytes, codec)

        # Flatten to environment variables
        flattened = _flatten_dict(secrets, prefix=prefix)

        # Output as KEY=value
        for key, value in flattened.items():
            typer.echo(f"{key}={value}")

    except KilitError as e:
        handle_error(e)
    except Exception as e:
        handle_error(e)


def _flatten_dict(data, prefix="", separator="_"):
    """
    Recursively flatten nested dictionary to UPPER_SNAKE_CASE keys.

    Args:
        data: Dictionary to flatten
        prefix: Prefix for keys
        separator: Separator for nested keys

    Returns:
        Flattened dictionary with UPPER_SNAKE_CASE keys

    Examples:
        {"db": {"host": "localhost", "port": 5432}}
        -> {"DB_HOST": "localhost", "DB_PORT": "5432"}
    """
    result = {}

    if not isinstance(data, dict):
        # Non-dict value, convert to string
        return {prefix.upper(): str(data)} if prefix else {}

    for key, value in data.items():
        # Build new key
        new_key = f"{prefix}{separator}{key}" if prefix else key

        if isinstance(value, dict):
            # Recursively flatten nested dict
            result.update(_flatten_dict(value, prefix=new_key, separator=separator))
        else:
            # Leaf value - convert key to UPPER_SNAKE_CASE
            result[new_key.upper()] = str(value)

    return result


@app.command()
def rotate(
    path: str = typer.Option(
        "config/credentials.json.enc",
        "--path",
        "-p",
        help="Path to encrypted credentials file",
    ),
    master_key: str | None = typer.Option(
        None, "--master-key", help="Current master key (base64 encoded)"
    ),
    master_key_path: str | None = typer.Option(
        None, "--master-key-path", help="Path to current master key file"
    ),
    new_master_key: str | None = typer.Option(
        None, "--new-master-key", help="New master key (base64 encoded)"
    ),
    write_new_master_key: str | None = typer.Option(
        None,
        "--write-new-master-key",
        help="Path to save new master key",
    ),
    print_new_master_key: bool = typer.Option(
        False,
        "--print-new-master-key/--no-print-new-master-key",
        help="Print new master key to stdout (default: no)",
    ),
):
    """
    Rotate master key by re-encrypting with a new key.

    This command:
    1. Decrypts credentials with current master key
    2. Generates new master key (or uses provided one)
    3. Re-encrypts credentials with new master key
    4. Optionally saves or prints new key

    Security:
    - New master key is NOT printed by default (prevents accidental leakage)
    - Use --print-new-master-key to explicitly print it
    - Use --write-new-master-key to save it to a file
    - If neither flag is used, you'll get a warning to save the key

    Examples:
        kilit rotate
        kilit rotate --print-new-master-key
        kilit rotate --write-new-master-key config/master.key
        kilit rotate --new-master-key <base64-key>
    """
    try:
        creds_path = Path(path)

        # Check if file exists
        if not creds_path.exists():
            raise KilitFileError(f"Encrypted file not found: {creds_path}")

        # Load current master key
        key_path_obj = Path(master_key_path) if master_key_path else None
        current_master_key_bytes = _service.load_master_key(
            env_var="KILIT_MASTER_KEY" if not master_key else None,
            cli_key=master_key,
            cli_key_path=key_path_obj,
            default_path=Path("config/master.key"),
        )

        # Decrypt with current key
        codec = _service.codec_registry.get_codec_for_path(creds_path)
        secrets = _service.decrypt_credentials(creds_path, current_master_key_bytes, codec)

        # Generate or use provided new master key
        if new_master_key:
            new_master_key_str = new_master_key
        else:
            new_master_key_str = generate_master_key()

        # Decode new master key
        import base64

        padding = "=" * (4 - len(new_master_key_str) % 4) if len(new_master_key_str) % 4 else ""
        new_master_key_bytes = base64.urlsafe_b64decode(new_master_key_str + padding)

        # Re-encrypt with new key (this generates new salt and nonce)
        _service.encrypt_credentials(secrets, creds_path, new_master_key_bytes, codec)

        # Save new master key if requested
        if write_new_master_key:
            write_path = Path(write_new_master_key)
            _service.storage.save_key(new_master_key_str, write_path)
            typer.echo(f"✅ New master key saved to: {write_path}")

        # Print success message
        typer.echo(f"✅ Master key rotated: {creds_path}")

        # Optionally print new master key
        if print_new_master_key:
            typer.echo(f"\n🔑 New master key: {new_master_key_str}")
            typer.echo("\n⚠️  Save this key securely! Never commit it to version control.")
        elif not write_new_master_key:
            # Warn if key not printed and not written
            typer.echo(
                "\n⚠️  New master key was NOT printed or saved to a file.",
                err=True,
            )
            typer.echo(
                "   Use --print-new-master-key or --write-new-master-key to save it.",
                err=True,
            )
            typer.echo(
                "   Without the new key, you won't be able to decrypt this file!",
                err=True,
            )

    except KilitError as e:
        handle_error(e)
    except Exception as e:
        handle_error(e)
