import argparse
import ipaddress
import textwrap
import urllib.parse
from typing import List, Optional, cast

from fugue.cli import config as cli_config
from fugue.cli import explain as cli_explain
from fugue.cli import hub as cli_hub
from fugue.cli import interface as cli_interface
from fugue.cli import peer as cli_peer
from fugue.cli import show as cli_show
from fugue.cli import spoke as cli_spoke
from fugue.cli import tunnel as cli_tunnel
from fugue.config import CONFIG
from fugue.config import show as config_show
from fugue.lib import FS, Hub, Interface, Peer, Spoke, Tunnel, utils

# TODO: what license to use?
# They should also have to republish modifications under the same license. I
# guess I'd also want modifications to be open-source.
#
# I think that's helpful?
# https://opensource.stackexchange.com/questions/4875/open-source-license-to-prevent-commercial-use  # noqa: E501
#
# I think this didn't really have what I was looking for: https://man.sr.ht/license.md.

# TODO: can we run checks to ensure that a site's port is properly forwarded through the router?
# TODO: can we run checks to make sure a site's endpoint is reachable?
# TODO: can we run checks to make sure a site's has got dynamic DNS properly setup?
# TODO: should we check that the site is properly set as the LAN's default DNS, if possible?
# TODO: is any of the above NOT in the scope of fugue?


def parse_args(
    init_interfaces: List[Interface],
    init_peers: List[Peer],
    init_tunnels: List[Tunnel],
    init_hubs: List[Hub],
) -> argparse.Namespace:
    parser = argparse.ArgumentParser()

    subparsers = parser.add_subparsers(help="Fugue command", dest="command")

    # the subparser had subparsers
    cli_explain.set_subparser(cast(argparse.Namespace, subparsers))
    cli_show.set_subparser(cast(argparse.Namespace, subparsers))
    cli_config.set_subparser(cast(argparse.Namespace, subparsers))
    cli_interface.set_subparser(
        cast(argparse.Namespace, subparsers), init_interfaces
    )
    cli_peer.set_subparser(cast(argparse.Namespace, subparsers), init_peers)
    cli_tunnel.set_subparser(
        cast(argparse.Namespace, subparsers),
        init_tunnels,
        init_interfaces,
        init_peers,
    )
    cli_hub.set_subparser(cast(argparse.Namespace, subparsers))
    cli_spoke.set_subparser(cast(argparse.Namespace, subparsers), init_hubs)

    return parser.parse_args()


