"""Management command to auto-generate Django apps with full CRUD boilerplate.

Generates models, views, URLs, forms, admin, templates, static files,
and management commands from Jinja2 templates.
"""

from __future__ import annotations

import importlib.resources
import logging
import re
from pathlib import Path
from typing import Any

from django.core.management.base import BaseCommand, CommandError
from django.utils.crypto import get_random_string
from jinja2 import Environment, FileSystemLoader, TemplateNotFound, StrictUndefined

logger: logging.Logger = logging.getLogger(__name__)

__all__: list[str] = ["Command"]


class Command(BaseCommand):
    """Auto-generate a Django app with boilerplate CRUD code.

    Creates a complete Django app including models, views, URLs, forms,
    admin configuration, HTML templates, and static files from Jinja2
    templates shipped with this package.

    Usage:
        python manage.py autoapp <app_name> <model_name> [--dry-run] [--force]
    """

    help: str = (
        "Auto-generates Django app with boilerplate code including models, views, urls, "
        "forms, admin, templates, static files, and management commands."
    )

    # Configuration constants
    DEFAULT_TEMPLATE_DIR: str = "codegen_templates"
    APP_NAME_REGEX: re.Pattern[str] = re.compile(r"^[a-z][a-z0-9_]*$")
    MODEL_NAME_REGEX: re.Pattern[str] = re.compile(r"^[A-Z][a-zA-Z0-9]*$")

    def add_arguments(self, parser: Any) -> None:
        """Register CLI arguments for the autoapp command.

        Args:
            parser: The ``argparse.ArgumentParser`` instance provided by Django.
        """
        parser.add_argument("app_name", type=str, help="Name of the new Django app")
        parser.add_argument("model_name", type=str, help="Name of the primary model")
        parser.add_argument(
            "--template-dir",
            type=str,
            default=self.DEFAULT_TEMPLATE_DIR,
            help="Custom template directory path",
        )
        parser.add_argument(
            "--dry-run",
            action="store_true",
            help="Simulate execution without writing files",
        )
        parser.add_argument(
            "--force", action="store_true", help="Overwrite existing files"
        )

    def validate_inputs(self, app_name: str, model_name: str) -> None:
        """Validate application and model naming conventions.

        Args:
            app_name: The Django app name to validate.
            model_name: The Django model name to validate.

        Raises:
            CommandError: If either name fails validation.
        """
        if not self.APP_NAME_REGEX.match(app_name):
            raise CommandError(
                f"Invalid app name '{app_name}'. Must be lowercase with letters, numbers, and underscores."
            )
        if not self.MODEL_NAME_REGEX.match(model_name):
            raise CommandError(
                f"Invalid model name '{model_name}'. Must start with uppercase letter and be alphanumeric."
            )

    def create_directory(self, path: Path) -> None:
        """Safely create a directory with conflict handling.

        Args:
            path: The directory path to create.

        Raises:
            CommandError: If the directory cannot be created.
        """
        if path.exists():
            self._skipped_items.add(str(path))
            logger.warning("Directory exists: %s", path)
            return
        try:
            if not self._dry_run:
                path.mkdir(parents=True, exist_ok=True)
            logger.info("Created directory: %s", path)
        except OSError as e:
            raise CommandError(f"Error creating directory {path}: {e}") from e

    def write_file(self, path: Path, content: str) -> None:
        """Write content to a file with conflict handling.

        Args:
            path: The target file path.
            content: The string content to write.

        Raises:
            CommandError: If the file cannot be written.
        """
        if path.exists() and not self._force:
            self._skipped_items.add(str(path))
            logger.warning("File exists: %s", path)
            return
        try:
            if not self._dry_run:
                path.parent.mkdir(parents=True, exist_ok=True)
                path.write_text(content, encoding="utf-8")
            logger.info("Generated: %s", path)
        except OSError as e:
            raise CommandError(f"Error writing file {path}: {e}") from e

    def get_jinja_environment(
        self, search_paths: list[str], html: bool = False
    ) -> Environment:
        """Create a configured Jinja2 environment with security settings.

        When ``html=True``, uses alternate delimiters (``<% %>`` / ``<< >>``)
        to avoid conflicts with Django's own template syntax.

        Args:
            search_paths: Directories to search for templates.
            html: Whether to configure for HTML template generation.

        Returns:
            A configured ``jinja2.Environment`` instance.
        """
        return Environment(
            loader=FileSystemLoader(search_paths),
            undefined=StrictUndefined,
            autoescape=html,
            block_start_string="<%" if html else "{%",
            block_end_string="%>" if html else "%}",
            variable_start_string="<<" if html else "{{",
            variable_end_string=">>" if html else "}}",
            comment_start_string="<#" if html else "{#",
            comment_end_string="#>" if html else "#}",
            lstrip_blocks=True,
            trim_blocks=True,
        )

    def render_templates(
        self,
        env: Environment,
        templates: dict[str, Path],
        context: dict[str, Any],
    ) -> None:
        """Batch-render Jinja2 templates to their output paths.

        Args:
            env: The Jinja2 environment to use for rendering.
            templates: Mapping of template filenames to output paths.
            context: The template context variables.

        Raises:
            CommandError: If a template fails to render.
        """
        for template_name, output_path in templates.items():
            try:
                template = env.get_template(template_name)
                content = template.render(context)
                self.write_file(output_path, content)
            except TemplateNotFound:
                logger.warning("Template not found: %s", template_name)
            except Exception as e:
                raise CommandError(f"Error rendering {template_name}: {e}") from e

    def get_templates_dir(self, options: dict[str, Any]) -> Path:
        """Resolve the templates directory path.

        Priority:
            1. Custom ``--template-dir`` if provided and exists on disk.
            2. Bundled templates from the installed ``django_autoapp`` package.

        Args:
            options: Parsed command-line options from Django.

        Returns:
            Resolved ``Path`` to the templates directory.

        Raises:
            CommandError: If templates cannot be located.
        """
        custom_dir: str | None = options.get("template_dir")

        # If user supplied a custom dir that actually exists, use it
        if custom_dir and custom_dir != self.DEFAULT_TEMPLATE_DIR:
            custom_path = Path(custom_dir)
            if custom_path.exists():
                return custom_path

        # Resolve from installed package using importlib.resources
        try:
            templates_ref = importlib.resources.files("django_autoapp").joinpath(
                f"templates/{self.DEFAULT_TEMPLATE_DIR}"
            )
            # For on-disk packages (editable installs / sdists), we can
            # resolve to an actual filesystem path directly.
            templates_path = Path(str(templates_ref))
            if templates_path.is_dir():
                return templates_path
        except (TypeError, FileNotFoundError):
            pass

        raise CommandError(
            "Templates directory not found in installed package. "
            "Please provide --template-dir option."
        )

    def handle(self, *args: Any, **options: Any) -> None:
        """Execute the autoapp code-generation command.

        Args:
            *args: Positional arguments (unused).
            **options: Parsed command-line options.
        """
        # Initialize instance state (not class-level to avoid shared state)
        self._dry_run: bool = options["dry_run"]
        self._force: bool = options["force"]
        self._skipped_items: set[str] = set()

        app_name: str = options["app_name"].strip()
        model_name: str = options["model_name"].strip()

        # Validate inputs
        self.validate_inputs(app_name, model_name)

        # Get the templates directory
        templates_dir: Path = self.get_templates_dir(options)
        if not templates_dir.exists():
            raise CommandError(f"Template directory does not exist: {templates_dir}")

        # Initialize paths
        project_root: Path = Path.cwd()
        app_path: Path = project_root / app_name

        # Check existing application
        if app_path.exists() and not self._force:
            raise CommandError(
                f"Application '{app_name}' already exists. Use --force to overwrite."
            )

        # Create directory structure
        dirs_to_create: list[Path] = [
            app_path,
            app_path / "migrations",
            app_path / "templates" / app_name,
            app_path / "management" / "commands",
        ]
        for directory in dirs_to_create:
            self.create_directory(directory)

        # Create __init__.py files
        init_files: list[Path] = [
            app_path / "__init__.py",
            app_path / "migrations" / "__init__.py",
            app_path / "management" / "__init__.py",
            app_path / "management" / "commands" / "__init__.py",
        ]
        for init_file in init_files:
            self.write_file(init_file, "")

        # Setup template context with precomputed URL names and a secret key
        context: dict[str, Any] = {
            "app_name": app_name,
            "model_name": model_name,
            "secret_key": get_random_string(50),
            "urls": {
                "create": f"{app_name}:{model_name.lower()}_create",
                "update": f"{app_name}:{model_name.lower()}_update",
                "delete": f"{app_name}:{model_name.lower()}_delete",
                "list": f"{app_name}:{model_name.lower()}_list",
            },
        }

        # Generate Python files using the package templates
        python_env: Environment = self.get_jinja_environment([str(templates_dir)])
        python_templates: dict[str, Path] = {
            "models.j2": app_path / "models.py",
            "views.j2": app_path / "views.py",
            "urls.j2": app_path / "urls.py",
            "forms.j2": app_path / "forms.py",
            "admin.j2": app_path / "admin.py",
            "run_project.j2": app_path / "management" / "commands" / "run_project.py",
            "apps.j2": app_path / "apps.py",
        }
        self.render_templates(python_env, python_templates, context)

        # Generate HTML templates
        html_env: Environment = self.get_jinja_environment(
            [str(project_root / "templates"), str(templates_dir)], html=True
        )
        html_templates: dict[str, Path] = {
            "list_template.j2": app_path
            / "templates"
            / app_name
            / f"{model_name.lower()}_list.html",
            "form_template.j2": app_path
            / "templates"
            / app_name
            / f"{model_name.lower()}_form.html",
            "confirm_delete_template.j2": app_path
            / "templates"
            / app_name
            / f"{model_name.lower()}_confirm_delete.html",
            "detail_template.j2": app_path
            / "templates"
            / app_name
            / f"{model_name.lower()}_detail.html",
            "project_base_template.j2": project_root / "templates" / "base.html",
            "project_nav_template.j2": project_root / "templates" / "navbar.html",
        }
        self.render_templates(html_env, html_templates, context)

        # Generate static files
        static_files: dict[Path, str] = {
            project_root
            / "static/css/style.css": "/* Basic CSS */\nbody { font-family: Arial, sans-serif; }\n",
            project_root
            / "static/js/script.js": "// Basic JS\nconsole.log('Hello from script.js');\n",
        }
        for path, content in static_files.items():
            self.write_file(path, content)

        # Final output
        self.stdout.write(
            self.style.SUCCESS(f"\nSuccessfully created app '{app_name}'")
        )
        if self._skipped_items:
            self.stdout.write(self.style.WARNING("\nSkipped items:"))
            for item in sorted(self._skipped_items):
                self.stdout.write(f"  - {item}")
        if self._dry_run:
            self.stdout.write(
                self.style.WARNING("\nDRY RUN: No files were actually written")
            )
