# createsonline/customization/loader.py
"""
Upgrade-Safe Customization Loader

Automatically discovers and loads user customizations from the `custom/`
directory (or any configured directory) at project root. This ensures
framework upgrades via pip never overwrite user code.

Usage in main.py::

    from createsonline import create_app
    from createsonline.customization import CustomizationLoader

    app = create_app()
    
    # Load all user customizations (routes, models, admin, hooks, middleware)
    loader = CustomizationLoader(app)
    loader.load_all()
    
    # Or load selectively:
    loader.load_routes()
    loader.load_models()
    loader.load_admin()
    loader.load_middleware()
    loader.load_hooks()
"""

import os
import sys
import importlib
import importlib.util
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Type


# ============================================================
# Hook Registry — lifecycle hooks users can register
# ============================================================

class HookRegistry:
    """
    Registry for lifecycle hooks. Users register callbacks for events.
    
    Usage in custom/hooks.py::
    
        from createsonline.customization import HookRegistry
        
        hooks = HookRegistry()
        
        @hooks.on('before_request')
        async def log_request(request):
            print(f"Request: {request.method} {request.path}")
        
        @hooks.on('after_response')
        async def add_header(request, response):
            response.headers['X-Custom'] = 'yes'
        
        @hooks.on('app_startup')
        async def on_startup(app):
            print("App started!")
        
        @hooks.on('app_shutdown')
        async def on_shutdown(app):
            print("App shutting down!")
    """
    
    VALID_HOOKS = {
        'app_startup', 'app_shutdown',
        'before_request', 'after_request',
        'before_response', 'after_response',
        'before_db_save', 'after_db_save',
        'before_db_delete', 'after_db_delete',
        'admin_login', 'admin_logout',
        'model_registered', 'route_registered',
        'error_handler', 'template_rendered',
    }
    
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._hooks: Dict[str, List[Callable]] = {}
        return cls._instance
    
    @classmethod
    def reset(cls):
        """Reset singleton (for testing)."""
        cls._instance = None
    
    def on(self, event: str):
        """Decorator to register a hook callback."""
        def decorator(fn):
            if event not in self._hooks:
                self._hooks[event] = []
            self._hooks[event].append(fn)
            return fn
        return decorator
    
    def register(self, event: str, callback: Callable):
        """Register a hook callback."""
        if event not in self._hooks:
            self._hooks[event] = []
        self._hooks[event].append(callback)
    
    async def trigger(self, event: str, *args, **kwargs) -> List[Any]:
        """Trigger all callbacks for an event."""
        results = []
        for callback in self._hooks.get(event, []):
            try:
                import asyncio
                if asyncio.iscoroutinefunction(callback):
                    result = await callback(*args, **kwargs)
                else:
                    result = callback(*args, **kwargs)
                results.append(result)
            except Exception as e:
                print(f"[CREATESONLINE] Hook error ({event}): {e}")
        return results
    
    def trigger_sync(self, event: str, *args, **kwargs) -> List[Any]:
        """Trigger hooks synchronously."""
        results = []
        for callback in self._hooks.get(event, []):
            try:
                import asyncio
                if asyncio.iscoroutinefunction(callback):
                    # Skip async hooks in sync context
                    continue
                result = callback(*args, **kwargs)
                results.append(result)
            except Exception as e:
                print(f"[CREATESONLINE] Hook error ({event}): {e}")
        return results
    
    def get_hooks(self, event: str) -> List[Callable]:
        return self._hooks.get(event, [])
    
    def list_events(self) -> List[str]:
        return list(self._hooks.keys())


# ============================================================
# Plugin Base — for structured user extensions
# ============================================================

class PluginBase:
    """
    Base class for user plugins. Provides a structured way to extend
    the framework that survives upgrades.
    
    Usage in custom/plugins/my_plugin.py::
    
        from createsonline.customization import PluginBase
        
        class AnalyticsPlugin(PluginBase):
            name = "analytics"
            version = "1.0.0"
            
            def setup(self, app):
                # Register routes, hooks, middleware
                @app.route('/analytics')
                async def analytics_view(request):
                    return {"data": "analytics"}
            
            def teardown(self, app):
                pass
    """
    
    name: str = 'unnamed'
    version: str = '0.1.0'
    description: str = ''
    dependencies: List[str] = []
    
    def setup(self, app):
        """Called when plugin is loaded. Register routes, hooks, etc."""
        pass
    
    def teardown(self, app):
        """Called when plugin is unloaded."""
        pass
    
    def __repr__(self):
        return f"Plugin({self.name} v{self.version})"


# ============================================================
# User Override — file-level override registry
# ============================================================

