"""
File Generation System for Django GraphQL Auto-Generation

This module provides the FileGenerator class, which is responsible for generating
and managing GraphQL schema files for Django apps and models.
"""

import ast
import logging
import os
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Set, Type, Union

import black
from django.apps import apps
from django.db import models
from django.template.loader import render_to_string

from ..core.error_handling import get_error_handler
from ..core.performance import get_query_optimizer
from ..core.security import get_authz_manager
from ..core.settings import SchemaSettings
from .introspector import ModelIntrospector
from .mutations import MutationGenerator
from .queries import QueryGenerator
from .types import TypeGenerator

logger = logging.getLogger(__name__)


class FileGenerator:
    """
    Generates and manages GraphQL schema files for Django apps and models.
    Handles file creation, updates, and versioning.

    This class supports:
    - Multi-schema file generation
    - Code formatting and validation
    - Performance optimization integration
    - Security and authorization integration
    - Error handling integration
    """

    def __init__(self, settings: Optional[SchemaSettings] = None, schema_name: str = "default"):
        """
        Initialize the FileGenerator.

        Args:
            settings: Schema settings or None for defaults
            schema_name: Name of the schema for multi-schema support
        """
        self.schema_name = schema_name

        # Use hierarchical settings if no explicit settings provided
        if settings is None:
            self.settings = SchemaSettings.from_schema(schema_name)
        else:
            self.settings = settings

        # Initialize components with schema-specific settings
        self.type_generator = TypeGenerator(schema_name=schema_name)
        self.query_generator = QueryGenerator(self.type_generator, schema_name=schema_name)
        self.mutation_generator = MutationGenerator(self.type_generator, schema_name=schema_name)

        # Initialize performance and security components
        self.query_optimizer = get_query_optimizer(schema_name)
        self.authorization_manager = get_authz_manager(schema_name)
        self.error_handler = get_error_handler(schema_name)

    def _should_include_model(self, model: Type[models.Model]) -> bool:
        """
        Check if a model should be included based on settings.

        Args:
            model: Django model to check

        Returns:
            bool: True if model should be included
        """
        # Check excluded models
        if hasattr(self.settings, 'excluded_models') and self.settings.excluded_models:
            if model.__name__ in self.settings.excluded_models:
                return False

        # Check excluded apps
        if hasattr(self.settings, 'excluded_apps') and self.settings.excluded_apps:
            if model._meta.app_label in self.settings.excluded_apps:
                return False

        return True

    def _should_enable_filtering(self) -> bool:
        """
        Check if filtering should be enabled based on settings.

        Returns:
            bool: True if filtering should be enabled
        """
        return getattr(self.settings, 'enable_filtering', True)

    def _should_enable_ordering(self) -> bool:
        """
        Check if ordering should be enabled based on settings.

        Returns:
            bool: True if ordering should be enabled
        """
        return getattr(self.settings, 'enable_ordering', True)

    def _ensure_directory(self, directory: Union[str, Path]) -> None:
        """
        Ensures that a directory exists, creating it if necessary.
        """
        Path(directory).mkdir(parents=True, exist_ok=True)

    def _format_python_code(self, code: str) -> str:
        """
        Formats Python code using black.
        """
        try:
            return black.format_str(code, mode=black.FileMode())
        except Exception as e:
            logger.warning(f"Failed to format code: {str(e)}")
            return code

    def _generate_header(self, app_name: str) -> str:
        """
        Generates a file header with metadata and timestamp.
        """
        return f'''"""
GraphQL Schema for {app_name}

This file was auto-generated by django-graphql-auto.
Generated at: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

WARNING: Do not edit this file directly.
Any changes made to this file will be overwritten when the schema is regenerated.
"""

import graphene
from graphene_django import DjangoObjectType
from django.db import models
from django_filters import FilterSet, OrderingFilter

'''

    def generate_types_file(self, app_config, models: List[Type[models.Model]], output_dir: Union[str, Path]) -> Path:
        """
        Generates a types.py file containing GraphQL type definitions for models.
        """
        output_dir = Path(output_dir)
        self._ensure_directory(output_dir)
        types_file = output_dir / 'types.py'

        code = [self._generate_header(app_config.name)]

        # Add imports for related models from other apps
        related_models = set()
        for model in models:
            introspector = ModelIntrospector(model)
            for relation in introspector.get_relationships():
                if relation.related_model._meta.app_label != app_config.name:
                    related_models.add(relation.related_model)

        if related_models:
            code.append("\n# Related model imports")
            for related_model in sorted(related_models, key=lambda m: m.__name__):
                app_label = related_model._meta.app_label
                code.append(f"from {app_label}.models import {related_model.__name__}")

        # Generate type definitions
        for model in sorted(models, key=lambda m: m.__name__):
            code.append(f"\n\nclass {model.__name__}Type(DjangoObjectType):")
            code.append("    class Meta:")
            code.append(f"        model = {model.__name__}")

            # Add filter fields if enabled
            if self.settings.enable_filtering:
                filter_fields = self.type_generator.get_filterable_fields(model)
                if filter_fields:
                    fields_str = ", ".join(f"'{field}'" for field in filter_fields)
                    code.append(f"        filter_fields = [{fields_str}]")

            # Add interfaces if any
            interfaces = self.type_generator.get_interfaces(model)
            if interfaces:
                interfaces_str = ", ".join(interface.__name__ for interface in interfaces)
                code.append(f"        interfaces = ({interfaces_str},)")

        # Write the file
        types_file.write_text(self._format_python_code("\n".join(code)))
        logger.info(f"Generated types file: {types_file}")
        return types_file

    def generate_queries_file(self, app_config, models: List[Type[models.Model]], output_dir: Union[str, Path]) -> Path:
        """
        Generates a queries.py file containing GraphQL query definitions.
        """
        output_dir = Path(output_dir)
        self._ensure_directory(output_dir)
        queries_file = output_dir / 'queries.py'

        code = [self._generate_header(app_config.name)]
        code.append("from .types import *")

        # Generate Query class
        code.append("\n\nclass Query(graphene.ObjectType):")

        for model in sorted(models, key=lambda m: m.__name__):
            model_name = model.__name__
            type_name = f"{model_name}Type"

            # Single object query
            code.append(f"\n    {model_name.lower()} = graphene.Field(")
            code.append(f"        {type_name},")
            code.append("        id=graphene.ID(required=True),")
            code.append("        description=f'Get a specific {model_name} by ID'")
            code.append("    )")

            # List query
            code.append(f"\n    {model_name.lower()}s = graphene.List(")
            code.append(f"        {type_name},")
            if self.settings.enable_filtering:
                code.append("        filter=graphene.String(),")
            if self.settings.enable_ordering:
                code.append("        order_by=graphene.String(),")
            code.append(f"        description=f'Get a list of {model_name} objects'")
            code.append("    )")

            # Add resolvers
            code.append(f"\n    def resolve_{model_name.lower()}(self, info, id):")
            code.append(f"        return {model_name}.objects.get(pk=id)")

            code.append(f"\n    def resolve_{model_name.lower()}s(self, info, **kwargs):")
            code.append(f"        queryset = {model_name}.objects.all()")
            if self.settings.enable_filtering:
                code.append("        filter_param = kwargs.get('filter')")
                code.append("        if filter_param:")
                code.append("            queryset = queryset.filter(filter_param)")
            if self.settings.enable_ordering:
                code.append("        order_by = kwargs.get('order_by')")
                code.append("        if order_by:")
                code.append("            queryset = queryset.order_by(order_by)")
            code.append("        return queryset")

        # Write the file
        queries_file.write_text(self._format_python_code("\n".join(code)))
        logger.info(f"Generated queries file: {queries_file}")
        return queries_file

    def generate_mutations_file(self, app_config, models: List[Type[models.Model]], output_dir: Union[str, Path]) -> Path:
        """
        Generates a mutations.py file containing GraphQL mutation definitions.
        """
        output_dir = Path(output_dir)
        self._ensure_directory(output_dir)
        mutations_file = output_dir / 'mutations.py'

        code = [self._generate_header(app_config.name)]
        code.append("from .types import *")

        # Generate mutations for each model
        for model in sorted(models, key=lambda m: m.__name__):
            model_name = model.__name__

            # Create mutation
            code.append(f"\n\nclass Create{model_name}(graphene.Mutation):")
            code.append("    class Arguments:")
            for field in model._meta.fields:
                if not field.primary_key:
                    field_type = self.type_generator.FIELD_TYPE_MAP.get(
                        type(field), "graphene.String"
                    )
                    code.append(f"        {field.name} = {field_type}(required=True)")

            code.append(f"\n    {model_name.lower()} = graphene.Field({model_name}Type)")

            code.append("\n    @classmethod")
            code.append("    def mutate(cls, root, info, **kwargs):")
            code.append(f"        instance = {model_name}.objects.create(**kwargs)")
            code.append("        return cls(" + f"{model_name.lower()}=instance)")

            # Update mutation
            code.append(f"\n\nclass Update{model_name}(graphene.Mutation):")
            code.append("    class Arguments:")
            code.append("        id = graphene.ID(required=True)")
            for field in model._meta.fields:
                if not field.primary_key:
                    field_type = self.type_generator.FIELD_TYPE_MAP.get(
                        type(field), "graphene.String"
                    )
                    code.append(f"        {field.name} = {field_type}()")

            code.append(f"\n    {model_name.lower()} = graphene.Field({model_name}Type)")

            code.append("\n    @classmethod")
            code.append("    def mutate(cls, root, info, id, **kwargs):")
            code.append(f"        instance = {model_name}.objects.get(pk=id)")
            code.append("        for key, value in kwargs.items():")
            code.append("            setattr(instance, key, value)")
            code.append("        instance.save()")
            code.append("        return cls(" + f"{model_name.lower()}=instance)")

            # Delete mutation
            code.append(f"\n\nclass Delete{model_name}(graphene.Mutation):")
            code.append("    class Arguments:")
            code.append("        id = graphene.ID(required=True)")

            code.append("\n    success = graphene.Boolean()")

            code.append("\n    @classmethod")
            code.append("    def mutate(cls, root, info, id):")
            code.append(f"        instance = {model_name}.objects.get(pk=id)")
            code.append("        instance.delete()")
            code.append("        return cls(success=True)")

        # Generate Mutation class
        code.append("\n\nclass Mutation(graphene.ObjectType):")
        for model in sorted(models, key=lambda m: m.__name__):
            model_name = model.__name__
            code.append(f"    create_{model_name.lower()} = Create{model_name}.Field()")
            code.append(f"    update_{model_name.lower()} = Update{model_name}.Field()")
            code.append(f"    delete_{model_name.lower()} = Delete{model_name}.Field()")

        # Write the file
        mutations_file.write_text(self._format_python_code("\n".join(code)))
        logger.info(f"Generated mutations file: {mutations_file}")
        return mutations_file

    def generate_schema_file(self, app_config, output_dir: Union[str, Path]) -> Path:
        """
        Generates a schema.py file that combines types, queries, and mutations.
        """
        output_dir = Path(output_dir)
        self._ensure_directory(output_dir)
        schema_file = output_dir / 'schema.py'

        code = [self._generate_header(app_config.name)]
        code.append("from .types import *")
        code.append("from .queries import Query")
        code.append("from .mutations import Mutation")

        code.append("\n\nschema = graphene.Schema(")
        code.append("    query=Query,")
        code.append("    mutation=Mutation,")
        code.append("    auto_camelcase=True")
        code.append(")")

        # Write the file
        schema_file.write_text(self._format_python_code("\n".join(code)))
        logger.info(f"Generated schema file: {schema_file}")
        return schema_file

    def generate_app_schema(self, app_label: str, output_dir: Optional[Union[str, Path]] = None) -> Dict[str, Path]:
        """
        Generates all schema files for a Django app.
        """
        try:
            app_config = apps.get_app_config(app_label)
            if not output_dir:
                output_dir = Path(app_config.path) / 'schema'

            # Get all models for the app
            models = [
                model for model in app_config.get_models()
                if not model._meta.abstract and model.__name__ not in self.settings.excluded_models
            ]

            if not models:
                logger.warning(f"No models found in app '{app_label}'")
                return {}

            # Generate all schema files
            generated_files = {
                'types': self.generate_types_file(app_config, models, output_dir),
                'queries': self.generate_queries_file(app_config, models, output_dir),
                'mutations': self.generate_mutations_file(app_config, models, output_dir),
                'schema': self.generate_schema_file(app_config, output_dir)
            }

            logger.info(f"Generated schema files for app '{app_label}' in {output_dir}")
            return generated_files

        except Exception as e:
            logger.error(
                f"Failed to generate schema for app '{app_label}': {str(e)}", exc_info=True)
            raise

    def generate_all_schemas(self, output_base_dir: Optional[Union[str, Path]] = None) -> Dict[str, Dict[str, Path]]:
        """
        Generates schema files for all registered Django apps.
        """
        generated_files = {}

        for app_config in apps.get_app_configs():
            if app_config.name not in self.settings.excluded_apps:
                app_output_dir = Path(output_base_dir) / \
                    app_config.name if output_base_dir else None
                try:
                    app_files = self.generate_app_schema(app_config.name, app_output_dir)
                    if app_files:
                        generated_files[app_config.name] = app_files
                except Exception as e:
                    logger.error(f"Failed to generate schema for app '{app_config.name}': {str(e)}")

        return generated_files

    def update_app_schema(self, app_label: str, output_dir: Optional[Union[str, Path]] = None) -> Dict[str, Path]:
        """
        Updates schema files for a specific Django app.
        """
        try:
            app_config = apps.get_app_config(app_label)
            if not output_dir:
                output_dir = Path(app_config.path) / 'schema'

            if not output_dir.exists():
                return self.generate_app_schema(app_label, output_dir)

            # Get all models for the app
            models = [
                model for model in app_config.get_models()
                if not model._meta.abstract and model.__name__ not in self.settings.excluded_models
            ]

            if not models:
                logger.warning(f"No models found in app '{app_label}'")
                return {}

            # Update each schema file
            updated_files = {}

            # Update types.py
            types_file = output_dir / 'types.py'
            if types_file.exists():
                updated_files['types'] = self.generate_types_file(app_config, models, output_dir)

            # Update queries.py
            queries_file = output_dir / 'queries.py'
            if queries_file.exists():
                updated_files['queries'] = self.generate_queries_file(
                    app_config, models, output_dir)

            # Update mutations.py
            mutations_file = output_dir / 'mutations.py'
            if mutations_file.exists():
                updated_files['mutations'] = self.generate_mutations_file(
                    app_config, models, output_dir)

            # Update schema.py
            schema_file = output_dir / 'schema.py'
            if schema_file.exists():
                updated_files['schema'] = self.generate_schema_file(app_config, output_dir)

            logger.info(f"Updated schema files for app '{app_label}' in {output_dir}")
            return updated_files

        except Exception as e:
            logger.error(f"Failed to update schema for app '{app_label}': {str(e)}", exc_info=True)
            raise
