import argparse
import logging
import sys
from argparse import ArgumentError, Namespace

from asgiref.compatibility import guarantee_single_callable

from .access import AccessLogGenerator
from .endpoints import build_endpoint_description_strings
from .server import Server
from .utils import import_by_path

logger = logging.getLogger(__name__)

DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8000


class CommandLineInterface:
    """
    Acts as the main CLI entry point for running the server.
    """

    description = "Django HTTP/WebSocket server"

    server_class = Server

    def __init__(self):
        self.parser = argparse.ArgumentParser(description=self.description)
        self.parser.add_argument(
            "-p", "--port", type=int, help="Port number to listen on", default=None
        )
        self.parser.add_argument(
            "-b",
            "--bind",
            dest="host",
            help="The host/address to bind to",
            default=None,
        )
        self.parser.add_argument(
            "--websocket_timeout",
            type=int,
            help="Maximum time to allow a websocket to be connected. -1 for infinite.",
            default=86400,
        )
        self.parser.add_argument(
            "--websocket_connect_timeout",
            type=int,
            help="Maximum time to allow a connection to handshake. -1 for infinite",
            default=5,
        )
        self.parser.add_argument(
            "-u",
            "--unix-socket",
            dest="unix_socket",
            help="Bind to a UNIX socket rather than a TCP host/port",
            default=None,
        )
        self.parser.add_argument(
            "--fd",
            type=int,
            dest="file_descriptor",
            help="Bind to a file descriptor rather than a TCP host/port or named unix socket",
            default=None,
        )
        self.parser.add_argument(
            "-e",
            "--endpoint",
            dest="socket_strings",
            action="append",
            help="Use raw server strings passed directly to twisted",
            default=[],
        )
        self.parser.add_argument(
            "-v",
            "--verbosity",
            type=int,
            help="How verbose to make the output",
            default=1,
        )
        self.parser.add_argument(
            "-t",
            "--http-timeout",
            type=int,
            help="How long to wait for worker before timing out HTTP connections",
            default=None,
        )
        self.parser.add_argument(
            "--access-log",
            help="Where to write the access log (- for stdout, the default for verbosity=1)",
            default=None,
        )
        self.parser.add_argument(
            "--log-fmt",
            help="Log format to use",
            default="%(asctime)-15s %(levelname)-8s %(message)s",
        )
        self.parser.add_argument(
            "--ping-interval",
            type=int,
            help="The number of seconds a WebSocket must be idle before a keepalive ping is sent",
            default=20,
        )
        self.parser.add_argument(
            "--ping-timeout",
            type=int,
            help="The number of seconds before a WebSocket is closed if no response to a keepalive ping",
            default=30,
        )
        self.parser.add_argument(
            "--application-close-timeout",
            type=int,
            help="The number of seconds an ASGI application has to exit after client disconnect before it is killed",
            default=10,
        )
        self.parser.add_argument(
            "--root-path",
            dest="root_path",
            help="The setting for the ASGI root_path variable",
            default="",
        )
        self.parser.add_argument(
            "--proxy-headers",
            dest="proxy_headers",
            help="Enable parsing and using of X-Forwarded-For and X-Forwarded-Port headers and using that as the "
            "client address",
            default=False,
            action="store_true",
        )
        self.arg_proxy_host = self.parser.add_argument(
            "--proxy-headers-host",
            dest="proxy_headers_host",
            help="Specify which header will be used for getting the host "
            "part. Can be omitted, requires --proxy-headers to be specified "
            'when passed. "X-Real-IP" (when passed by your webserver) is a '
            "good candidate for this.",
            default=False,
            action="store",
        )
        self.arg_proxy_port = self.parser.add_argument(
            "--proxy-headers-port",
            dest="proxy_headers_port",
            help="Specify which header will be used for getting the port "
            "part. Can be omitted, requires --proxy-headers to be specified "
            "when passed.",
            default=False,
            action="store",
        )
        self.parser.add_argument(
            "application",
            help="The application to dispatch to as path.to.module:instance.path",
        )
        self.parser.add_argument(
            "-s",
            "--server-name",
            dest="server_name",
            help="specify which value should be passed to response header Server attribute",
            default="daphne",
        )
        self.parser.add_argument(
            "--no-server-name", dest="server_name", action="store_const", const=""
        )

        self.server = None

    @classmethod
    def entrypoint(cls):
        """
        Main entrypoint for external starts.
        """
        cls().run(sys.argv[1:])

    def _check_proxy_headers_passed(self, argument: str, args: Namespace):
        """Raise if the `--proxy-headers` weren't specified."""
        if args.proxy_headers:
            return
        raise ArgumentError(
            argument=argument,
            message="--proxy-headers has to be passed for this parameter.",
        )

    def _get_forwarded_host(self, args: Namespace):
        """
        Return the default host header from which the remote hostname/ip
        will be extracted.
        """
        if args.proxy_headers_host:
            self._check_proxy_headers_passed(argument=self.arg_proxy_host, args=args)
            return args.proxy_headers_host
        if args.proxy_headers:
            return "X-Forwarded-For"

    def _get_forwarded_port(self, args: Namespace):
        """
        Return the default host header from which the remote hostname/ip
        will be extracted.
        """
        if args.proxy_headers_port:
            self._check_proxy_headers_passed(argument=self.arg_proxy_port, args=args)
            return args.proxy_headers_port
        if args.proxy_headers:
            return "X-Forwarded-Port"

    def run(self, args):
        """
        Pass in raw argument list and it will decode them
        and run the server.
        """
        # Decode args
        args = self.parser.parse_args(args)
        # Set up logging
        logging.basicConfig(
            level={
                0: logging.WARN,
                1: logging.INFO,
                2: logging.DEBUG,
                3: logging.DEBUG,  # Also turns on asyncio debug
            }[args.verbosity],
            format=args.log_fmt,
        )
        # If verbosity is 1 or greater, or they told us explicitly, set up access log
        access_log_stream = None
        if args.access_log:
            if args.access_log == "-":
                access_log_stream = sys.stdout
            else:
                access_log_stream = open(args.access_log, "a", 1)
        elif args.verbosity >= 1:
            access_log_stream = sys.stdout

        # Import application
        sys.path.insert(0, ".")
        application = import_by_path(args.application)
        application = guarantee_single_callable(application)

        # Set up port/host bindings
        if not any(
            [
                args.host,
                args.port is not None,
                args.unix_socket,
                args.file_descriptor is not None,
                args.socket_strings,
            ]
        ):
            # no advanced binding options passed, patch in defaults
            args.host = DEFAULT_HOST
            args.port = DEFAULT_PORT
        elif args.host and args.port is None:
            args.port = DEFAULT_PORT
        elif args.port is not None and not args.host:
            args.host = DEFAULT_HOST
        # Build endpoint description strings from (optional) cli arguments
        endpoints = build_endpoint_description_strings(
            host=args.host,
            port=args.port,
            unix_socket=args.unix_socket,
            file_descriptor=args.file_descriptor,
        )
        endpoints = sorted(args.socket_strings + endpoints)
        # Start the server
        logger.info("Starting server at {}".format(", ".join(endpoints)))
        self.server = self.server_class(
            application=application,
            endpoints=endpoints,
            http_timeout=args.http_timeout,
            ping_interval=args.ping_interval,
            ping_timeout=args.ping_timeout,
            websocket_timeout=args.websocket_timeout,
            websocket_connect_timeout=args.websocket_connect_timeout,
            websocket_handshake_timeout=args.websocket_connect_timeout,
            application_close_timeout=args.application_close_timeout,
            action_logger=(
                AccessLogGenerator(access_log_stream) if access_log_stream else None
            ),
            root_path=args.root_path,
            verbosity=args.verbosity,
            proxy_forwarded_address_header=self._get_forwarded_host(args=args),
            proxy_forwarded_port_header=self._get_forwarded_port(args=args),
            proxy_forwarded_proto_header=(
                "X-Forwarded-Proto" if args.proxy_headers else None
            ),
            server_name=args.server_name,
        )
        self.server.run()
