"""SSH key deployment utility similar to ssh-copy-id."""

from __future__ import annotations

import argparse
import contextlib
import logging
import subprocess
import sys
import tempfile
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path

logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)


MIN_KEY_PARTS = 2
MIN_PORT = 1
MAX_PORT = 65535


@dataclass
class SSHCopyIDConfig:
    """Configuration for SSH key deployment."""

    hostname: str
    username: str
    password: str
    port: int = 22
    public_key_path: str = "~/.ssh/id_rsa.pub"
    timeout: int = 30
    connect_timeout: int = 10

    @cached_property
    def expanded_key_path(self) -> Path:
        """Get expanded public key file path."""
        return Path(self.public_key_path).expanduser()

    @cached_property
    def ssh_command_parts(self) -> list[str]:
        """Generate base SSH command parts."""
        return [
            "ssh",
            "-p",
            str(self.port),
            "-o",
            "StrictHostKeyChecking=no",
            "-o",
            "UserKnownHostsFile=/dev/null",
            "-o",
            f"ConnectTimeout={self.connect_timeout}",
            f"{self.username}@{self.hostname}",
        ]


class SSHAuthenticationError(Exception):
    """Raised when SSH authentication fails."""


class SSHConnectionError(Exception):
    """Raised when SSH connection fails."""


class SSHKeyNotFoundError(Exception):
    """Raised when SSH public key file is not found."""


def deploy_with_native_ssh(config: SSHCopyIDConfig, pub_key: str) -> None:
    """Deploy SSH key using native SSH without sshpass."""
    import shlex

    # Create temporary script for SSH key deployment
    # Extract key type and key data for more reliable matching
    key_parts = pub_key.split()
    if len(key_parts) >= MIN_KEY_PARTS:
        key_type = key_parts[0]
        key_data = key_parts[1]
        grep_pattern = f"{key_type}.*{key_data}"
    else:
        # Fallback for non-standard key formats
        grep_pattern = pub_key

    # Safely escape shell variables to prevent command injection
    safe_grep_pattern = shlex.quote(grep_pattern)
    safe_pub_key = shlex.quote(pub_key)

    script_content = f"""#!/bin/bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
cd ~/.ssh
touch authorized_keys
chmod 600 authorized_keys
grep -qF {safe_grep_pattern} authorized_keys 2>/dev/null || \\
    echo {safe_pub_key} >> authorized_keys
"""

    with tempfile.NamedTemporaryFile(
        encoding="utf-8",
        mode="w",
        suffix=".sh",
        delete=False,
    ) as script_file:
        script_file.write(script_content)
        script_path = script_file.name

    try:
        # Use SSH to execute the deployment script by piping it to bash
        command = [
            "ssh",
            *config.ssh_command_parts[:-1],  # All parts except hostname
            f"{config.username}@{config.hostname}",
            "bash -s",
        ]

        logger.info("Attempting deployment using native SSH...")
        process = subprocess.run(
            command,
            check=False,
            capture_output=True,
            text=True,
            timeout=config.timeout,
            input=script_content,
        )

        if process.returncode != 0:
            if "Permission denied" in process.stderr:
                msg = "Authentication failed with native SSH"
                raise SSHAuthenticationError(msg)
            msg = f"Native SSH deployment failed: {process.stderr}"
            raise SSHConnectionError(
                msg,
            )

        logger.info("SSH key deployed successfully using native SSH method")

    finally:
        # Clean up temporary file
        with contextlib.suppress(BaseException):
            Path(script_path).unlink()


def _read_public_key(key_path: Path) -> str:
    """Read and validate public key file.

    Returns
    -------
        Public key content

    Raises
    ------
        SSHKeyNotFoundError: Key file not found or empty
        SSHConnectionError: Failed to read key file
    """
    # Validate key file exists
    if not key_path.exists():
        msg = f"Public key file not found: {key_path}"
        raise SSHKeyNotFoundError(msg)

    try:
        with key_path.open(encoding="utf-8") as f:
            pub_key = f.read().strip()

    except Exception as e:
        msg = f"Failed to read public key file: {e!s}"
        raise SSHConnectionError(msg) from e

    # Validate key content
    if not pub_key:
        msg = f"Public key file is empty: {key_path}"
        raise SSHKeyNotFoundError(msg)

    return pub_key


