"""
SSH configuration and legacy device support.

Ported from nterm's SSHClient with:
- Multi-method auth (password, key file, key content/PEM, passphrase)
- Legacy device algorithm negotiation (per-transport, never global)
- Multi-format key loading (Ed25519 → RSA → ECDSA)
- SHA2 RSA fallback strategy
- ANSI sequence filtering (for future sniffer use)

Changes from v1:
- LegacySSHSupport now holds validated constants only — no global mutation
- Algorithm lists validated against paramiko 3.4 _cipher_info/_kex_info registries
- Removed 3 ciphers and 2 KEX algos that paramiko 3.4 can't instantiate
"""

import os
import re
import logging
from io import StringIO
from dataclasses import dataclass
from typing import Optional

import paramiko

logger = logging.getLogger(__name__)


def filter_ansi_sequences(text: str) -> str:
    """
    Remove ANSI escape sequences and control characters.

    Preserves printable content for downstream parsing (sniffer, Parsing Fire).
    The terminal frontend (xterm.js) receives raw bytes and renders ANSI natively;
    this filter is only used for structured text processing.
    """
    if not text:
        return text
    ansi_pattern = r'\x1b\[[0-9;?]*[a-zA-Z]|\x1b[()][AB012]|\x07|[\x00-\x08\x0B\x0C\x0E-\x1F]'
    return re.sub(ansi_pattern, '', text)


@dataclass
class SSHConfig:
    """
    SSH connection configuration supporting all auth modes.

    Auth priority:
        1. key_content (PEM string from vault) + optional password
        2. key_file (path on server) + optional password
        3. password only
    """
    hostname: str
    port: int = 22
    username: str = ""
    password: Optional[str] = None
    key_file: Optional[str] = None       # Path to key file on server
    key_content: Optional[str] = None    # PEM string (from vault or paste)
    key_passphrase: Optional[str] = None # Passphrase for encrypted keys
    legacy_mode: bool = False
    timeout: int = 30
    keepalive_interval: int = 60

    @property
    def auth_method(self) -> str:
        """Describe the active authentication method."""
        if self.key_content:
            return "key(paste)"
        elif self.key_file:
            return "key(file)"
        elif self.password:
            return "password"
        return "none"

    def validate(self) -> None:
        """Raise ValueError if configuration is insufficient."""
        if not self.hostname:
            raise ValueError("hostname is required")
        if not self.username:
            raise ValueError("username is required")
        if not self.password and not self.key_content and not self.key_file:
            raise ValueError("At least one auth method required: password, key_file, or key_content")


class LegacySSHSupport:
    """
    Legacy device algorithm sets for paramiko 3.4+.

    These are CONSTANTS — never applied globally. The SSHSession uses
    them via transport_factory to configure individual Transport instances,
    keeping legacy sessions isolated from modern ones.

    All entries validated against paramiko 3.4's internal registries:
    - Ciphers:  Transport._cipher_info
    - KEX:      Transport._kex_info
    - Keys:     Transport._preferred_keys (all stock entries supported)

    Removed from v1 (not in paramiko 3.4 registry):
    - Ciphers:  aes256-gcm@openssh.com, aes128-gcm@openssh.com,
                chacha20-poly1305@openssh.com
    - KEX:      curve25519-sha256 (only @libssh.org variant exists),
                diffie-hellman-group18-sha512
    """

    # Legacy-first ordering: oldest algos first so ancient devices
    # find a match early in the negotiation list.

    LEGACY_KEX = (
        "diffie-hellman-group1-sha1",
        "diffie-hellman-group14-sha1",
        "diffie-hellman-group-exchange-sha1",
        "diffie-hellman-group-exchange-sha256",
        "ecdh-sha2-nistp256",
        "ecdh-sha2-nistp384",
        "ecdh-sha2-nistp521",
        "curve25519-sha256@libssh.org",
        "diffie-hellman-group16-sha512",
    )

    LEGACY_CIPHERS = (
        "aes128-cbc",
        "aes256-cbc",
        "3des-cbc",
        "aes192-cbc",
        "aes128-ctr",
        "aes192-ctr",
        "aes256-ctr",
    )

    LEGACY_KEYS = (
        "ssh-rsa",
        "ssh-dss",
        "ecdsa-sha2-nistp256",
        "ecdsa-sha2-nistp384",
        "ecdsa-sha2-nistp521",
        "ssh-ed25519",
        "rsa-sha2-256",
        "rsa-sha2-512",
    )

    @staticmethod
    def make_transport(sock, **kwargs):
        """
        Transport factory for paramiko SSHClient.connect(transport_factory=...).

        Creates a Transport instance with legacy algorithm preferences set
        at the INSTANCE level — no global state mutation.

        Args:
            sock: Connected socket (passed by SSHClient.connect)
            **kwargs: gss_kex, gss_deleg_creds, disabled_algorithms
                      (forwarded from SSHClient.connect)
        """
        t = paramiko.Transport(sock, **kwargs)
        t._preferred_kex = LegacySSHSupport.LEGACY_KEX
        t._preferred_ciphers = LegacySSHSupport.LEGACY_CIPHERS
        t._preferred_keys = LegacySSHSupport.LEGACY_KEYS
        logger.debug("Transport configured with legacy algorithms (instance-level)")
        return t


def load_private_key(config: SSHConfig) -> paramiko.PKey:
    """
    Load private key from PEM string or file.

    Tries key types in order: Ed25519 → RSA → ECDSA.
    This covers the common key formats encountered in production
    network environments.

    Args:
        config: SSHConfig with key_content or key_file set.

    Returns:
        Loaded paramiko PKey instance.

    Raises:
        ValueError: If no key source provided or key format unsupported.
    """
    passphrase = config.key_passphrase

    if config.key_content:
        key_source = StringIO(config.key_content)
        load_method = "from_private_key"
        logger.debug("Loading key from memory (vault/paste)")
    elif config.key_file:
        key_file = os.path.expanduser(config.key_file)
        if not os.path.exists(key_file):
            raise ValueError(f"Key file not found: {key_file}")
        key_source = key_file
        load_method = "from_private_key_file"
        logger.debug(f"Loading key from file: {key_file}")
    else:
        raise ValueError("No key source provided")

    key_classes = [
        ("Ed25519", paramiko.Ed25519Key),
        ("RSA", paramiko.RSAKey),
        ("ECDSA", paramiko.ECDSAKey),
    ]

    for key_name, key_class in key_classes:
        try:
            loader = getattr(key_class, load_method)
            if load_method == "from_private_key":
                if hasattr(key_source, "seek"):
                    key_source.seek(0)
                return loader(key_source, password=passphrase)
            else:
                return loader(key_source, password=passphrase)
        except Exception as e:
            logger.debug(f"{key_name} key load failed: {e}")
            continue

    raise ValueError(
        "Unable to load private key — unsupported format. "
        "Supported: Ed25519, RSA, ECDSA"
    )