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

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

console = Console()

class ProposedReaderEntry(BaseModel):
    """Definition of a proposed reader."""
    name: str = Field(..., description="The class name for the reader")
    storage_variable: str = Field(..., description="The variable in conf.storage to use")
    storage_subpath: Optional[str] = Field(None, description="The sub-path within the storage variable")
    description: Optional[str] = Field(None, description="Optional description for the reader")

class AppReaderConfig(BaseModel):
    """Registry of proposed readers for an application."""
    readers: List[ProposedReaderEntry] = Field(default_factory=list)

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

class ReaderProposer(SecureResource):
    """
    Handles scanning S3 layouts and proposing readers for applications.
    """
    
    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)

    def propose(
        self, 
        domain: str, 
        layout_name: Optional[str | list[str]] = None, 
        depot_name: Optional[str] = None,
        force: bool = False
    ) -> None:
        """
        Scans conf/storage.yaml and updates the domain's reader blueprint.
        """
        # Target blueprints directory
        readers_dir = self.project_root / "blueprints" / "readers"
        readers_dir.mkdir(parents=True, exist_ok=True)
        
        readers_yaml_path = readers_dir / f"{domain}.yaml"
        
        # 1. Load storage.yaml
        storage_path = self.project_root / "conf" / "storage.yaml"
        if not storage_path.exists():
            console.print(f"[yellow]Warning: {storage_path} not found. Run 'sibi-flux env' first.[/yellow]")
            return

        with self.open_secure(self.get_secure_path(storage_path), "r") as f:
            storage_data = yaml.safe_load(f) or {}

        s3_layouts = storage_data.get("s3_layouts", {})
        if not s3_layouts:
            console.print("[yellow]No S3 layouts found in storage.yaml.[/yellow]")
            return

        filesystems = storage_data.get("filesystems", [])
        
        # 2. Resolve layout names
        resolved_layout_names = []
        
        if layout_name:
            if isinstance(layout_name, list):
                resolved_layout_names = layout_name
            else:
                resolved_layout_names = [layout_name]
        else:
            # Try to discover from filesystem entry
            fs_entry = next((fs for fs in filesystems if fs.get("id") == domain), None)
            if fs_entry and "layout" in fs_entry:
                fs_layout = fs_entry["layout"]
                if isinstance(fs_layout, list):
                    resolved_layout_names = fs_layout
                elif fs_layout:
                    resolved_layout_names = [fs_layout]
            
            # Skip if no layout specified in storage.yaml
            if not resolved_layout_names:
                console.print(f"[dim]No layout specified for domain '{domain}' in storage.yaml. Skipping reader generation.[/dim]")
                if force and readers_yaml_path.exists():
                    readers_yaml_path.unlink()
                    console.print(f"  [yellow]Cleanup: Deleted stale blueprint {readers_yaml_path.name}[/yellow]")
                return
            
            console.print(f"[dim]Resolved layout(s) for '{domain}' from storage.yaml: {', '.join(resolved_layout_names)}[/dim]")

        # 3. Generate proposals across all applicable layouts
        proposals: List[ProposedReaderEntry] = []
        
        for l_name in resolved_layout_names:
            if l_name not in s3_layouts:
                console.print(f"[red]Error: Layout '{l_name}' not found in storage.yaml. Skipping.[/red]")
                continue

            layout_def = s3_layouts[l_name]
            
            # Smart Filtering per layout
            current_depot_name = depot_name
            if not current_depot_name:
                candidates = [d for d in layout_def.keys() if domain in d.lower()]
                if len(candidates) == 1:
                    current_depot_name = candidates[0]
                    console.print(f"[dim]Smart match in '{l_name}': Filtering readers for depot '{current_depot_name}'.[/dim]")
                elif not candidates:
                    # If we have multiple layouts, we definitely only want to propose ALL if it's the ONLY layout
                    # or if specifically requested. For now, we follow the warning pattern.
                    console.print(f"[yellow]Warning: No depot matches domain '{domain}' in layout '{l_name}'. Proposing ALL readers from this layout.[/yellow]")
            
            for d_name, subdirs in layout_def.items():
                if current_depot_name and d_name != current_depot_name:
                    continue

                depot_base = d_name.replace("_storage", "")
                depot_prefix = "".join([p.capitalize() for p in depot_base.split("_") if p])
                storage_var = d_name if d_name.endswith("_storage") else f"{d_name}_storage"

                for subdir in subdirs:
                    suffix_parts = subdir.replace("/", "_").split("_")
                    suffix = "".join([p.capitalize() for p in suffix_parts if p])
                    class_name = f"{depot_prefix}{suffix}Reader"

                    if is_valid_identifier(class_name):
                        proposals.append(ProposedReaderEntry(
                            name=class_name,
                            storage_variable=storage_var,
                            storage_subpath=subdir,
                            description=f"Reader for {subdir} in {d_name} (from {l_name})."
                        ))

        if not proposals:
            console.print(f"[yellow]No readers could be proposed for domain '{domain}' from resolved layouts.[/yellow]")
            return

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

        # 5. Merge Proposals
        existing_names = {r.name for r in app_config.readers}
        added_count = 0
        
        if force:
            # If force, we wipe existing readers for this domain and re-propose
            app_config.readers = []
            existing_names = set()

        for p in proposals:
            if p.name not in existing_names:
                app_config.readers.append(p)
                added_count += 1
                existing_names.add(p.name)
                
        # 6. Save
        if added_count > 0 or force:
            app_config.readers.sort(key=lambda x: x.name)
            with self.open_secure(self.get_secure_path(readers_yaml_path), "w") as f:
                yaml.dump(app_config.model_dump(), f, sort_keys=False)
            console.print(f"[green]Successfully proposed {added_count} readers for domain '{domain}' across {len(resolved_layout_names)} layouts.[/green]")
        else:
            console.print(f"[blue]Domain '{domain}' readers are already up-to-date.[/blue]")

def propose_readers(
    domain: str, 
    layout_name: Optional[str] = None, 
    depot_name: Optional[str] = None,
    force: bool = False
) -> None:
    """Entry point for CLI calls."""
    proposer = ReaderProposer()
    proposer.propose(domain, layout_name, depot_name=depot_name, force=force)
