# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

from __future__ import annotations

from typing import TYPE_CHECKING, Callable

from twisted.conch import error
from twisted.conch.ssh import transport
from twisted.internet import defer, protocol, reactor
from twisted.internet.address import IPv4Address, IPv6Address
from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.interfaces import (
    IAddress,
    IConnector,
    IListeningPort,
    IReactorTCP,
)
from twisted.python.failure import Failure

if TYPE_CHECKING:
    from twisted.conch.client.options import ConchOptions
    from twisted.conch.ssh.userauth import SSHUserAuthClient


class SSHClientFactory(protocol.ClientFactory["transport.SSHClientTransport"]):
    def __init__(
        self,
        d: Deferred[None],
        options: ConchOptions,
        verifyHostKey: _VHK,
        userAuthObject: SSHUserAuthClient,
    ) -> None:
        self.d: Deferred[None] | None = d
        self.options = options
        self.verifyHostKey = verifyHostKey
        self.userAuthObject = userAuthObject

    def clientConnectionLost(self, connector: IConnector, reason: Failure) -> None:
        if self.options["reconnect"]:
            connector.connect()

    def clientConnectionFailed(self, connector: IConnector, reason: Failure) -> None:
        if self.d is None:
            return
        d, self.d = self.d, None
        d.errback(reason)

    def buildProtocol(self, addr: IAddress | None) -> SSHClientTransport:
        trans = SSHClientTransport(self)
        if self.options["ciphers"]:
            trans.supportedCiphers = self.options["ciphers"]
        if self.options["macs"]:
            trans.supportedMACs = self.options["macs"]
        if self.options["compress"]:
            trans.supportedCompressions[0:1] = [b"zlib"]
        if self.options["host-key-algorithms"]:
            trans.supportedPublicKeys = self.options["host-key-algorithms"]
        return trans


class SSHClientTransport(transport.SSHClientTransport):
    # pre-mypy LSP violation
    factory: SSHClientFactory

    def __init__(self, factory: SSHClientFactory) -> None:
        self.factory = factory
        self.unixServer: None | IListeningPort = None

    def connectionLost(self, reason: Failure | None = None) -> None:
        if self.unixServer:
            # The C{unixServer} attribute is untested, and it's not entirely
            # clear that it does anything at all. It appears to be a vestigial
            # attempt to support something like OpenSSH's ControlMaster client
            # option; at some point we should either document and test it, or
            # remove it.

            # https://github.com/twisted/twisted/issues/12418
            d = maybeDeferred(self.unixServer.stopListening)  # pragma: no cover
            self.unixServer = None  # pragma: no cover
        else:
            d = defer.succeed(None)
        d.addCallback(
            lambda x: transport.SSHClientTransport.connectionLost(self, reason)
        )

    def receiveError(self, code, desc):
        if self.factory.d is None:
            return
        d, self.factory.d = self.factory.d, None
        d.errback(error.ConchError(desc, code))

    def sendDisconnect(self, code, reason):
        if self.factory.d is None:
            return
        d, self.factory.d = self.factory.d, None
        transport.SSHClientTransport.sendDisconnect(self, code, reason)
        d.errback(error.ConchError(reason, code))

    def receiveDebug(self, alwaysDisplay, message, lang):
        self._log.debug(
            "Received Debug Message: {message}",
            message=message,
            alwaysDisplay=alwaysDisplay,
            lang=lang,
        )
        if alwaysDisplay:  # XXX what should happen here?
            print(message)

    def verifyHostKey(self, pubKey: bytes, fingerprint: str) -> Deferred[bool]:
        transport = self.transport
        assert transport is not None
        peer = transport.getPeer()
        assert isinstance(
            peer, (IPv4Address, IPv6Address)
        ), "Address must have a host to verify against."
        return self.factory.verifyHostKey(
            self, peer.host.encode("utf-8"), pubKey, fingerprint
        )

    def setService(self, service):
        self._log.info("setting client server to {service}", service=service)
        transport.SSHClientTransport.setService(self, service)
        if service.name != "ssh-userauth" and self.factory.d is not None:
            d, self.factory.d = self.factory.d, None
            d.callback(None)

    def connectionSecure(self):
        self.requestService(self.factory.userAuthObject)


_VHK = Callable[[SSHClientTransport, bytes, bytes, str], Deferred[bool]]


def connect(
    host: str,
    port: int,
    options: ConchOptions,
    verifyHostKey: _VHK,
    userAuthObject: SSHUserAuthClient,
) -> Deferred[None]:
    d: Deferred[None] = defer.Deferred()
    factory: protocol.ClientFactory[transport.SSHClientTransport] = SSHClientFactory(
        d, options, verifyHostKey, userAuthObject
    )
    # this is just broken, right? very straightforwardly this is inference
    # giving up because there's a cycle in the type graph
    # (_ProtoWithFactory.factory -> Factory[Self] -> Factory.protocol:
    # "Optional[Callable[..., P]]" -> P < _ProtoWithFactory)
    IReactorTCP(reactor).connectTCP(host, port, factory)  # type:ignore[arg-type]
    return d
