# createsonline/autodiscover.py
"""
CREATESONLINE Auto-Discovery System

Automatically discovers and loads ANY .py file inside user-configured
directories (routes/, views/, api/, handlers/, or ANY name the user chooses).

This gives users Next.js-style file-based organization:

    project_root/
    ├── main.py
    ├── routes/           ← user creates this (any name works)
    │   ├── post.py       ← auto-discovered, any filename
    │   ├── user.py
    │   ├── product.py
    │   └── auth/         ← subdirectories work too
    │       ├── login.py
    │       └── register.py
    ├── views/            ← another directory, also auto-discovered
    │   ├── dashboard.py
    │   └── profile.py
    └── services/         ← or any custom name
        └── payment.py

Usage in main.py::

    from createsonline import create_app
    from createsonline.autodiscover import discover

    app = create_app()
    
    # Auto-discover everything in routes/ and views/ directories
    discover(app, ['routes', 'views'])
    
    # Or discover a single directory
    discover(app, 'routes')
    
    # Or discover with a URL prefix derived from directory structure
    discover(app, 'api', prefix='/api')
    
    # Or auto-detect: scans for any directory containing .py files with urlpatterns
    discover(app)

Each .py file can export:
    1. urlpatterns = [path('/...', handler), ...]   ← Django-style
    2. Decorated handlers with @route('/...')         ← decorator-style
    3. Async handler functions following naming conventions ← convention-based
"""

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

logger = logging.getLogger('createsonline.autodiscover')


# ============================================================
# Route decorator for use inside discovered files
# ============================================================

def route(path: str, methods: List[str] = None, name: str = None):
    """
    Decorator to mark a function as a route handler inside any .py file.
    
    Usage in routes/post.py::
    
        from createsonline.autodiscover import route
        
        @route('/posts')
        async def list_posts(request):
            return {'posts': []}
        
        @route('/posts', methods=['POST'])
        async def create_post(request):
            return {'created': True}
        
        @route('/posts/{id}')
        async def get_post(request, id):
            return {'post': id}
    """
    methods = methods or ['GET']
    
    def decorator(fn):
        fn._route_path = path
        fn._route_methods = [m.upper() for m in methods]
        fn._route_name = name or fn.__name__
        return fn
    return decorator


# ============================================================
# Module Scanner
# ============================================================