def _extract_key_pattern(pub_key: str) -> str:
    """Extract grep pattern from public key for duplicate detection.

    Args:
        pub_key: SSH public key content

    Returns
    -------
        Pattern for grep matching (key_type.*key_data)
    """
    key_parts = pub_key.split()
    if len(key_parts) >= MIN_KEY_PARTS:
        key_type = key_parts[0]
        key_data = key_parts[1]
        return f"{key_type}.*{key_data}"
    # Fallback for non-standard key formats
    return pub_key


def _build_ssh_command(config: SSHCopyIDConfig, grep_pattern: str, pub_key: str) -> list[str]:
    """Build SSH command for key deployment using sshpass.

    Args:
        config: SSH configuration
        grep_pattern: Pattern for grep matching to avoid duplicate keys
        pub_key: Public key content

    Returns
    -------
        Command list for subprocess (sshpass + ssh + remote commands)
    """
    import shlex

    # Safely escape shell arguments to prevent command injection
    safe_grep_pattern = shlex.quote(grep_pattern)
    safe_pub_key = shlex.quote(pub_key)

    return [
        "sshpass",
        "-p",
        config.password,
        *config.ssh_command_parts,
        (
            f"mkdir -p ~/.ssh && chmod 700 ~/.ssh && "
            f"cd ~/.ssh && touch authorized_keys && "
            f"chmod 600 authorized_keys && "
            f"grep -qF {safe_grep_pattern} "
            f"authorized_keys 2>/dev/null || "
            f"echo {safe_pub_key} >> authorized_keys"
        ),
    ]


def _handle_ssh_error(process: subprocess.CompletedProcess, config: SSHCopyIDConfig) -> None:
    """Handle SSH process errors and raise appropriate exceptions.

    Args:
        process: Completed subprocess with return code and stderr
        config: SSH configuration for error context

    Raises
    ------
        SSHAuthenticationError: Authentication failed (Permission denied)
        SSHConnectionError: Connection failed (refused, network unreachable, etc.)
    """
    if "Permission denied" in process.stderr:
        msg = "Authentication failed, please check username or password"
        raise SSHAuthenticationError(msg)
    if "Connection refused" in process.stderr:
        msg = f"Connection refused to {config.hostname}:{config.port}"
        raise SSHConnectionError(msg)
    if "Network is unreachable" in process.stderr:
        msg = f"Network unreachable to {config.hostname}"
        raise SSHConnectionError(msg)
    msg = f"SSH execution failed: {process.stderr.strip()}"
    raise SSHConnectionError(msg)


def _try_alternative_methods(config: SSHCopyIDConfig, pub_key: str) -> None:
    """Try alternative deployment methods when sshpass is unavailable.

    Attempts to use native SSH (without password authentication) as fallback.
    If all methods fail, provides installation instructions for sshpass.

    Args:
        config: SSH configuration
        pub_key: Public key content

    Raises
    ------
        SystemExit: If all methods fail, exits with code 1
    """
    logger.warning(
        "sshpass tool not found. Attempting alternative deployment methods...",
    )

    try:
        deploy_with_native_ssh(config, pub_key)
    except Exception:
        logger.exception(
            "Native SSH deployment also failed. Please install sshpass or use manual method:\n"
            "Installation methods:\n"
            "Windows: Download sshpass for Windows or use WSL\n"
            "Ubuntu/Debian: sudo apt-get install sshpass\n"
            "CentOS/RHEL: sudo yum install sshpass\n"
            "macOS: brew install hudochenkov/sshpass/sshpass\n"
            f"Or manually: ssh-copy-id -p {config.port} {config.username}@{config.hostname}",
        )
        sys.exit(1)
    else:
        logger.info("SSH key deployed successfully using native SSH method")


