import sys

from attr import attrib, attrs
from twisted.internet.defer import inlineCallbacks
from twisted.internet.endpoints import clientFromString
from zope.interface.declarations import directlyProvides

from . import _interfaces, errors
from .timing import DebugTiming

try:
    import txtorcon
except ImportError:
    txtorcon = None


@attrs
class SocksOnlyTor:
    _reactor = attrib()

    def stream_via(self, host, port, tls=False):
        return txtorcon.TorClientEndpoint(
            host,
            port,
            socks_endpoint=None,  # tries localhost:9050 and 9150
            tls=tls,
            reactor=self._reactor,
        )


@inlineCallbacks
def get_tor(reactor,
            launch_tor=False,
            tor_control_port=None,
            timing=None,
            stderr=sys.stderr):
    """
    If launch_tor=True, I will try to launch a new Tor process, ask it
    for its SOCKS and control ports, and use those for outbound
    connections (and inbound onion-service listeners, if necessary).

    Otherwise if tor_control_port is provided, I will attempt to connect
    to an existing Tor's control port at the endpoint it specifies.  I'll
    ask that Tor for its SOCKS port.

    With no arguments, I will try to connect to an existing Tor's control
    port at the usual places: [unix:/var/run/tor/control,
    tcp:127.0.0.1:9051, tcp:127.0.0.1:9151].  If any are successful, I'll
    ask that Tor for its SOCKS port.  If none are successful, I'll
    attempt to do SOCKS to the usual places: [tcp:127.0.0.1:9050,
    tcp:127.0.0.1:9150].

    If I am unable to make a SOCKS connection, the initial connection to
    the Rendezvous Server will fail, and the program will terminate.

    Control-port connections can only succeed if I can authenticate (by
    reading a cookie file named by the Tor process), so the current user
    must have permission to read that file (either they started Tor, e.g.
    TorBrowser, or they are in a unix group that's been given access,
    e.g. debian-tor).
    """
    # rationale: launching a new Tor takes a long time, so only do it if
    # the user specifically asks for it with --launch-tor. Using an
    # existing Tor should be much faster, but still requires general
    # permission via --tor.

    if not txtorcon:
        raise errors.NoTorError()

    if not isinstance(launch_tor, bool):  # note: False is int
        raise TypeError("launch_tor= must be boolean")
    if not isinstance(tor_control_port, (str, type(None))):
        raise TypeError("tor_control_port= must be str or None")
    assert tor_control_port != ""
    if launch_tor and tor_control_port is not None:
        raise ValueError("cannot combine --launch-tor and --tor-control-port=")
    timing = timing or DebugTiming()

    # Connect to an existing Tor, or create a new one. If we need to
    # launch an onion service, then we need a working control port (and
    # authentication cookie). If we're only acting as a client, we don't
    # need the control port.

    if launch_tor:
        print(
            " launching a new Tor process, this may take a while..",
            file=stderr)
        with timing.add("launch tor"):
            tor = yield txtorcon.launch(reactor,
                                        # data_directory=,
                                        # tor_binary=,
                                        )
    elif tor_control_port:
        with timing.add("find tor"):
            control_ep = clientFromString(reactor, tor_control_port)
            tor = yield txtorcon.connect(reactor, control_ep)  # might raise
            print(
                f" using Tor via control port at {tor_control_port}",
                file=stderr)
    else:
        # Let txtorcon look through a list of usual places. If that fails,
        # we'll arrange to attempt the default SOCKS port
        with timing.add("find tor"):
            try:
                tor = yield txtorcon.connect(reactor)
                print(" using Tor via default control port", file=stderr)
            except Exception:
                # TODO: make this more specific. I think connect() is
                # likely to throw a reactor.connectTCP -type error, like
                # ConnectionFailed or ConnectionRefused or something
                print(
                    " unable to find default Tor control port, using SOCKS",
                    file=stderr)
                tor = SocksOnlyTor(reactor)
    directlyProvides(tor, _interfaces.ITorManager)
    return tor