class UserOverride:
    """
    Manages file-level overrides. When a user creates a file in `custom/`
    that corresponds to a framework file, the user's version takes priority.
    
    This is used primarily for templates and static files.
    """
    
    def __init__(self, custom_dir: str = 'custom'):
        self._custom_dir = Path(custom_dir).resolve()
        self._overrides: Dict[str, Path] = {}
        self._scan_overrides()
    
    def _scan_overrides(self):
        """Scan custom directory for overrides."""
        if not self._custom_dir.exists():
            return
        
        for path in self._custom_dir.rglob('*'):
            if path.is_file() and not path.name.startswith('__'):
                # Key is relative to custom dir
                rel = path.relative_to(self._custom_dir)
                self._overrides[str(rel)] = path
    
    def resolve(self, path: str, default: Optional[str] = None) -> Optional[str]:
        """
        Resolve a file path — returns user override if it exists,
        otherwise returns the default framework path.
        
        Args:
            path: Relative path (e.g., 'templates/index.html')
            default: Default framework path to fall back to
            
        Returns:
            Absolute path to the file to use
        """
        override = self._overrides.get(path)
        if override and override.exists():
            return str(override)
        return default
    
    def has_override(self, path: str) -> bool:
        """Check if user has an override for this path."""
        return path in self._overrides
    
    def get_template_dirs(self) -> List[str]:
        """Get template directories — user's custom/templates/ first."""
        dirs = []
        custom_templates = self._custom_dir / 'templates'
        if custom_templates.exists():
            dirs.append(str(custom_templates))
        return dirs
    
    def get_static_dirs(self) -> List[str]:
        """Get static file directories — user's custom/static/ first."""
        dirs = []
        custom_static = self._custom_dir / 'static'
        if custom_static.exists():
            dirs.append(str(custom_static))
        return dirs
    
    def list_overrides(self) -> Dict[str, str]:
        return {k: str(v) for k, v in self._overrides.items()}


# ============================================================
# Customization Loader — the main entry point
# ============================================================

