"""Fail-closed, read-only SSH authentication and server-identity probe.

The probe never opens an SSH channel or executes a command.  It verifies a
pinned server host key before presenting the vault-decrypted password, then
reports only public SSH identity metadata.
"""
from __future__ import annotations

import base64
import hashlib
import hmac
import socket
from dataclasses import dataclass

from app.core.device_credentials import (
    CredentialDecryptionError,
    CredentialKeyMissingError,
    decrypt_secret,
)


@dataclass(frozen=True)
class SSHIdentityTestResult:
    ok: bool
    message: str
    server_fingerprint: str | None = None
    server_banner: str | None = None


def _normalize_sha256_fingerprint(value: str | None) -> str | None:
    """Return canonical SSH SHA256 fingerprint text, or None for invalid input."""
    if not value:
        return None
    candidate = value.strip()
    if candidate.startswith("SHA256:"):
        candidate = candidate[7:]
    candidate = candidate.rstrip("=")
    if not candidate:
        return None
    try:
        decoded = base64.b64decode(candidate + ("=" * (-len(candidate) % 4)), validate=True)
    except Exception:
        return None
    return base64.b64encode(decoded).decode("ascii").rstrip("=")


def _fingerprint_for_key(key: object) -> str:
    key_bytes = key.asbytes()
    return base64.b64encode(hashlib.sha256(key_bytes).digest()).decode("ascii").rstrip("=")


def test_pinned_ssh_identity(
    *,
    host: str,
    port: int,
    username: str,
    encrypted_secret: str,
    expected_host_key_fingerprint: str | None,
    timeout_seconds: float = 5.0,
) -> SSHIdentityTestResult:
    """Authenticate only after strict host-key pin verification; send no commands."""
    expected = _normalize_sha256_fingerprint(expected_host_key_fingerprint)
    if expected is None:
        return SSHIdentityTestResult(False, "Tes SSH diblokir: fingerprint host key SSH yang dipin belum dikonfigurasi.")
    if not host or not (1 <= port <= 65535):
        return SSHIdentityTestResult(False, "Tes SSH diblokir: host atau port management tidak valid.")

    transport = None
    sock = None
    try:
        # Import lazily so unrelated application paths do not require Paramiko.
        import paramiko

        sock = socket.create_connection((host, port), timeout=timeout_seconds)
        transport = paramiko.Transport(sock)
        transport.banner_timeout = timeout_seconds
        transport.auth_timeout = timeout_seconds
        transport.start_client(timeout=timeout_seconds)

        server_key = transport.get_remote_server_key()
        observed = _fingerprint_for_key(server_key)
        if not hmac.compare_digest(observed, expected):
            return SSHIdentityTestResult(False, "Tes SSH diblokir: fingerprint server tidak cocok dengan host key yang dipin.")

        # Decrypt only after the server identity is pinned and verified.  The
        # plaintext remains function-local and is never logged or returned.
        secret = decrypt_secret(encrypted_secret)
        try:
            transport.auth_password(username, secret, fallback=False)
        finally:
            secret = ""
        if not transport.is_authenticated():
            return SSHIdentityTestResult(False, "Autentikasi SSH ditolak. Tidak ada perintah perangkat yang dijalankan.")

        banner = transport.remote_version or None
        return SSHIdentityTestResult(
            True,
            "Autentikasi SSH dan identitas host key terverifikasi. Tidak ada channel atau perintah perangkat yang dijalankan.",
            server_fingerprint=f"SHA256:{observed}",
            server_banner=banner,
        )
    except (CredentialKeyMissingError, CredentialDecryptionError):
        return SSHIdentityTestResult(False, "Tes SSH diblokir: credential vault tidak dapat digunakan oleh server.")
    except (OSError, socket.timeout):
        return SSHIdentityTestResult(False, "Tes SSH gagal: host atau port management tidak dapat dijangkau.")
    except Exception:
        # Paramiko errors intentionally stay server-side: they can include
        # target-specific details that do not belong in API responses.
        return SSHIdentityTestResult(False, "Tes SSH gagal atau autentikasi ditolak. Tidak ada perintah perangkat yang dijalankan.")
    finally:
        if transport is not None:
            transport.close()
        elif sock is not None:
            sock.close()