class ModuleScanner:
    """Scans a directory and loads all .py modules from it."""
    
    def __init__(self, base_dir: Path = None):
        self.base_dir = base_dir or Path.cwd()
        self._loaded: Dict[str, Any] = {}
        self._errors: List[str] = []
    
    def scan_directory(self, dir_path: Path, prefix: str = '') -> List[dict]:
        """
        Scan a directory recursively for .py files and extract routes.
        
        Returns list of route dicts: {'path', 'handler', 'methods', 'name', 'source'}
        """
        routes = []
        
        if not dir_path.exists() or not dir_path.is_dir():
            return routes
        
        # Get all .py files recursively, sorted for deterministic order
        py_files = sorted(dir_path.rglob('*.py'))
        
        for py_file in py_files:
            if py_file.name.startswith('_'):
                continue  # Skip __init__.py, __pycache__, _private files
            
            module = self._load_file(py_file, dir_path)
            if module is None:
                continue
            
            # Calculate URL prefix from subdirectory structure
            rel_path = py_file.relative_to(dir_path)
            sub_prefix = self._path_to_prefix(rel_path, prefix)
            
            # Extract routes from the module
            file_routes = self._extract_routes(module, sub_prefix, str(py_file))
            routes.extend(file_routes)
        
        return routes
    
    def _load_file(self, file_path: Path, base: Path) -> Optional[Any]:
        """Load a single .py file as a module."""
        file_key = str(file_path)
        if file_key in self._loaded:
            return self._loaded[file_key]
        
        try:
            # Create unique module name from path
            rel = file_path.relative_to(base.parent)
            module_name = f'_discovered.{str(rel).replace(os.sep, ".")[:-3]}'
            
            spec = importlib.util.spec_from_file_location(module_name, str(file_path))
            if not spec or not spec.loader:
                return None
            
            module = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = module
            spec.loader.exec_module(module)
            self._loaded[file_key] = module
            return module
        
        except Exception as e:
            self._errors.append(f'{file_path.name}: {e}')
            logger.warning(f'Failed to load {file_path}: {e}')
            return None
    
    def _path_to_prefix(self, rel_path: Path, base_prefix: str = '') -> str:
        """
        Convert file path to URL prefix.
        
        routes/api/v2/user.py  →  /api/v2  (directory part only, not filename)
        routes/post.py         →  ''       (no subdirectory)
        """
        parts = list(rel_path.parts[:-1])  # exclude filename
        if parts:
            sub = '/' + '/'.join(parts)
        else:
            sub = ''
        
        if base_prefix:
            return base_prefix.rstrip('/') + sub
        return sub
    
    def _extract_routes(self, module: Any, prefix: str, source: str) -> List[dict]:
        """Extract all route definitions from a loaded module."""
        routes = []
        
        # Method 1: urlpatterns list (Django-style)
        urlpatterns = getattr(module, 'urlpatterns', None)
        if urlpatterns:
            for pattern in urlpatterns:
                route_info = self._parse_pattern(pattern, prefix, source)
                if route_info:
                    routes.append(route_info)
        
        # Method 2: @route() decorated functions
        for attr_name in dir(module):
            if attr_name.startswith('_'):
                continue
            attr = getattr(module, attr_name, None)
            if callable(attr) and hasattr(attr, '_route_path'):
                path = attr._route_path
                if not path.startswith('/'):
                    path = '/' + path
                full_path = prefix + path if prefix else path
                routes.append({
                    'path': full_path,
                    'handler': attr,
                    'methods': getattr(attr, '_route_methods', ['GET']),
                    'name': getattr(attr, '_route_name', attr_name),
                    'source': source,
                })
        
        # Method 3: Convention-based (function name → HTTP method)
        #   get_posts → GET /posts
        #   post_users → POST /users
        #   def index(request) → GET /  (from file context)
        conventions = getattr(module, '__autodiscover__', None)
        if conventions is True:
            for attr_name in dir(module):
                if attr_name.startswith('_'):
                    continue
                attr = getattr(module, attr_name, None)
                if not callable(attr) or hasattr(attr, '_route_path'):
                    continue
                
                route_info = self._convention_to_route(attr_name, attr, prefix, source)
                if route_info:
                    routes.append(route_info)
        
        return routes
    
    def _parse_pattern(self, pattern, prefix: str, source: str) -> Optional[dict]:
        """Parse a single urlpattern entry."""
        # CreatesonlineRoute object
        if hasattr(pattern, 'path') and hasattr(pattern, 'handler'):
            path = pattern.path
            if prefix and not path.startswith(prefix):
                path = prefix + path
            return {
                'path': path,
                'handler': pattern.handler,
                'methods': getattr(pattern, 'methods', ['GET']),
                'name': getattr(pattern, 'name', ''),
                'source': source,
            }
        
        # Dict-style: {'path': ..., 'endpoint': ..., 'methods': ...}
        if isinstance(pattern, dict):
            path = pattern.get('path', pattern.get('url', ''))
            handler = pattern.get('endpoint', pattern.get('handler', pattern.get('view')))
            if path and handler:
                if prefix and not path.startswith(prefix):
                    path = prefix + path
                return {
                    'path': path,
                    'handler': handler,
                    'methods': pattern.get('methods', ['GET']),
                    'name': pattern.get('name', ''),
                    'source': source,
                }
        
        # List of sub-patterns (from include())
        if isinstance(pattern, list):
            # Will be flattened by the caller
            pass
        
        return None
    
    def _convention_to_route(self, name: str, fn: Callable,
                              prefix: str, source: str) -> Optional[dict]:
        """
        Derive route from function name convention.
        
        get_posts     → GET    /posts
        post_users    → POST   /users
        put_item      → PUT    /item
        delete_order  → DELETE /order
        index         → GET    /
        detail        → GET    /{id}
        """
        http_methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']
        
        parts = name.split('_', 1)
        if len(parts) == 2 and parts[0].lower() in http_methods:
            method = parts[0].upper()
            resource = '/' + parts[1].replace('_', '/')
        elif name == 'index':
            method = 'GET'
            resource = '/'
        elif name == 'detail':
            method = 'GET'
            resource = '/{id}'
        else:
            return None
        
        full_path = prefix + resource if prefix else resource
        return {
            'path': full_path,
            'handler': fn,
            'methods': [method],
            'name': name,
            'source': source,
        }


# ============================================================
# Main discover() function
# ============================================================