class CustomizationLoader:
    """
    Main loader that discovers and applies all user customizations.
    
    Usage::
    
        from createsonline import create_app
        from createsonline.customization import CustomizationLoader
    
        app = create_app()
        loader = CustomizationLoader(app)
        loader.load_all()
    """
    
    def __init__(self, app=None, custom_dir: str = 'custom'):
        self.app = app
        self.custom_dir = Path(custom_dir).resolve()
        self.hooks = HookRegistry()
        self.overrides = UserOverride(custom_dir)
        self.plugins: List[PluginBase] = []
        self._loaded_modules: Dict[str, Any] = {}
        self._errors: List[str] = []
    
    def load_all(self):
        """Load all customizations in the correct order."""
        if not self.custom_dir.exists():
            self._create_custom_scaffold()
            return
        
        # Ensure custom/ is importable
        self._ensure_on_path()
        
        # Load in dependency order
        self.load_hooks()
        self.load_models()
        self.load_middleware()
        self.load_admin()
        self.load_routes()
        self.load_plugins()
        
        # Trigger startup hook
        self.hooks.trigger_sync('app_startup', self.app)
        
        if self._errors:
            print(f"[CREATESONLINE] {len(self._errors)} customization warning(s):")
            for err in self._errors:
                print(f"  - {err}")
    
    def _ensure_on_path(self):
        """Make sure custom/ directory is on sys.path for imports."""
        custom_str = str(self.custom_dir)
        parent_str = str(self.custom_dir.parent)
        for p in [custom_str, parent_str]:
            if p not in sys.path:
                sys.path.insert(0, p)
    
    def _load_module(self, name: str) -> Optional[Any]:
        """Load a Python module from the custom directory."""
        module_path = self.custom_dir / f'{name}.py'
        if not module_path.exists():
            return None
        
        try:
            spec = importlib.util.spec_from_file_location(
                f'custom.{name}', str(module_path)
            )
            if spec and spec.loader:
                module = importlib.util.module_from_spec(spec)
                sys.modules[f'custom.{name}'] = module
                spec.loader.exec_module(module)
                self._loaded_modules[name] = module
                return module
        except Exception as e:
            self._errors.append(f"Failed to load custom/{name}.py: {e}")
        return None
    
    # ------------------------------------------------------------------
    # Individual loaders
    # ------------------------------------------------------------------
    
    def load_hooks(self):
        """Load user lifecycle hooks from custom/hooks.py."""
        module = self._load_module('hooks')
        if module:
            # If module defines a 'hooks' HookRegistry, merge it
            user_hooks = getattr(module, 'hooks', None)
            if user_hooks and isinstance(user_hooks, HookRegistry):
                for event, callbacks in user_hooks._hooks.items():
                    for cb in callbacks:
                        self.hooks.register(event, cb)
    
    def load_models(self):
        """
        Load user models from custom/models.py.
        These are auto-registered with SQLAlchemy and admin.
        """
        module = self._load_module('models')
        if module:
            # Trigger hook so admin can auto-register
            models = []
            for attr_name in dir(module):
                attr = getattr(module, attr_name)
                if (isinstance(attr, type) 
                    and hasattr(attr, '__tablename__')
                    and not attr_name.startswith('_')):
                    models.append(attr)
                    self.hooks.trigger_sync('model_registered', attr)
    
    def load_middleware(self):
        """Load user middleware from custom/middleware.py."""
        module = self._load_module('middleware')
        if not module or not self.app:
            return
        
        # Look for MIDDLEWARE list or individual middleware classes
        middleware_list = getattr(module, 'MIDDLEWARE', [])
        for mw in middleware_list:
            if callable(mw):
                try:
                    if hasattr(self.app, 'add_middleware'):
                        self.app.add_middleware(mw)
                except Exception as e:
                    self._errors.append(f"Failed to add middleware {mw}: {e}")
    
    def load_admin(self):
        """
        Load admin customizations from custom/admin.py.
        Users can override admin configs, register models, customize views.
        """
        module = self._load_module('admin')
        if not module:
            return
        
        # Look for register_admin function
        register_fn = getattr(module, 'register_admin', None)
        if register_fn and callable(register_fn):
            try:
                register_fn(self.app)
            except Exception as e:
                self._errors.append(f"Failed to run register_admin: {e}")
        
        # Look for ADMIN_MODELS dict
        admin_models = getattr(module, 'ADMIN_MODELS', {})
        if admin_models and self.app:
            try:
                from createsonline.admin import DynamicAdmin
                admin = getattr(self.app, '_dynamic_admin', None)
                if admin:
                    for model, config in admin_models.items():
                        admin.register(model, config)
            except Exception as e:
                self._errors.append(f"Failed to register admin models: {e}")
    
    def load_routes(self):
        """
        Load user routes from custom/views.py.
        Users define urlpatterns or use @route decorator.
        """
        module = self._load_module('views')
        if not module or not self.app:
            return
        
        # Look for urlpatterns list
        urlpatterns = getattr(module, 'urlpatterns', [])
        for pattern in urlpatterns:
            try:
                if hasattr(self.app, 'add_route'):
                    path = pattern.get('path', pattern.get('url', ''))
                    endpoint = pattern.get('endpoint', pattern.get('view', None))
                    methods = pattern.get('methods', ['GET'])
                    if path and endpoint:
                        self.app.add_route(path, endpoint, methods=methods)
                        self.hooks.trigger_sync('route_registered', path)
            except Exception as e:
                self._errors.append(f"Failed to add route: {e}")
        
        # Look for route-decorated functions
        for attr_name in dir(module):
            attr = getattr(module, attr_name)
            if callable(attr) and hasattr(attr, '_route_path'):
                try:
                    path = attr._route_path
                    methods = getattr(attr, '_route_methods', ['GET'])
                    if hasattr(self.app, 'add_route'):
                        self.app.add_route(path, attr, methods=methods)
                except Exception as e:
                    self._errors.append(f"Failed to add decorated route: {e}")
    
    def load_plugins(self):
        """Load plugins from custom/plugins/ directory."""
        plugins_dir = self.custom_dir / 'plugins'
        if not plugins_dir.exists():
            return
        
        for item in sorted(plugins_dir.iterdir()):
            if item.suffix == '.py' and not item.name.startswith('_'):
                try:
                    name = item.stem
                    spec = importlib.util.spec_from_file_location(
                        f'custom.plugins.{name}', str(item)
                    )
                    if spec and spec.loader:
                        module = importlib.util.module_from_spec(spec)
                        spec.loader.exec_module(module)
                        
                        # Find PluginBase subclasses
                        for attr_name in dir(module):
                            attr = getattr(module, attr_name)
                            if (isinstance(attr, type) 
                                and issubclass(attr, PluginBase)
                                and attr is not PluginBase):
                                plugin = attr()
                                plugin.setup(self.app)
                                self.plugins.append(plugin)
                except Exception as e:
                    self._errors.append(f"Failed to load plugin {item.name}: {e}")
    
    # ------------------------------------------------------------------
    # Scaffold
    # ------------------------------------------------------------------
    
    def _create_custom_scaffold(self):
        """Create the custom/ directory with starter files."""
        self.custom_dir.mkdir(parents=True, exist_ok=True)
        
        # __init__.py
        init_content = '''# custom/__init__.py
"""
Your customizations go here. Everything in this directory
survives framework upgrades. See the files below for examples.
"""
'''
        (self.custom_dir / '__init__.py').write_text(init_content)
        
        # models.py
        models_content = '''# custom/models.py
"""
Define your custom SQLAlchemy models here.
These are auto-discovered and registered with the admin panel.

  UPGRADE-SAFE - This file is never overwritten by framework updates.
"""
# from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean
# from sqlalchemy.orm import declarative_base
# from datetime import datetime
# 
# Base = declarative_base()
# 
# class Article(Base):
#     __tablename__ = 'articles'
#     id = Column(Integer, primary_key=True)
#     title = Column(String(200), nullable=False)
#     content = Column(Text)
#     published = Column(Boolean, default=False)
#     created_at = Column(DateTime, default=datetime.utcnow)
'''
        (self.custom_dir / 'models.py').write_text(models_content)
        
        # views.py
        views_content = '''# custom/views.py
"""
Define your custom route handlers (views) here.
These are auto-discovered and mounted in the app.

  UPGRADE-SAFE - This file is never overwritten by framework updates.
"""
# from createsonline.routing import path
# 
# async def my_api(request):
#     return {"message": "Hello from custom view!"}
# 
# async def dashboard(request):
#     return {"page": "dashboard"}
# 
# urlpatterns = [
#     {"path": "/api/custom", "endpoint": my_api, "methods": ["GET"]},
#     {"path": "/dashboard", "endpoint": dashboard, "methods": ["GET"]},
# ]
'''
        (self.custom_dir / 'views.py').write_text(views_content)
        
        # admin.py
        admin_content = '''# custom/admin.py
"""
Customize the admin panel here.
Register models, override admin configs, add custom views.

  UPGRADE-SAFE - This file is never overwritten by framework updates.
"""
# from createsonline.admin import DynamicAdmin, ModelConfig
# 
# def register_admin(app):
#     """Called automatically to register your admin customizations."""
#     admin = getattr(app, '_dynamic_admin', None)
#     if not admin:
#         return
#     
#     # Example: Register a model with custom config
#     # from custom.models import Article
#     # admin.register(Article, ModelConfig(
#     #     list_display=['title', 'published', 'created_at'],
#     #     search_fields=['title', 'content'],
#     #     list_filter=['published'],
#     #     icon='📝',
#     # ))
'''
        (self.custom_dir / 'admin.py').write_text(admin_content)
        
        # hooks.py
        hooks_content = '''# custom/hooks.py
"""
Lifecycle hooks — run custom code at framework events.

  UPGRADE-SAFE - This file is never overwritten by framework updates.

Available hooks:
    app_startup, app_shutdown,
    before_request, after_request,
    before_response, after_response,
    before_db_save, after_db_save,
    before_db_delete, after_db_delete,
    admin_login, admin_logout,
    model_registered, route_registered,
    error_handler, template_rendered
"""
# from createsonline.customization import HookRegistry
# 
# hooks = HookRegistry()
# 
# @hooks.on('app_startup')
# def on_startup(app):
#     print("[Custom] App starting up!")
# 
# @hooks.on('before_request')
# async def log_request(request):
#     print(f"[Custom] {request.method} {request.path}")
'''
        (self.custom_dir / 'hooks.py').write_text(hooks_content)
        
        # middleware.py
        middleware_content = '''# custom/middleware.py
"""
Custom middleware — runs on every request/response.

  UPGRADE-SAFE - This file is never overwritten by framework updates.
"""
# class TimingMiddleware:
#     """Example: Add response timing header"""
#     async def __call__(self, request, call_next):
#         import time
#         start = time.time()
#         response = await call_next(request)
#         elapsed = time.time() - start
#         response.headers['X-Response-Time'] = f'{elapsed:.3f}s'
#         return response
# 
# MIDDLEWARE = [
#     TimingMiddleware(),
# ]
'''
        (self.custom_dir / 'middleware.py').write_text(middleware_content)
        
        # templates/ dir
        (self.custom_dir / 'templates').mkdir(exist_ok=True)
        (self.custom_dir / 'templates' / '.gitkeep').write_text(
            '# Put template overrides here. Same filename = overrides framework default.\n'
        )
        
        # static/ dir
        (self.custom_dir / 'static').mkdir(exist_ok=True)
        (self.custom_dir / 'static' / '.gitkeep').write_text(
            '# Put static file overrides here.\n'
        )
        
        # plugins/ dir
        (self.custom_dir / 'plugins').mkdir(exist_ok=True)
        (self.custom_dir / 'plugins' / '__init__.py').write_text('')
        
        print(f"[CREATESONLINE] Created custom/ directory with starter files.")
        print(f"  Edit files in custom/ — they survive framework upgrades!")