def ssh_copy_id(config: SSHCopyIDConfig) -> None:
    """Deploy SSH public key to remote server.

    Args:
        config: SSH copy ID configuration

    Raises
    ------
        SSHAuthenticationError: Authentication failed
        SSHConnectionError: Connection failed
        SSHKeyNotFoundError: Public key file not found
        Exception: Other exceptions
        ValueError: Invalid port number
    """
    # Read local public key content
    pub_key = _read_public_key(config.expanded_key_path)

    # Security warning: StrictHostKeyChecking=no disables host key verification
    # Only use this option in trusted network environments
    logger.warning(
        "Security warning: Disabling SSH host key verification. Ensure you are in a trusted network environment.",
    )

    # Validate configuration
    if not config.hostname or not config.username or not config.password:
        msg = "Hostname, username, and password cannot be empty"
        raise ValueError(msg)

    if config.port < MIN_PORT or config.port > MAX_PORT:
        msg = f"Invalid port number: {config.port}, must be in range 1-65535"
        raise ValueError(msg)

    # Extract key pattern for reliable matching
    grep_pattern = _extract_key_pattern(pub_key)

    try:
        # Execute remote command using sshpass
        command = _build_ssh_command(config, grep_pattern, pub_key)

        process = subprocess.run(
            command,
            check=False,
            capture_output=True,
            text=True,
            timeout=config.timeout,
        )

        if process.returncode != 0:
            _handle_ssh_error(process, config)

    except FileNotFoundError:
        # If sshpass is not found, try alternative methods
        _try_alternative_methods(config, pub_key)
    except subprocess.TimeoutExpired as e:
        msg = f"SSH connection timeout after {config.timeout} seconds"
        raise SSHConnectionError(msg) from e
    except Exception as e:
        msg = f"SSH operation failed: {e!s}"
        raise SSHConnectionError(msg) from e


def parse_args() -> argparse.Namespace:
    """Parse command line arguments.

    Returns
    -------
        Parsed arguments
    """
    parser = argparse.ArgumentParser(
        description="Deploy SSH public key to remote server",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  sshcopyid 192.168.1.100 user password
  sshcopyid 192.168.1.100 user password -p 2222
  sshcopyid 192.168.1.100 user password -k ~/.ssh/id_ed25519.pub
        """,
    )
    parser.add_argument("hostname", help="Remote server hostname or IP address")
    parser.add_argument("username", help="Username for remote server")
    parser.add_argument("password", help="Password for remote server")
    parser.add_argument(
        "-p",
        "--port",
        type=int,
        default=22,
        help="SSH port (default: 22)",
    )
    parser.add_argument(
        "-k",
        "--keypath",
        type=str,
        default="~/.ssh/id_rsa.pub",
        help="Public key file path (default: ~/.ssh/id_rsa.pub)",
    )
    parser.add_argument(
        "-t",
        "--timeout",
        type=int,
        default=30,
        help="SSH operation timeout in seconds (default: 30)",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Enable verbose logging",
    )
    return parser.parse_args()


def setup_logging(*, verbose: bool = False) -> None:
    """Set up logging configuration.

    Args:
        verbose: Enable verbose logging
    """
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        handlers=[
            logging.StreamHandler(sys.stdout),
        ],
    )


def main() -> None:
    """Run entry point for the command-line interface."""
    args = parse_args()
    setup_logging(args.verbose)

    # Create configuration object
    config = SSHCopyIDConfig(
        hostname=args.hostname,
        username=args.username,
        password=args.password,
        port=args.port,
        public_key_path=args.keypath,
        timeout=args.timeout,
    )

    # Validate configuration before execution
    if not config.hostname or not config.username or not config.password:
        logger.error("Hostname, username, and password cannot be empty")
        sys.exit(1)

    if config.port < MIN_PORT or config.port > MAX_PORT:
        logger.error(f"Invalid port number: {config.port}, must be in range 1-65535")
        sys.exit(1)

    # Validate public key file
    if not config.expanded_key_path.exists():
        logger.error(f"Public key file does not exist: {config.expanded_key_path}")
        sys.exit(1)

    if not config.expanded_key_path.is_file():
        logger.error(f"Specified path is not a file: {config.expanded_key_path}")
        sys.exit(1)

    try:
        logger.info(
            f"Deploying SSH key to {config.username}@{config.hostname}:{config.port}",
        )
        logger.info(f"Using public key: {config.expanded_key_path}")

        ssh_copy_id(config)
        logger.info("SSH key deployment completed successfully")

    except SSHAuthenticationError:
        logger.exception("Authentication failed")
        sys.exit(2)
    except SSHConnectionError:
        logger.exception("Connection failed")
        sys.exit(3)
    except SSHKeyNotFoundError:
        logger.exception("Key file error")
        sys.exit(4)
    except Exception:
        logger.exception("Unexpected error")
        sys.exit(1)