def cli() -> None:
    # TODO: I don't think that's needed anymore. If the configuration points at
    # </etc/wireguard/> then we should let a permission error bubble
    utils.check_privileges()
    fs: FS = FS(CONFIG)

    init_interfaces: List[Interface] = fs.get_interfaces()
    init_peers: List[Peer] = fs.get_peers()
    init_tunnels: List[Tunnel] = fs.get_tunnels()
    init_hubs: List[Hub] = fs.get_hubs()

    args = parse_args(init_interfaces, init_peers, init_tunnels, init_hubs)
    cli_interface.check_args(args)

    # init vars
    interfaces: List[Interface]
    peers: List[Peer]
    tunnels: List[Tunnel]
    hubs: List[Hub]
    spokes: Optional[List[Spoke]]

    interface: Interface
    peer: Peer
    tunnel: Tunnel
    hub: Optional[Hub]
    spoke: Spoke

    # explain
    # ==========================================================================
    if args.command == "explain":
        # we'll just cover the basics for now
        peers = fs.get_peers()
        interfaces = fs.get_interfaces()
        hubs = fs.get_hubs()
        spokes = fs.get_spokes()

        # 1. peer acts as a hub
        message: str = ""
        if len(hubs) == 1:
            hub = hubs[0]
            hub_address = hub.tunnel.interface.address
            hub_listen_port = hub.tunnel.interface.listen_port
            if hub.as_peer.endpoint is None:
                raise RuntimeError(
                    "Peer configuration for a Hub can not have an empty endpoint"
                )
            hub_endpoint = hub.as_peer.endpoint.path

            message += textwrap.fill(
                f"This computer acts as a Hub with name: '{hub.name}' for IP "
                f"addresses belonging to the subnet: {hub_address}. It is "
                f"listening on the port: {hub_listen_port}, and it "
                f"currently serves {len(hub.tunnel.peers)} Spoke(s).",
                width=80,
            )

            message += "\n\n"

            message += textwrap.fill(
                f"It should be accessible at the endpoint: {hub_endpoint}. "
                "If it is running behind a NAT device, remember to set the "
                "proper port-forwarding in place; e.g. from any port on the "
                "router, e.g. 45678, to this computer's private IP address "
                f"on port: {hub_listen_port}. Running 'ip addr' should tell "
                "what the private IP is.",
                width=80,
            )

            message += "\n\n"

            if hub.tunnel.is_running():
                message += textwrap.fill(
                    "It is currently running and accepting connections. It "
                    "can be stopped with the CLI by typing:",
                    width=80,
                )
                message += "\n\n"
                message += textwrap.fill(
                    "It is currently running and accepting connections. It "
                    f"$ fugue hub stop -n {hub.name}",
                    width=80,
                )

            else:
                message += textwrap.fill(
                    "It is currently not running. It can be started with the "
                    "CLI by typing:",
                    width=80,
                )
                message += "\n\n"
                message += textwrap.fill(
                    f"$ fugue hub start -n {hub.name}", width=80
                )

            message += "\n\n"
            message += textwrap.fill(
                "It can also be enabled or disabled with the CLI: ", width=80
            )
            message += "\n\n"
            message += textwrap.fill(
                f"$ fugue hub enable -n {hub.name}", width=80
            )
            message += "\n"
            message += textwrap.fill(
                f"$ fugue hub disable -n {hub.name}", width=80
            )

        if message != "":
            print(message)

    # show
    # ==========================================================================
    if args.command == "show":
        # show exhaustive list of resources
        peers = fs.get_peers()
        interfaces = fs.get_interfaces()
        tunnels = fs.get_tunnels()
        hubs = fs.get_hubs()
        spokes = fs.get_spokes()

        config_show(CONFIG)
        print()
        print(f"interfaces ({len(interfaces)}): {interfaces}")
        print()
        print(f"peers ({len(peers)}): {peers}")
        print()
        print(f"tunnels ({len(tunnels)}): {tunnels}")
        print()
        print(f"hubs ({len(hubs)}): {hubs}")
        print()
        print(f"spokes ({len(spokes)}): {spokes}")

    # config
    # ==========================================================================
    elif args.command == "config":
        if args.action == "show":
            config_show(CONFIG)

    # Interface
    # ==========================================================================
    elif args.command == "interface":
        if args.action == "new":
            interface = Interface(
                args.name,
                args.private_key,
                ipaddress.IPv4Network(args.address),
                listen_port=args.listen_port,
                dns=[ipaddress.IPv4Address(dns) for dns in args.dns],
                table=args.table,
                mtu=args.mtu,
                pre_ups=args.pre_ups,
                post_ups=args.post_ups,
                pre_downs=args.pre_downs,
                post_downs=args.post_downs,
            )
            print(interface)
            fs.write(interface)

        elif args.action == "show":
            interfaces = fs.get_interfaces()
            print(interfaces)

        elif args.action == "rm":
            interface = fs.get_interface_from_name(args.name)
            fs.rm(interface)

        elif args.action == "set":
            # TODO: do we need to check whether that interface is used by any tunnel?
            interface = fs.get_interface_from_name(args.name)
            if args.private_key:
                interface.private_key = args.private_key

            if args.address:
                interface.address = ipaddress.IPv4Network(args.address)

            if args.listen_port:
                interface.listen_port = args.listen_port

            if args.name:
                # TODO: if we change the name of the interface, should the filename (or any symlink) be cleaned up?
                # If we wanted **not to**, then filename should not depend on the # Name attribute
                # Which prompts the question, are any "Names" unique in fugue? How about hub's names?
                # The only unique thing about a tunnel, interface or peer, is the combination of its attributes
                interface.name = args.name

            if args.dns:
                interface.dns = [ipaddress.IPv4Address(dns) for dns in args.dns]

            if args.private_key:
                interface.private_key = args.private_key

            if args.private_key:
                interface.private_key = args.private_key

            if args.private_key:
                interface.private_key = args.private_key

            # TODO: I think we should write this after it's been set, right?
            fs.write(interface)

    # Peer
    # ==========================================================================
    elif args.command == "peer":
        if args.action == "new":
            peer = Peer(
                args.name,
                args.public_key,
                [ipaddress.IPv4Network(aip) for aip in args.allowed_ips],
                endpoint=args.endpoint,
                persistent_keepalive=args.persistent_keepalive,
            )
            print(peer)
            fs.write(peer)

        elif args.action == "show":
            peers = fs.get_peers()
            print(peers)

        elif args.action == "rm":
            peer = fs.get_peer_from_name(args.name)
            fs.rm(peer)

    # Tunnel
    # ==========================================================================
    elif args.command == "tunnel":
        if args.action == "show":
            tunnels = fs.get_tunnels()
            print(f"tunnels: {tunnels}")

        elif args.action == "new":
            interface = fs.get_interface_from_name(args.interface_name)
            peers = []
            for peer_name in args.peer_names:
                peer = fs.get_peer_from_name(peer_name)
                peers.append(peer)

            # TODO: how to properly handle names when those do not exist?
            name: str = "unnamed"
            tunnel = Tunnel(name, interface, peers)
            print(tunnel)
            fs.write(tunnel)

        elif args.action == "rm":
            tunnel = fs.get_tunnel_from_name(args.name)
            fs.rm(tunnel)

        elif args.action == "start":
            tunnel = fs.get_tunnel_from_name(args.name)
            tunnel.start()

        elif args.action == "stop":
            tunnel = fs.get_tunnel_from_name(args.name)
            tunnel.stop()

        elif args.action == "enable":
            tunnel = fs.get_tunnel_from_name(args.name)
            tunnel.enable()

        elif args.action == "disable":
            tunnel = fs.get_tunnel_from_name(args.name)
            tunnel.disable()

    # Hub
    # ==========================================================================
    elif args.command == "hub":
        if args.action == "gen":
            hub = Hub.autogen(
                args.name,
                args.listen_port,
                urllib.parse.urlparse(args.endpoint),
            )
            print(hub)
            fs.write(hub)

        elif args.action == "rm":
            hub = fs.get_hub_from_name(args.name)
            fs.rm(hub)

        elif args.action == "show":
            hubs = fs.get_hubs()
            print(f"hubs: {hubs}")

        elif args.action == "start":
            hub = fs.get_hub_from_name(args.name)
            hub.tunnel.start()

        elif args.action == "stop":
            hub = fs.get_hub_from_name(args.name)
            hub.tunnel.stop()

        elif args.action == "enable":
            hub = fs.get_hub_from_name(args.name)
            hub.tunnel.enable()

        elif args.action == "disable":
            hub = fs.get_hub_from_name(args.name)
            hub.tunnel.disable()

        elif args.action == "forward":
            hub = fs.get_hub_from_name(args.name)
            hub.forward(args.external_port)

    # Spoke
    # ==========================================================================
    elif args.command == "spoke":
        if args.action == "gen":
            hub = fs.get_hub_from_name(args.hub_name)
            spoke = Spoke.autogen(args.name, hub)
            print(spoke)
            fs.write(spoke)
            # need to regenerate the tunnel (as it's got new peers)
            # TODO: if the tunnel is already up we should be running some
            # custom <wg> commands I reckon. There's a git branch for that
            fs.write(spoke.hub.tunnel)

        elif args.action == "show":
            if args.hub_name:
                hub = fs.get_hub_from_name(args.hub_name)
                spokes = fs.get_spokes_for_hub(hub)
            else:
                spokes = fs.get_spokes()
            print(spokes)


if __name__ == "__main__":
    cli()