def discover(app, directories: Union[str, List[str]] = None,
             prefix: str = '', base_dir: str = None,
             verbose: bool = False) -> int:
    """
    Auto-discover and register routes from user directories.
    
    Args:
        app: CREATESONLINE app instance
        directories: Directory name(s) to scan. Can be:
            - A string: 'routes'
            - A list: ['routes', 'views', 'api']
            - None: auto-detect directories containing .py files
        prefix: URL prefix to prepend to all discovered routes
        base_dir: Base directory to look in (default: cwd)
        verbose: Print discovery details
    
    Returns:
        Number of routes registered
    
    Examples::
    
        # Scan routes/ directory
        discover(app, 'routes')
        
        # Scan multiple directories
        discover(app, ['routes', 'views', 'api'])
        
        # Scan with prefix
        discover(app, 'api', prefix='/api/v1')
        
        # Auto-detect (scans any directory with .py files containing urlpatterns)
        discover(app)
    """
    base = Path(base_dir) if base_dir else Path.cwd()
    scanner = ModuleScanner(base)
    
    # Determine directories to scan
    if directories is None:
        directories = _auto_detect_directories(base)
    elif isinstance(directories, str):
        directories = [directories]
    
    total_registered = 0
    
    for dir_name in directories:
        dir_path = base / dir_name
        if not dir_path.exists() or not dir_path.is_dir():
            if verbose:
                logger.info(f'Directory not found: {dir_name}/')
            continue
        
        # Determine prefix for this directory
        dir_prefix = prefix
        
        # Scan and collect routes
        routes = scanner.scan_directory(dir_path, dir_prefix)
        
        # Register routes with the app
        for route_info in routes:
            registered = _register_route(app, route_info)
            if registered:
                total_registered += 1
                if verbose:
                    methods = ','.join(route_info['methods'])
                    print(f'  [{methods}] {route_info["path"]} ← {Path(route_info["source"]).name}')
    
    # Report errors
    if scanner._errors:
        for err in scanner._errors:
            logger.warning(f'Auto-discover error: {err}')
    
    if verbose or total_registered > 0:
        logger.debug(f'Auto-discovered {total_registered} routes from {directories}')
    
    return total_registered


def discover_views(app, directory: str = 'views', prefix: str = '',
                   base_dir: str = None, verbose: bool = False) -> int:
    """Convenience: discover from views/ directory."""
    return discover(app, directory, prefix=prefix, base_dir=base_dir, verbose=verbose)


def discover_api(app, directory: str = 'api', prefix: str = '/api',
                 base_dir: str = None, verbose: bool = False) -> int:
    """Convenience: discover from api/ directory with /api prefix."""
    return discover(app, directory, prefix=prefix, base_dir=base_dir, verbose=verbose)


# ============================================================
# Internal helpers
# ============================================================

def _auto_detect_directories(base: Path) -> List[str]:
    """Auto-detect directories that likely contain route/view files."""
    candidates = []
    
    # Common directory names
    common_names = {
        'routes', 'views', 'api', 'handlers', 'endpoints',
        'controllers', 'pages', 'resources',
    }
    
    for item in base.iterdir():
        if not item.is_dir() or item.name.startswith(('.', '_')):
            continue
        
        # Check if directory name is a common pattern
        if item.name.lower() in common_names:
            candidates.append(item.name)
            continue
        
        # Check if any .py file in this directory has urlpatterns or @route
        for py_file in item.glob('*.py'):
            if py_file.name.startswith('_'):
                continue
            try:
                content = py_file.read_text(encoding='utf-8', errors='ignore')
                if ('urlpatterns' in content or '@route(' in content 
                    or '__autodiscover__' in content):
                    candidates.append(item.name)
                    break
            except Exception:
                continue
    
    return sorted(candidates)


def _register_route(app, route_info: dict) -> bool:
    """Register a single route with the app."""
    path = route_info['path']
    handler = route_info['handler']
    methods = route_info.get('methods', ['GET'])
    
    try:
        for method in methods:
            method_lower = method.lower()
            
            # Try the decorator pattern (app.get, app.post, etc.)
            if hasattr(app, method_lower):
                decorator = getattr(app, method_lower)
                decorator(path)(handler)
            # Try add_route method
            elif hasattr(app, 'add_route'):
                app.add_route(path, handler, methods=[method])
            # Try direct routes dict
            elif hasattr(app, 'routes'):
                route_key = f'{method}:{path}'
                app.routes[route_key] = handler
            else:
                return False
        
        return True
    
    except Exception as e:
        logger.warning(f'Failed to register {path}: {e}')
        return False


# ============================================================
# Export
# ============================================================

__all__ = [
    'discover',
    'discover_views',
    'discover_api',
    'route',
    'ModuleScanner',
]
