#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""

import os
import sys
from collections.abc import Sequence
from typing import cast

import django
import docker
import docker.errors
from decouple import UndefinedValueError
from django.contrib.auth import get_user_model
from django.core.management import call_command, execute_from_command_line
from docker.models.containers import Container

from vuln_demo.util.config import get_config
from vuln_demo.util.log import get_logger

logger = get_logger(__name__)


class SiteManager:
    """
    Manages the preparation and serving of the website.

    Handles both development and production environments.

    Usage:
        manage.py run [option]...

    Options:
        --debug     Runs a development server with debug mode enabled.
        --silent    Sets minimal console output.
        --verbose   Sets verbose console output.
    """

    def __init__(self, args: Sequence[str]):
        self.debug = "--debug" in args
        self.silent = "--silent" in args

        if self.silent:
            self.verbosity = 0
        else:
            self.verbosity = 2 if "--verbose" in args else 1

        if self.debug:
            os.environ.setdefault("DEBUG", "1")
            logger.info("Running in debug mode")

    def prepare_environment(self) -> None:
        """Perform common preparation tasks."""
        django.setup()

        logger.info("Applying migrations")
        call_command("migrate", verbosity=self.verbosity)

    @staticmethod
    def create_superuser() -> None:
        """Create a default django admin super user in development environments."""
        logger.info("Creating a superuser")

        try:
            name = get_config("SUPER_USERNAME")
            password = get_config("SUPER_PASSWORD")
        except UndefinedValueError:
            logger.info("Skipping automatic superuser creation (no credentials provided)")
            return
        user = get_user_model()

        # Get or create admin superuser.
        if user.objects.filter(username=name).exists():
            user = user.objects.get(username=name)
            logger.info("Admin superuser already exists.")
        else:
            user = user.objects.create_superuser(name, "", password)  # pyright: ignore[reportAttributeAccessIssue]
            logger.info("Admin superuser created")

    def prepare_server(self) -> None:
        """Perform runserver-specific preparation tasks."""
        if self.debug:
            # In production, collectstatic is ran in the Docker image
            logger.info("Collecting static files")
            call_command(
                "collectstatic",
                interactive=False,
                clear=True,
                verbosity=self.verbosity - 1,
            )

            self.create_superuser()

    def docker_cleanup(self) -> None:
        """
        Delete any stale Docker containers.

        Generally, there shouldn't be any left-over containers, however, when running in debug mode
        sometimes the auto-refreshing triggers before our cleanup code, leaving a container behind.
        This can also happen if the server is killed.

        This will find any containers created by us, stop them, and remove them.
        """
        logger.info("Deleting stale Docker containers")
        docker_client = docker.client.from_env()
        containers = docker_client.containers.list(filters={"name": "vuln_demo_"})
        for container in containers:
            container = cast(Container, container)
            logger.info(f"Stopping and removing container {container.name!r}")
            container.stop(timeout=2)  # wait at most 2 seconds, then SIGKILL
            try:
                container.remove(v=True)  # remove the container, and its subvolumes
            except docker.errors.APIError as exc:
                # Ignore removal already in progress, we know it will get removed.
                # (this can happen if the container wad configured with auto_remove=True)
                if not (
                    isinstance(exc.explanation, str)
                    and exc.explanation.startswith("removal of container ")
                    and exc.explanation.endswith(" is already in progress")
                ):
                    raise exc from exc

                container_id = exc.explanation.removeprefix("removal of container ")
                container_id = container_id.removesuffix(" is already in progress")
                logger.debug(f"Container {container_id!r} is already being removed")

        if len(containers) == 0:
            logger.info("No stale containers found")

    def run_server(self) -> None:
        """Prepare and run the web server."""
        # Check if the server is being run by the Django reloader
        # (the reloader sets this variable automatically)
        in_reloader = get_config("RUN_MAIN", cast=bool, default=False)

        # Prevent preparing twice when in dev mode due to reloader
        if not self.debug or in_reloader:
            self.prepare_environment()
            self.prepare_server()
            self.docker_cleanup()

        host = get_config("SERVER_HOST", default="0.0.0.0")  # noqa: S104 # we want to bind to all interfaces by default
        port = get_config("SERVER_PORT", default="8000")

        no_daphne = get_config("NO_DAPHNE", cast=bool, default=False)

        logger.info("Starting server")
        if self.debug or no_daphne:
            call_command("runserver", f"{host}:{port}")
            return

        # Only import daphne manually if we aren't in debug mode
        from daphne.cli import CommandLineInterface

        sys.argv = [
            "daphne",
            "-b",
            host,
            "-p",
            port,
            "vuln_demo.asgi:application",
            "--access-log",
            "-",
            "--verbosity",
            "2",
        ]

        # This sets a special environment variable that will skip some steps in asgi.py
        # as we handle those manually in this function (such as running django.setup()).
        # These tasks are configured in `asgi.py` in case the user runs `daphne` command
        # manually, but since we're running it from here, we can skip these tasks.
        os.environ.setdefault("VULN_DEMO_SKIP_ASGI_SETUP", "1")

        # Run daphne for the production server
        logger.info("Starting daphne server")
        CommandLineInterface.entrypoint()


def main() -> None:
    """Run administrative tasks."""
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vuln_demo.settings")

    # Use the custom site manager for run command
    if len(sys.argv) > 1:
        if sys.argv[1] == "run":
            manager = SiteManager(sys.argv)
            manager.run_server()
        elif sys.argv[1] == "docker_cleanup":
            manager = SiteManager(sys.argv)
            manager.docker_cleanup()
        else:
            execute_from_command_line(sys.argv)

    # Pass any other commands to standard management commands
    else:
        execute_from_command_line(sys.argv)


if __name__ == "__main__":
    main()
