# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
#
# SPDX-License-Identifier: MIT

"""a plain CoAP proxy that can work both as forward and as reverse proxy"""

import sys
import argparse

import aiocoap
from aiocoap.proxy.server import (
    ForwardProxyWithPooledObservations,
    ProxyWithPooledObservations,
    NameBasedVirtualHost,
    SubdomainVirtualHost,
    SubresourceVirtualHost,
    UnconditionalRedirector,
)
from aiocoap.util.cli import AsyncCLIDaemon
from aiocoap.cli.common import add_server_arguments, server_context_from_arguments


def build_parser():
    p = argparse.ArgumentParser(description=__doc__)

    mode = p.add_argument_group(
        "mode", "Required argument for setting the operation mode"
    )
    mode.add_argument("--forward", help="Run as forward proxy", action="store_true")
    mode.add_argument("--reverse", help="Run as reverse proxy", action="store_true")

    details = p.add_argument_group(
        "details", "Options that govern how requests go in and out"
    )
    add_server_arguments(details)
    details.add_argument(
        "--register",
        help="Register with a Resource directory",
        metavar="RD-URI",
        nargs="?",
        default=False,
    )
    details.add_argument(
        "--register-as",
        help="Endpoint name (with possibly a domain after a dot) to register as",
        metavar="EP[.D]",
        default=None,
    )
    details.add_argument(
        "--register-proxy",
        help="Ask the RD to serve as a reverse proxy. Note that this is only practical for --unconditional or --pathbased reverse proxies.",
        action="store_true",
    )

    r = p.add_argument_group(
        "Rules",
        description="Sequence of forwarding rules "
        "that, if matched by a request, specify a forwarding destination. Destinations can be prefixed to change their behavior: With an '@' sign, they are treated as forward proxies. With a '!' sign, the destination is set as Uri-Host.",
    )

    class TypedAppend(argparse.Action):
        def __call__(self, parser, namespace, values, option_string=None):
            if getattr(namespace, self.dest) is None:
                setattr(namespace, self.dest, [])
            getattr(namespace, self.dest).append((option_string, values))

    r.add_argument(
        "--namebased",
        help="If Uri-Host matches NAME, route to DEST",
        metavar="NAME:DEST",
        action=TypedAppend,
        dest="r",
    )
    r.add_argument(
        "--subdomainbased",
        help="If Uri-Host is anything.NAME, route to DEST",
        metavar="NAME:DEST",
        action=TypedAppend,
        dest="r",
    )
    r.add_argument(
        "--pathbased",
        help="If a requested path starts with PATH, split that part off and route to DEST",
        metavar="PATH:DEST",
        action=TypedAppend,
        dest="r",
    )
    r.add_argument(
        "--unconditional",
        help="Route all requests not previously matched to DEST",
        metavar="DEST",
        action=TypedAppend,
        dest="r",
    )

    return p


def destsplit(dest):
    use_as_proxy = False
    rewrite_uri_host = False
    if dest.startswith("!"):
        dest = dest[1:]
        rewrite_uri_host = True
    if dest.startswith("@"):
        dest = dest[1:]
        use_as_proxy = True
    return dest, rewrite_uri_host, use_as_proxy


class Main(AsyncCLIDaemon):
    async def start(self, args=None):
        parser = build_parser()
        options = parser.parse_args(args if args is not None else sys.argv[1:])
        self.options = options

        if not options.forward and not options.reverse:
            raise parser.error("At least one of --forward and --reverse must be given.")

        self.outgoing_context = await aiocoap.Context.create_client_context()
        if options.forward:
            proxy = ForwardProxyWithPooledObservations(self.outgoing_context)
        else:
            proxy = ProxyWithPooledObservations(self.outgoing_context)
        for kind, data in options.r or ():
            if kind in ("--namebased", "--subdomainbased"):
                try:
                    name, dest = data.split(":", 1)
                except Exception:
                    raise parser.error("%s needs NAME:DEST as arguments" % kind)
                dest, rewrite_uri_host, use_as_proxy = destsplit(dest)
                if rewrite_uri_host and kind == "--subdomainbased":
                    parser.error(
                        "The flag '!' makes no sense for subdomain based redirection as the subdomain data would be lost"
                    )
                r = (
                    NameBasedVirtualHost
                    if kind == "--namebased"
                    else SubdomainVirtualHost
                )(name, dest, rewrite_uri_host, use_as_proxy)
            elif kind == "--pathbased":
                try:
                    path, dest = data.split(":", 1)
                except Exception:
                    raise parser.error("--pathbased needs PATH:DEST as arguments")
                r = SubresourceVirtualHost(path.split("/"), dest)
            elif kind == "--unconditional":
                dest, rewrite_uri_host, use_as_proxy = destsplit(data)
                if rewrite_uri_host:
                    parser.error(
                        "The flag '!' makes no sense for unconditional redirection as the host name data would be lost"
                    )
                r = UnconditionalRedirector(dest, use_as_proxy)
            else:
                raise AssertionError("Unknown redirectory kind")
            proxy.add_redirector(r)

        self.proxy_context = await server_context_from_arguments(proxy, options)

        if options.register is not False:
            from aiocoap.resourcedirectory.client.register import Registerer

            params = {}
            if options.register_as:
                ep, _, d = options.register_as.partition(".")
                params["ep"] = ep
                if d:
                    params["d"] = d
            if options.register_proxy:
                # FIXME: Check this in discovery
                params["proxy"] = "on"
            # FIXME: Construct this from settings (path-based), and forward results
            proxy.get_resources_as_linkheader = lambda: ""
            self.registerer = Registerer(
                self.proxy_context,
                rd=options.register,
                lt=60,
                registration_parameters=params,
            )

    async def shutdown(self):
        if self.options.register is not False:
            await self.registerer.shutdown()
        await self.outgoing_context.shutdown()
        await self.proxy_context.shutdown()


sync_main = Main.sync_main

if __name__ == "__main__":
    # if you want to run this using `python3 -m`, see http://bugs.python.org/issue22480
    sync_main()
