import os
import re
import logging
import pkg_resources
from pathlib import Path
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.getLogger(__name__)


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

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

    def add_arguments(self, parser):
        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."""
        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 directory with conflict handling."""
        if path.exists():
            self.SKIPPED_ITEMS.add(str(path))
            logger.warning(f"Directory exists: {path}")
            return
        try:
            if not self.dry_run:
                path.mkdir(parents=True, exist_ok=True)
            logger.info(f"Created directory: {path}")
        except OSError as e:
            raise CommandError(f"Error creating directory {path}: {e}") from e

    def write_file(self, path: Path, content: str) -> None:
        """Safe file writing with conflict handling."""
        if path.exists() and not self.force:
            self.SKIPPED_ITEMS.add(str(path))
            logger.warning(f"File exists: {path}")
            return
        try:
            if not self.dry_run:
                path.parent.mkdir(parents=True, exist_ok=True)
                path.write_text(content)
            logger.info(f"Generated: {path}")
        except OSError as e:
            raise CommandError(f"Error writing file {path}: {e}") from e

    def get_jinja_environment(
        self, search_paths: list, html: bool = False
    ) -> Environment:
        """Create configured Jinja environment with security settings."""
        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, context: dict
    ) -> None:
        """Batch process templates with error handling."""
        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(f"Template not found: {template_name}")
            except Exception as e:
                raise CommandError(f"Error rendering {template_name}: {e}") from e

    def get_templates_dir(self, options) -> str:
        """
        Return the template directory:
          - If --template-dir is provided and exists, return it.
          - Otherwise, load templates from the installed package.
        """
        custom_dir = options.get("template_dir")
        if custom_dir and os.path.exists(custom_dir):
            return custom_dir
        try:
            # Loads the 'templates/codegen_templates' folder from your installed package.
            return pkg_resources.resource_filename(
                "django_autoapp", "templates/codegen_templates"
            )
        except Exception as e:
            raise CommandError(
                "Templates directory not found in installed package. "
                "Please provide --template-dir option."
            ) from e

    def handle(self, *args, **options):
        # Initialize parameters
        self.dry_run = options["dry_run"]
        self.force = options["force"]
        app_name = options["app_name"].strip()
        model_name = options["model_name"].strip()

        # Validate inputs
        self.validate_inputs(app_name, model_name)

        # Get the templates directory (use pkg_resources if no custom directory provided)
        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.cwd()
        app_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 = [
            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 = [
            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 = {
            "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 = self.get_jinja_environment([str(templates_dir)])
        python_templates = {
            "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 = self.get_jinja_environment(
            [str(project_root / "templates"), str(templates_dir)], html=True
        )
        html_templates = {
            "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 = {
            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")
            )
