import errno
import os
import re
import socket
import sys

from datetime import datetime
from pathlib import Path

from django.conf import settings
from django.contrib.staticfiles.handlers import StaticFilesHandler
from django.core.management.base import BaseCommand, CommandError
from django.core.servers.basehttp import WSGIServer, get_internal_wsgi_application
from django.utils import autoreload
from django.utils.regex_helper import _lazy_re_compile
from werkzeug import run_simple
from werkzeug.serving import WSGIRequestHandler, make_ssl_devcert

from PtpUploader import Ptp
from PtpUploader.Job import Supervisor
from PtpUploader.MyGlobals import MyGlobals
from PtpUploader.Settings import Settings, config


naiveip_re = _lazy_re_compile(
    r"""^(?:
(?P<addr>
    (?P<ipv4>\d{1,3}(?:\.\d{1,3}){3}) |         # IPv4 address
    (?P<ipv6>\[[a-fA-F0-9:]+\]) |               # IPv6 address
    (?P<fqdn>[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN
):)?(?P<port>\d+)$""",
    re.X,
)


class Command(BaseCommand):
    help = "Starts a lightweight Web server for development."

    # Validation is called explicitly each time the server is reloaded.
    requires_system_checks = []
    stealth_options = ("shutdown_message",)

    default_addr = "127.0.0.1"
    default_addr_ipv6 = "::1"
    default_port = "8000"
    protocol = "http"
    server_cls = WSGIServer

    def add_arguments(self, parser):
        parser.add_argument(
            "addrport", nargs="?", help="Optional port number, or ipaddr:port"
        )
        parser.add_argument(
            "--ipv6",
            "-6",
            action="store_true",
            dest="use_ipv6",
            help="Tells Django to use an IPv6 address.",
        )
        parser.add_argument(
            "--nothreading",
            action="store_false",
            dest="use_threading",
            help="Tells Django to NOT use threading.",
        )
        parser.add_argument(
            "--reload",
            action="store_true",
            dest="use_reloader",
            help="Tells Django to use the auto-reloader.",
        )
        parser.add_argument(
            "--nostatic",
            action="store_false",
            dest="use_static_handler",
            help="Tells Django to NOT automatically serve static files at STATIC_URL.",
        )
        parser.add_argument(
            "--insecure",
            action="store_true",
            dest="insecure_serving",
            help="Allows serving static files even if DEBUG is False.",
        )

    def execute(self, *args, **options):
        if options["no_color"]:
            # We rely on the environment because it's currently the only
            # way to reach WSGIRequestHandler. This seems an acceptable
            # compromise considering `runserver` runs indefinitely.
            os.environ["DJANGO_COLORS"] = "nocolor"
        super().execute(*args, **options)

    def get_handler(self, *args, **options):
        """Return the default WSGI handler for the runner."""
        return StaticFilesHandler(get_internal_wsgi_application())

    def initialize(self, options):
        from django.core.management import call_command
        from django.core.management.commands import migrate
        from django.db import DEFAULT_DB_ALIAS

        from PtpUploader.Job.JobRunningState import JobRunningState
        from PtpUploader.MyGlobals import MyGlobals
        from PtpUploader.ReleaseInfo import ReleaseInfo
        from PtpUploader.Source.SourceFactory import SourceFactory

        Settings.LoadSettings()

        call_command("migrate")

        MyGlobals.InitializeGlobals(Settings.WorkingPath)
        MyGlobals.SourceFactory = SourceFactory()
        MyGlobals.Logger.info("Initializing database...")

        # Reset any possibling interrupted jobs
        for releaseInfo in ReleaseInfo.objects.filter(
            JobRunningState__in=[
                JobRunningState.WaitingForStart,
                JobRunningState.InProgress,
            ]
        ):
            releaseInfo.JobRunningState = JobRunningState.Paused
            releaseInfo.save()

        Settings.VerifyPaths()

    def handle(self, *args, **options):
        self.initialize(options)
        if not settings.DEBUG and not settings.ALLOWED_HOSTS:
            raise CommandError("You must set settings.ALLOWED_HOSTS if DEBUG is False.")

        self.use_ipv6 = options["use_ipv6"]
        if self.use_ipv6 and not socket.has_ipv6:
            raise CommandError("Your Python does not support IPv6.")
        self._raw_ipv6 = False
        if not options["addrport"] and config.web.address:
            options["addrport"] = config.web.address
        if not options["addrport"]:
            self.addr = ""
            self.port = self.default_port
        else:
            m = re.match(naiveip_re, options["addrport"])
            if m is None:
                raise CommandError(
                    '"%s" is not a valid port number '
                    "or address:port pair." % options["addrport"]
                )
            self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
            if not self.port.isdigit():
                raise CommandError("%r is not a valid port number." % self.port)
            if self.addr:
                if _ipv6:
                    self.addr = self.addr[1:-1]
                    self.use_ipv6 = True
                    self._raw_ipv6 = True
                elif self.use_ipv6 and not _fqdn:
                    raise CommandError('"%s" is not a valid IPv6 address.' % self.addr)
        if not self.addr:
            self.addr = self.default_addr_ipv6 if self.use_ipv6 else self.default_addr
            self._raw_ipv6 = self.use_ipv6
        self.run(**options)

    def run(self, **options):
        """Run the server, using the autoreloader if needed."""
        use_reloader = options["use_reloader"]

        if use_reloader:
            autoreload.run_with_reloader(self.inner_run, **options)
        else:
            Ptp.Login()
            MyGlobals.PtpUploader = Supervisor.JobSupervisor()
            MyGlobals.PtpUploader.start()
            self.inner_run(None, **options)

    def ssl_context(self):
        if config.web.ssl.enabled:
            if config.web.ssl.cert and config.web.ssl.key:
                return (config.web.ssl.cert, config.web.ssl.key)
            else:
                return make_ssl_devcert(
                    Path("~/.config/ptpuploader/ssl/").expanduser(), host="localhost"
                )
        else:
            return None

    def inner_run(self, *args, **options):
        # If an exception was silenced in ManagementUtility.execute in order
        # to be raised in the child process, raise it now.
        autoreload.raise_last_exception()

        threading = options["use_threading"]
        # 'shutdown_message' is a stealth option.
        shutdown_message = options.get("shutdown_message", "")
        quit_command = "CTRL-BREAK" if sys.platform == "win32" else "CONTROL-C"

        self.stdout.write("Performing system checks...\n\n")
        self.check(display_num_errors=True)
        # Need to check migrations here, so can't use the
        # requires_migrations_check attribute.
        self.check_migrations()
        now = datetime.now().strftime("%B %d, %Y - %X")
        self.stdout.write(now)
        self.stdout.write(
            (
                "Django version %(version)s, using settings %(settings)r\n"
                "Starting development server at %(protocol)s://%(addr)s:%(port)s/\n"
                "Quit the server with %(quit_command)s."
            )
            % {
                "version": self.get_version(),
                "settings": settings.SETTINGS_MODULE,
                "protocol": self.protocol,
                "addr": "[%s]" % self.addr if self._raw_ipv6 else self.addr,
                "port": self.port,
                "quit_command": quit_command,
            }
        )

        try:
            handler = self.get_handler(*args, **options)
            run_simple(
                self.addr,
                int(self.port),
                handler,
                threaded=threading,
                request_handler=WSGIRequestHandler,
                ssl_context=self.ssl_context(),
            )
        except OSError as e:
            # Use helpful error messages instead of ugly tracebacks.
            ERRORS = {
                errno.EACCES: "You don't have permission to access that port.",
                errno.EADDRINUSE: "That port is already in use.",
                errno.EADDRNOTAVAIL: "That IP address can't be assigned to.",
            }
            try:
                error_text = ERRORS[e.errno]
            except KeyError:
                error_text = e
            self.stderr.write("Error: %s" % error_text)
            # Need to use an OS exit because sys.exit doesn't work in a thread
            os._exit(1)
        except KeyboardInterrupt:
            if shutdown_message:
                self.stdout.write(shutdown_message)
            sys.exit(0)
