"""
TLS cipher‑sweep utility for the Port Agent.

This module probes a local TLS‑enabled port by attempting handshakes across a
matrix of forced TLS versions and cipher suites. Each combination is tested
independently, producing a detailed compatibility map that shows exactly which
protocol versions and ciphers the server accepts. The goal is not to judge
security strength but to expose raw capability signals for higher‑level
analysis.

All scans are restricted to localhost via `resolve_local_address()` to ensure
the agent cannot probe remote systems. Each handshake attempt records whether
the cipher was accepted, rejected by the TLS context, blocked at TCP connect,
or failed during the handshake itself. Errors are captured as structured fields
so the agent can reason about misconfigurations and provide feedback to
developers.

The final result (`TlsCipherSweepResult`) includes the full test matrix,
deduplicated lists of supported versions and ciphers, and total sweep duration.
This tool is useful for diagnosing unexpected TLS behavior, identifying weak or
legacy cipher exposure, and helping the agent suggest improvements to the
tooling when patterns of failure emerge.
"""

import ssl
import socket
import time
from dataclasses import dataclass
from typing import List, Optional
from .utility import resolve_local_address


@dataclass
class CipherSupport:
    tls_version: str
    cipher: str
    success: bool
    error: Optional[str]
    errno: Optional[int]


@dataclass
class TlsCipherSweepResult:
    matrix: List[CipherSupport]
    supported_versions: List[str]
    supported_ciphers: List[str]
    duration_ms: float
    error: Optional[str]
    errno: Optional[int]


def tls_cipher_sweep(
    local_address_mode: str,
    port: int,
    timeout: float = 3.0,
    tls_versions: Optional[List[ssl.TLSVersion]] = None,
    cipher_list: Optional[List[str]] = None,
) -> TlsCipherSweepResult:
    """
    Enumerate supported TLS versions and cipher suites by attempting
    handshakes with forced version + cipher combinations.
    Localhost-only for safety.
    """

    # Enforce local-only restriction
    local_address = resolve_local_address(local_address_mode)
    if not local_address:
        return TlsCipherSweepResult(
            matrix=[],
            supported_versions=[],
            supported_ciphers=[],
            duration_ms=0.0,
            error="NON_LOCAL_ADDRESS",
            errno=-1,
        )

    # Default TLS versions
    if tls_versions is None:
        tls_versions = [
            ssl.TLSVersion.TLSv1,
            ssl.TLSVersion.TLSv1_1,
            ssl.TLSVersion.TLSv1_2,
            ssl.TLSVersion.TLSv1_3,
        ]

    # Default cipher list
    if cipher_list is None:
        cipher_list = ssl._DEFAULT_CIPHERS.split(":")

    results: List[CipherSupport] = []
    supported_versions = set()
    supported_ciphers = set()

    start = time.perf_counter()

    for version in tls_versions:
        for cipher in cipher_list:

            # Build TLS context
            ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE
            ctx.minimum_version = version
            ctx.maximum_version = version

            # Set cipher
            try:
                ctx.set_ciphers(cipher)
            except Exception as e:
                results.append(
                    CipherSupport(
                        tls_version=version.name,
                        cipher=cipher,
                        success=False,
                        error=f"cipher rejected by context: {e}",
                        errno=None,
                    )
                )
                continue

            # TCP connect
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.settimeout(timeout)

            try:
                err = s.connect_ex((local_address, port))
                if err != 0:
                    s.close()
                    results.append(
                        CipherSupport(
                            tls_version=version.name,
                            cipher=cipher,
                            success=False,
                            error=f"tcp connect_ex failed",
                            errno=err,
                        )
                    )
                    continue
            except Exception as e:
                s.close()
                results.append(
                    CipherSupport(
                        tls_version=version.name,
                        cipher=cipher,
                        success=False,
                        error=f"tcp exception: {e}",
                        errno=getattr(e, "errno", None),
                    )
                )
                continue

            # TLS wrap
            try:
                tls_sock = ctx.wrap_socket(s, server_hostname=local_address)
            except Exception as e:
                s.close()
                results.append(
                    CipherSupport(
                        tls_version=version.name,
                        cipher=cipher,
                        success=False,
                        error=f"wrap_socket error: {e}",
                        errno=getattr(e, "errno", None),
                    )
                )
                continue

            # TLS handshake
            try:
                tls_sock.do_handshake()
                tls_sock.close()

                results.append(
                    CipherSupport(
                        tls_version=version.name,
                        cipher=cipher,
                        success=True,
                        error=None,
                        errno=0,
                    )
                )

                supported_versions.add(version.name)
                supported_ciphers.add(cipher)

            except Exception as e:
                tls_sock.close()
                results.append(
                    CipherSupport(
                        tls_version=version.name,
                        cipher=cipher,
                        success=False,
                        error=str(e),
                        errno=getattr(e, "errno", None),
                    )
                )

    end = time.perf_counter()

    return TlsCipherSweepResult(
        matrix=results,
        supported_versions=sorted(supported_versions),
        supported_ciphers=sorted(supported_ciphers),
        duration_ms=(end - start) * 1000.0,
        error=None,
        errno=0,
    )


if __name__ == "__main__":
    localhost = "ipv4_loopback"
    port = 443

    print(f"Running tls_cipher_sweep({localhost}, {port})...")
    result = tls_cipher_sweep(localhost, port)

    print("\n=== TLS Cipher Sweep Result ===")
    print(f"Supported TLS Versions : {result.supported_versions}")
    print(f"Supported Ciphers      : {len(result.supported_ciphers)}")
    print(f"Duration (ms)          : {result.duration_ms:.2f}")
    print(f"Error                  : {result.error}")
    print(f"Errno                  : {result.errno}")

    print("\n--- Sample Results (first 10 entries) ---")
    for entry in result.matrix[:10]:
        print(
            f"{entry.tls_version} / {entry.cipher} -> "
            f"{entry.success} (errno={entry.errno}, error={entry.error})"
        )
