from __future__ import annotations
import yaml
import typer
from pathlib import Path
from typing import List, Dict, Any, Optional, Set
from rich.console import Console
from pydantic import BaseModel, Field

from sibi_flux.core import ProjectService, SecureResource
from sibi_flux.utils.security import is_secure_path, is_valid_identifier, is_valid_module_path

console = Console()

class GlobalRegistryMeta(BaseModel):
    """Metadata for a datacube in the global registry."""
    class_name: str
    path: str
    custom_name: Optional[str] = None
    connection_obj: Optional[str] = None
    field_map: Optional[str] = None

class ProposedCubeEntry(BaseModel):
    """Definition of a proposed datacube extension."""
    source: str
    name: str # The class name to use in the application
    module: str # The target module directory in the app
    gen_module: str # The original module path in the global registry
    original_class: Optional[str] = None

from pydantic import BaseModel, Field, field_validator

class AppDatacubeConfig(BaseModel):
    """Registry of datacube extensions for an application."""
    cubes: List[ProposedCubeEntry] = Field(default_factory=list)

    @field_validator("cubes", mode="before")
    @classmethod
    def ensure_list(cls, v: Any) -> List[Any]:
        if v is None:
            return []
        return v

AppDatacubeConfig.model_rebuild()

class CubeProposer(SecureResource):
    """
    Handles scanning the global datacube registry and proposing extensions for applications.
    Hardened with SecureResource for sandboxed I/O.
    """
    
    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)

    def propose(self, db_domain: str, app_name: str, force: bool = False) -> None:
        """
        Scans the global registry and updates the application's datacubes.yaml.
        """
        app_dir = self.project_root / app_name
        if not app_dir.exists() or not app_dir.is_dir():
            console.print(f"[red]Error: Application directory '{app_name}' not found.[/red]")
            raise typer.Exit(code=1)
            
        datacubes_dir = app_dir / "datacubes"
        datacubes_dir.mkdir(parents=True, exist_ok=True)
        
        # 0. Load Configuration via ProjectService to get modular paths
        params = ProjectService.load_modular_config(self.project_root)
        
        # Priority: configured path > known defaults
        reg_rel_path = (
            params.get("paths", {}).get("repositories", {}).get("global_datacube_registry_file")
            or params.get("global_datacube_registry_file")
        )
        
        global_registry_path = None
        if reg_rel_path:
             cand = self.project_root / reg_rel_path
             if cand.exists():
                 global_registry_path = cand
        
        if not global_registry_path:
            # Fallback chain for resilience
            for fallback in ["dataobjects/globals/datacube_registry.yaml", "conf/datacube_registry.yaml"]:
                cand = self.project_root / fallback
                if cand.exists():
                    global_registry_path = cand
                    break

        if not global_registry_path:
            console.print("[yellow]Warning: Global datacube registry not found in any known locations.[/yellow]")
            return

        registry_path = self.get_secure_path(datacubes_dir / "datacubes.yaml")
        global_registry_path = self.get_secure_path(global_registry_path)

        # 1. Load Global Registry
        with self.open_secure(global_registry_path, "r") as f:
            raw_global_data = yaml.safe_load(f) or {}

        # 2. Filter by domain
        matches: List[ProposedCubeEntry] = []
        for conn_name, tables_data in raw_global_data.items():
            if not isinstance(tables_data, dict):
                continue
            
            # Check for group-level domain override
            group_domain = tables_data.get("db_domain")
            
            for table_name, entry_dict in tables_data.items():
                if table_name == "db_domain":
                    continue
                if not isinstance(entry_dict, dict):
                    continue
                
                try:
                    entry = GlobalRegistryMeta(**entry_dict)
                    
                    # Logic: match if group domain matches OR if domain is in file path
                    path_obj = Path(entry.path)
                    is_match = (group_domain == db_domain) or (db_domain in path_obj.parts)
                    
                    if is_match:
                        # Extract a clean module name (e.g. 'asm' instead of 'dataobjects.gencubes.istmo360n.asm.asm_cubes')
                        parts = path_obj.parts
                        clean_module = path_obj.stem # Default fallback
                        if "gencubes" in parts:
                            idx = parts.index("gencubes")
                            # Structure: dataobjects/gencubes/<db_domain>/<module>/<file>.py
                            if len(parts) > idx + 2:
                                clean_module = parts[idx + 2]
                        
                        # Convert full file path to original module path (for imports)
                        gen_module = str(path_obj.with_suffix("")).replace("/", ".")

                        # Security Validation: Ensure valid identifiers for code generation
                        if not is_valid_identifier(clean_module) or not is_valid_module_path(gen_module) or not is_valid_identifier(entry.custom_name or entry.class_name):
                             console.print(f"[dim yellow]Warning: Skipping entry {table_name} due to invalid identifiers for code generation.[/dim yellow]")
                             continue
                        
                        matches.append(ProposedCubeEntry(
                            source=f"{conn_name}.{table_name}",
                            name=entry.custom_name or entry.class_name,
                            module=clean_module,
                            gen_module=gen_module,
                            original_class=entry.class_name
                        ))
                except Exception as e:
                    console.print(f"[dim yellow]Warning: Skipping malformed entry {table_name} in {conn_name}: {e}[/dim yellow]")
                    continue
        
        if not matches:
            console.print(f"[yellow]No Datacubes found for domain '{db_domain}' in registry.[/yellow]")
            return

        # 3. Load/Update App Config
        app_config = AppDatacubeConfig()
        if registry_path.exists() and not force:
            with self.open_secure(registry_path, "r") as f:
                data = yaml.safe_load(f) or {}
                app_config = AppDatacubeConfig(**data)

        # 4. Merge Matches
        existing_sources = {c.source for c in app_config.cubes}
        added_count = 0
        for m in matches:
            if m.source not in existing_sources or force:
                if force:
                    # Replace existing
                    app_config.cubes = [c for c in app_config.cubes if c.source != m.source]
                app_config.cubes.append(m)
                added_count += 1
                
        # 5. Save securely
        if added_count > 0 or force:
            with self.open_secure(registry_path, "w") as f:
                yaml.dump(app_config.model_dump(), f, sort_keys=False)
            console.print(f"[green]Successfully proposed {added_count} Datacubes for '{app_name}'.[/green]")
        else:
            console.print(f"[blue]App '{app_name}' is already up-to-date with domain '{db_domain}'.[/blue]")

def propose_cubes(db_domain: str, app_name: str, force: bool = False) -> None:
    """Entry point for CLI or legacy calls."""
    proposer = CubeProposer()
    proposer.propose(db_domain, app_name, force=force)
