from typing import Dict, Any, Optional, List, Set, Iterable
import re
from pathlib import Path
from sibi_flux.init.env_engine import EnvFile, EnvSection, EnvVar


class EnvGenerator:
    """
    Generates EnvSection objects from high-level configurations (Templates).
    """

    INTERPOLATION_RE = re.compile(r"\$\{([^}]+)\}")

    MAPPING = {
        "S3": "FsSettings",
        "POSTGRES": "DatabaseSettings",
        "MYSQL": "DatabaseSettings",
        "SQLALCHEMY_DATABASE": "DatabaseSettings",
        "CLICKHOUSE": "DatabaseSettings",
        "REDIS": "RedisBaseSettings",
        "DB_POOL": "DbPoolSettings",
        "AIRFLOW": "SibiBaseSettings",
        "OSMNX": "SibiBaseSettings",
        "SENTINEL": "SibiBaseSettings",
        "OPENOBSERVE": "SibiBaseSettings",
        "MCP": "SibiBaseSettings",
        "DASK": "SibiBaseSettings",
        "GENERIC": "SibiBaseSettings",
    }

    @staticmethod
    def _make_key(section_name: str, suffix: str) -> str:
        return f"{section_name.upper()}_{suffix.upper()}"

    @staticmethod
    def _sanitize_identifier(name: str) -> str:
        """
        Sanitize a string to be a valid Python identifier.
        """
        # Replace non-alphanumeric characters with underscores
        sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", name)
        # Ensure it doesn't start with a digit
        if sanitized and sanitized[0].isdigit():
            sanitized = f"var_{sanitized}"
        return sanitized

    @staticmethod
    def _to_class_name(name: str) -> str:
        parts = [part.capitalize() for part in name.split("_") if part]
        class_name = "".join(parts) + "Settings"
        return EnvGenerator._sanitize_identifier(class_name)

    @staticmethod
    def _to_conf_name(name: str) -> str:
        conf_name = f"{name.lower()}_conf"
        return EnvGenerator._sanitize_identifier(conf_name)

    @staticmethod
    def _compose_uri(section_type: str, config: Dict[str, Any]) -> Optional[str]:
        # Handle different types for URI composition
        if section_type in ["POSTGRES", "CLICKHOUSE", "MYSQL", "SQLALCHEMY_DATABASE"]:
            # postgresql://user:pass@host:port/db
            user = config.get("USER", "")
            password = config.get("PASSWORD", "")
            host = config.get("HOST", "localhost")
            port = config.get("PORT", "5432")
            db = config.get("DATABASE", "")
            dialect = config.get("DIALECT", "").lower()

            scheme = "postgresql"
            if section_type == "CLICKHOUSE":
                scheme = "clickhouse+native"
            elif section_type == "MYSQL":
                scheme = "mysql+pymysql"
                if port == "5432":
                    port = "3306"
            elif section_type == "SQLALCHEMY_DATABASE":
                if dialect == "mysql":
                    scheme = "mysql+pymysql"
                elif dialect == "clickhouse":
                    scheme = "clickhouse+native"
                elif dialect == "postgres":
                    scheme = "postgresql"
                else:
                    scheme = dialect or "postgresql"  # Fallback

            auth_part = f"{user}"
            if password:
                auth_part += f":{password}"

            return f"{scheme}://{auth_part}@{host}:{port}/{db}"

        elif section_type == "REDIS":
            # redis://host:port/db_index
            host = config.get("HOST", "localhost")
            port = config.get("PORT", "6379")
            db_index = config.get("DB_INDEX", "0")
            return f"redis://{host}:{port}/{db_index}"

        return None

    @staticmethod
    def generate_from_template(
        section_name: str,
        section_type: str,
        config: Dict[str, Any],
        resource_id: Optional[str] = None,
    ) -> EnvSection:
        """
        section_type: S3, POSTGRES, CLICKHOUSE, REDIS, MYSQL, GENERIC
        config: Dict of raw values (e.g. {'HOST': 'localhost', ...})
        """
        vars_list = []

        # 1. Generate base variables and preserve individual fields
        # Filter out legacy flags so they don't leak into the env file

        normalized_config = {
            k.upper(): v
            for k, v in config.items()
            if k.upper() not in ["URL_AUTO_GENERATED", "URI_AUTO_GENERATED"]
        }

        # Common fields for different types
        if section_type == "S3":
            # Map valid S3 fields to FS_*
            # We look for normalized keys in config (fs_key -> FS_KEY, access_key -> FS_KEY fallback)

            # Helper to find value from multiple keys
            def get_val(keys):
                for k in keys:
                    if k in normalized_config:
                        return normalized_config[k]
                return None

            # Standard FS keys
            vars_list.append(
                EnvVar(key=EnvGenerator._make_key(section_name, "FS_TYPE"), value="s3")
            )

            val = get_val(["FS_PATH", "PATH", "BUCKET"])
            if val:
                vars_list.append(
                    EnvVar(
                        key=EnvGenerator._make_key(section_name, "FS_PATH"),
                        value=str(val),
                    )
                )

            val = get_val(["FS_KEY", "ACCESS_KEY", "AWS_ACCESS_KEY_ID"])
            if val:
                vars_list.append(
                    EnvVar(
                        key=EnvGenerator._make_key(section_name, "FS_KEY"),
                        value=str(val),
                    )
                )

            val = get_val(["FS_SECRET", "SECRET_KEY", "AWS_SECRET_ACCESS_KEY"])
            if val:
                vars_list.append(
                    EnvVar(
                        key=EnvGenerator._make_key(section_name, "FS_SECRET"),
                        value=str(val),
                    )
                )

            val = get_val(["FS_ENDPOINT", "ENDPOINT_URL", "ENDPOINT", "S3_ENDPOINT"])
            if val:
                vars_list.append(
                    EnvVar(
                        key=EnvGenerator._make_key(section_name, "FS_ENDPOINT"),
                        value=str(val),
                    )
                )

            val = get_val(["FS_TOKEN"])
            if val:
                vars_list.append(
                    EnvVar(
                        key=EnvGenerator._make_key(section_name, "FS_TOKEN"),
                        value=str(val),
                    )
                )

        elif section_type in ["POSTGRES", "CLICKHOUSE", "MYSQL", "SQLALCHEMY_DATABASE"]:
            allowed_fields = ["HOST", "PORT", "USER", "PASSWORD", "DATABASE"]
            if section_type == "SQLALCHEMY_DATABASE":
                allowed_fields.append("DIALECT")

            # Always include structural fields for DBs
            for field in allowed_fields:
                if field in normalized_config:
                    vars_list.append(
                        EnvVar(
                            key=EnvGenerator._make_key(section_name, field),
                            value=str(normalized_config[field]),
                        )
                    )
        elif section_type == "REDIS":
            allowed_fields = ["HOST", "PORT", "DB_INDEX"]
            for field in allowed_fields:
                if field in normalized_config:
                    vars_list.append(
                        EnvVar(
                            key=EnvGenerator._make_key(section_name, field),
                            value=str(normalized_config[field]),
                        )
                    )

        # Handle Generic or extra fields not in allowed_list but present in config
        # For Typed Sections, we typically stick to the allowed list to avoid polluting the env with random UI state
        # But for GENERIC or Service Types (Airflow, etc), we want ALL fields.
        if section_type not in ["S3", "POSTGRES", "CLICKHOUSE", "MYSQL", "REDIS"]:
            existing_keys: Set[str] = {x.key for x in vars_list}
            for k, v in normalized_config.items():
                final_key = EnvGenerator._make_key(section_name, k)
                # Avoid duplicates
                if final_key not in existing_keys:
                    vars_list.append(EnvVar(key=final_key, value=str(v)))
                    existing_keys.add(final_key)

        # 2. URI Auto-generation
        # Always generate URI if supported
        uri = EnvGenerator._compose_uri(section_type, normalized_config)
        if uri and section_type != "S3":
            suffix = "URL"
            # Use DB_URL for Database types to match DatabaseSettings schema
            if section_type in ["POSTGRES", "MYSQL", "SQLALCHEMY_DATABASE"]:
                suffix = "DB_URL"

            vars_list.append(
                EnvVar(
                    key=EnvGenerator._make_key(section_name, suffix),
                    value=uri,
                    comment="Auto-generated connection string",
                )
            )

        return EnvSection(
            name=section_name,
            type=section_type,
            vars=vars_list,
            resource_id=resource_id,
        )

    @staticmethod
    def _parse_connection_string(scheme: str, url_str: str) -> Dict[str, Any]:
        """
        Reverse-engineer URL components into settings dict.
        """
        from urllib.parse import urlparse, unquote

        # Helper to clean schemes like mysql+pymysql://
        # urlparse handles most, but let's be safe.
        if "://" not in url_str:
            return {}

        try:
            parsed = urlparse(url_str)
        except Exception:
            return {}

        params: Dict[str, Any] = {}
        if parsed.hostname:
            params["HOST"] = parsed.hostname
        if parsed.port:
            params["PORT"] = parsed.port
        else:
            # Infer default ports for known schemes if not explicit
            scheme_map = {
                "clickhouse": 8123,
                "mysql": 3306,
                "redis": 6379,
                "postgresql": 5432,
            }
            try:
                base_scheme = parsed.scheme.split("+")[0]
                if base_scheme in scheme_map:
                    params["PORT"] = scheme_map[base_scheme]
            except (ValueError, AttributeError):
                pass

        if parsed.username:
            params["USER"] = parsed.username
        if parsed.password:
            try:
                params["PASSWORD"] = unquote(parsed.password)
            except Exception:
                params["PASSWORD"] = parsed.password

        if parsed.path:
            params["DATABASE"] = parsed.path.lstrip("/")

        # Dialect inference
        try:
            main_scheme = parsed.scheme.split("+")[0]
            if main_scheme == "mysql":
                params["DIALECT"] = "mysql"
            elif main_scheme == "postgresql":
                params["DIALECT"] = "postgresql"
            elif main_scheme == "clickhouse":
                params["DIALECT"] = "clickhouse"
        except (ValueError, AttributeError):
            pass

        return params

    @staticmethod
    def _enrich_database_urls(section: EnvSection) -> None:
        """Reverse engineer Database URLs and populate missing fields."""
        if section.type.upper() not in [
            "POSTGRES",
            "MYSQL",
            "CLICKHOUSE",
            "SQLALCHEMY_DATABASE",
            "REDIS",
        ]:
            return

        # Find URL var
        prefix = f"{section.name.upper()}_"
        url_var = next(
            (
                v
                for v in section.vars
                if v.key in (f"{prefix}URL", f"{prefix}DB_URL", "DATABASE_URL")
            ),
            None,
        )

        if url_var:
            parsed = EnvGenerator._parse_connection_string(
                section.type.lower(), url_var.value
            )
            existing_keys = {v.key for v in section.vars}
            for k, val in parsed.items():
                full_key = EnvGenerator._make_key(section.name, k)
                if full_key not in existing_keys:
                    section.vars.append(
                        EnvVar(
                            key=full_key,
                            value=str(val),
                            comment="Inferred from URL",
                        )
                    )
                    existing_keys.add(full_key)

    @staticmethod
    def _generate_field(
        lines: List[str],
        field_name: str,
        var: EnvVar,
        production_mode: bool,
        is_optional: bool = False,
    ) -> None:
        """Generates code for a single Pydantic field."""
        py_type = "str"
        val = var.value
        val_repr = repr(val)
        low_key = field_name.lower()
        low_val = val.lower()

        is_pure_ref = val.startswith("${") and val.endswith("}")

        if is_pure_ref:
            py_type = "SecretStr"
            # If it's a pure ref like ${MY_VAR}, we use it as an alias
            ref_name = val[2:-1]
            val_repr = f'Field(alias="{ref_name}")'
        elif any(k in low_key for k in ("secret", "password", "key", "token", "uri", "url", "dsn", "credential")):
            py_type = "SecretStr"
            val_repr = f'SecretStr({repr(val)})'
        elif low_val in ("true", "false"):
            py_type = "bool"
            val_repr = "True" if low_val == "true" else "False"
        elif val in ("0", "1") and any(k in low_key for k in ("secure", "verify", "enable", "debug", "insecure")):
            py_type = "bool"
            val_repr = "True" if val == "1" else "False"
        elif val.isdigit():
            py_type = "int"
            val_repr = val

        if production_mode:
            # In production, we NEVER want hardcoded default values for required fields.
            # Only aliases/refs are allowed as they describe where to find the value.
            if is_pure_ref:
                final_type = f"Optional[{py_type}]" if is_optional else py_type
                lines.append(f"    {field_name}: {final_type} = {val_repr}")
            elif is_optional:
                lines.append(f"    {field_name}: Optional[{py_type}] = None")
            else:
                lines.append(f"    {field_name}: {py_type}")
        else:
            lines.append(f"    {field_name}: {py_type} = {val_repr}")

    @staticmethod
    def generate_pydantic_code(
        env_file: EnvFile,
        env_file_name: Optional[str] = None,
        production_mode: bool = False,
    ) -> str:
        """Generates Python code compatible with sibi_flux.config.settings"""
        lines = [
            "from typing import Optional, ClassVar",
            "from pydantic import SecretStr, Field, computed_field, model_validator",
            "from typing_extensions import Self",
            "from pydantic_settings import SettingsConfigDict",
            "",
            "# Import Generic Settings from Core",
            "from sibi_flux.config.settings import (",
            "    SibiBaseSettings, FsSettings, WebDavSettings,",
            "    DatabaseSettings, RedisBaseSettings, DbPoolSettings",
            ")",
            "",
            "# --- Generated Settings ---",
            "",
        ]

        STRUCTURAL_TYPES = {
            "S3",
            "POSTGRES",
            "MYSQL",
            "CLICKHOUSE",
            "REDIS",
            "DB_POOL",
            "SQLALCHEMY_DATABASE",
        }

        FORCED_GENERATION_TYPES = {
            "POSTGRES",
            "MYSQL",
            "CLICKHOUSE",
            "SQLALCHEMY_DATABASE",
            "REDIS",
        }

        for section in env_file.sections:
            base_class = EnvGenerator.MAPPING.get(
                section.type.upper(), "SibiBaseSettings"
            )
            class_name = EnvGenerator._to_class_name(section.name)

            # Enrichment
            EnvGenerator._enrich_database_urls(section)

            lines.append(f"class {class_name}({base_class}):")
            conf_name = EnvGenerator._to_conf_name(section.name)
            lines.append(f'    conf_name: ClassVar[str] = "{conf_name}"')

            prefix = f"{section.name.upper()}_"
            processed_vars = []
            field_value_map = {}
            for var in section.vars:
                if var.key.startswith(prefix):
                    raw_field_name = var.key[len(prefix) :].lower()
                    field_name = EnvGenerator._sanitize_identifier(raw_field_name)
                    field_value_map[field_name] = var.value
                    processed_vars.append((field_name, var))

            if section.type.upper() not in STRUCTURAL_TYPES or (
                section.type.upper() in FORCED_GENERATION_TYPES and processed_vars
            ):
                computed_fields = []
                for field_name, var in processed_vars:
                    val = var.value
                    is_pure_ref = val.startswith("${") and val.endswith("}")

                    if not is_pure_ref and "${" in val:
                        placeholders = EnvGenerator.INTERPOLATION_RE.findall(val)
                        f_string_parts = val
                        valid_interpolation = True

                        for p in placeholders:
                            search_val = f"${{{p}}}"
                            matching_field = next(
                                (fk for fk, fv in field_value_map.items() if fv == search_val),
                                None
                            )

                            if matching_field:
                                f_string_parts = f_string_parts.replace(
                                    search_val,
                                    f"{{self.{matching_field}.get_secret_value()}}",
                                )
                            else:
                                valid_interpolation = False
                                break

                        if valid_interpolation:
                            # Sanitize static parts of the f-string to prevent injection
                            # We escape double quotes as the f-string uses them.
                            # Also escape braces that are NOT part of our interpolation logic.
                            safe_f_string = f_string_parts.replace('"', '\\"')
                            computed_fields.append((field_name, safe_f_string))
                            continue

                    # Mark DB fields as optional if they are part of a structured DB class
                    field_is_optional = False
                    if section.type.upper() in ["POSTGRES", "MYSQL", "CLICKHOUSE", "SQLALCHEMY_DATABASE"]:
                         if field_name in ["host", "port", "user", "password", "database", "dialect", "secure", "verify"]:
                              field_is_optional = True

                    EnvGenerator._generate_field(lines, field_name, var, production_mode, is_optional=field_is_optional)

                if computed_fields:
                    lines.append("")
                    for fname, fexpr in computed_fields:
                        lines.append("    @computed_field")
                        lines.append(f"    def {fname}(self) -> str:")
                        lines.append(f'        return f"{fexpr}"')

            config_str = f'env_prefix="{prefix}"'
            if env_file_name:
                config_str += f', env_file="{env_file_name}"'
            lines.append(f"    model_config = SettingsConfigDict({config_str})")

            stype_upper = section.type.upper()
            if stype_upper in ["POSTGRES", "MYSQL", "CLICKHOUSE", "SQLALCHEMY_DATABASE"]:
                scheme = "postgresql"
                if stype_upper == "CLICKHOUSE":
                    scheme = "clickhouse+connect"
                elif stype_upper == "MYSQL":
                    scheme = "mysql+pymysql"
                
                if stype_upper == "SQLALCHEMY_DATABASE":
                    uri_expr = f'f"{{self.dialect}}://{{self.user}}:{{self.password.get_secret_value()}}@{{self.host}}:{{self.port}}/{{self.database}}"'
                else:
                    uri_expr = f'f"{scheme}://{{self.user}}:{{self.password.get_secret_value()}}@{{self.host}}:{{self.port}}/{{self.database}}"'

                lines.extend([
                    "",
                    '    @model_validator(mode="after")',
                    "    def compute_db_url(self) -> Self:",
                    '        if self.db_url == "sqlite:///:memory:" or not self.db_url:',
                    f'            self.db_url = {uri_expr}',
                    "        return self"
                ])
            lines.append("")

        return "\n".join(lines)

    DERIVED_MAPPINGS = {
        "OPENOBSERVE": {
            "target_name": "logger_config",
            "mapping": {
                "logger_name": "logger_name",
                "enable_otel": "enable_otel",
                "otel_endpoint": "otel_grpc_endpoint",
            },
            "expressions": {
                "otel_insecure": "getattr(obj, 'otel_insecure', True)",
                "otel_service_name": "getattr(obj, 'otel_service_name', 'ibis')",
                "otel_stream_name": "getattr(obj, 'otel_stream_name', 'ibis')",
            },
        },
        "DASK": {
            "target_name": "dask_conf",
            "mapping": {
                "address": "scheduler_address",
            }
        }
    }

    @staticmethod
    def generate_credentials_code(env_file: EnvFile) -> str:
        """
        Generates the lazy-loading credentials.py script.
        """
        lines = [
            "from typing import Any, Dict, TYPE_CHECKING",
            "",
            "# Explicitly export names for star imports and documentation",
            "__all__ = [",
        ]

        # Collect names for __all__
        conf_names = []
        class_names = []
        derived_configs = []  # (type, config_name, class_name)

        # Track special types for derived configs (Singleton logic: first wins)
        seen_types = set()

        for section in env_file.sections:
            conf_name = EnvGenerator._to_conf_name(section.name)
            class_name = EnvGenerator._to_class_name(section.name)
            conf_names.append(conf_name)
            class_names.append(class_name)

            # Check for derived configs
            stype = section.type.upper()
            if stype in EnvGenerator.DERIVED_MAPPINGS and stype not in seen_types:
                definition = EnvGenerator.DERIVED_MAPPINGS[stype]
                target_name = definition.get("target_name")
                if not target_name:
                    target_name = f"{section.name.lower()}_{definition.get('target_suffix', 'config')}"

                # Special Case: Legacy overrides for known singletons (like clickhouse_config instead of main_db_config)
                if stype == "CLICKHOUSE":
                    target_name = "clickhouse_config"
                if stype == "REDIS":
                    target_name = "redis_config"
                # OpenObserve is already "logger_config" via target_name above

                derived_configs.append((stype, target_name, class_name))
                seen_types.add(stype)

        # Formatting __all__
        all_exports = conf_names + [dc[1] for dc in derived_configs]

        # Chunking for display
        chunk_size = 4
        for i in range(0, len(all_exports), chunk_size):
            chunk = all_exports[i : i + chunk_size]
            quoted = [f'"{n}"' for n in chunk]
            comma = "," if i + chunk_size < len(all_exports) else ""
            lines.append(f"    {', '.join(quoted)}{comma}")
        lines.append("]")
        lines.append("")

        # Inject Type Stubs
        lines.append("if TYPE_CHECKING:")
        if not all_exports:
            lines.append("    pass")
        else:
            for name in all_exports:
                lines.append(f"    {name}: Dict[str, Any]")
        lines.append("")

        lines.append("def __getattr__(name: str) -> Any:")
        lines.append('    """')
        lines.append("    Lazy load configuration using Pydantic Settings.")
        lines.append('    """')
        lines.append("    # Import locally to avoid module-level import side effects")
        if class_names:
            lines.append("    from ..settings import (")

            # Import list
            for i in range(0, len(class_names), 3):
                chunk = class_names[i : i + 3]
                comma = "," if i + 3 < len(class_names) else ""
                lines.append(f"        {', '.join(chunk)}{comma}")
            lines.append("    )")
        lines.append("")
        lines.append("    # Helper to return dict for backward compatibility")
        lines.append("    def to_dict(model_instance: Any) -> Dict[str, Any]:")
        lines.append("        return model_instance.model_dump()")
        lines.append("")
        lines.append("    # 1. Direct Config Mappings")
        lines.append("    _mappings = {")
        for section in env_file.sections:
            class_name = EnvGenerator._to_class_name(section.name)
            lines.append(f"        {class_name}.conf_name: {class_name},")
        lines.append("    }")
        lines.append("")
        lines.append("    if name in _mappings:")
        lines.append("        return to_dict(_mappings[name]())")

        lines.append("")
        lines.append("    # 2. Derived Configurations")

        for stype, target_name, class_name in derived_configs:
            definition = EnvGenerator.DERIVED_MAPPINGS[stype]

            lines.append(f'    if name == "{target_name}":')

            if "method" in definition:
                method_name = definition["method"]
                lines.append(f"        return {class_name}().{method_name}()")
            else:
                mapping = definition.get("mapping", {})
                expressions = definition.get("expressions", {})
                lines.append(f"        obj = {class_name}()")
                lines.append("        return {")
                for key, attr in mapping.items():
                    lines.append(f'            "{key}": obj.{attr},')
                for key, expr in expressions.items():
                    lines.append(f'            "{key}": {expr},')
                lines.append("        }")

            lines.append("")

        lines.append(
            f'    raise AttributeError(f"module {{__name__!r}} has no attribute {{name!r}}")'
        )
        lines.append("")
        lines.append("def __dir__():")
        lines.append("    return __all__")

        return "\n".join(lines)

    @staticmethod
    def generate_storage_code(discovery_data: Dict[str, Any], project_name: str = "solutions") -> str:
        """
        Generates the lazy-loading storage configuration for conf/storage/__init__.py
        """
        # We look for s3_layouts now
        s3_layouts = discovery_data.get("s3_layouts", discovery_data.get("layouts", {}))
        filesystems = discovery_data.get("filesystems", [])

        if not filesystems:
            return ""

        lines = [
            "from typing import Any, Dict, TYPE_CHECKING, Iterable",
            "from sibi_flux.storage import StorageManager, FsRegistry",
            "from sibi_flux.storage.factory import create_storage_manager",
            "",
            "class LayoutView:",
            "    \"\"\"Namespace-like proxy for a specific layout within a StorageManager.\"\"\"",
            "    def __init__(self, manager: StorageManager, layout_def: Dict[str, Iterable[str]]):",
            "        for depot, subdirs in layout_def.items():",
            "            # Create a simple container for the depot",
            "            depot_container = type('DepotView', (), {})()",
            "            # Root depot path",
            "            setattr(depot_container, '_root', manager.join_paths(manager.storage_path, depot))",
            "            # Subdirectory paths",
            "            for subdir in subdirs:",
            "                setattr(depot_container, subdir, manager.join_paths(manager.storage_path, depot, subdir))",
            "            setattr(self, depot, depot_container)",
            "",
            "if TYPE_CHECKING:",
            "    fs_registry: FsRegistry",
        ]
        
        # Add Layout-named attributes to TYPE_CHECKING
        for layout_name in s3_layouts:
            lines.append(f"    {layout_name}: LayoutView")

        # 1. Collect everything for TYPE_CHECKING and __all__
        all_exports = ["get_fs_instance", "fs_registry"]
        used_layouts = set()
        
        for fs in filesystems:
            lines.append(f"    {fs['id']}: StorageManager")
            all_exports.append(fs["id"])
            if "layout" in fs:
                lays = fs["layout"]
                if isinstance(lays, str):
                    used_layouts.add(lays)
                elif isinstance(lays, list):
                    used_layouts.update(lays)

        # Include Layout Names themselves in exports
        for layout_name in s3_layouts:
            all_exports.append(layout_name)

        # Include depots and subdirectories ONLY from USED layouts
        for layout_name in sorted(list(used_layouts)):
            if layout_name in s3_layouts:
                layout_def = s3_layouts[layout_name]
                for depot, subdirs in layout_def.items():
                    lines.append(f"    {depot}: Any")
                    all_exports.append(depot)
                    for subdir in subdirs:
                        alias = f"{subdir}_storage"
                        lines.append(f"    {alias}: Any")
                        all_exports.append(alias)

        all_exports = sorted(list(set(all_exports)))

        lines.extend([
            "",
            "# Registry of S3 storage layout definitions",
            "s3_layouts = {",
        ])

        # Add S3 layouts
        for layout_name, layout_def in s3_layouts.items():
            lines.append(f'    "{layout_name}": {repr(layout_def)},')
        lines.append("}")
        lines.append("")

        # Cache
        lines.append("# Cache for lazy-loaded objects")
        lines.append("_cache = {}")
        lines.append("")

        # Setup function
        lines.append("def _setup():")
        lines.append('    """')
        lines.append("    Lazy initialization of storage configurations.")
        lines.append('    """')
        lines.append('    if "fs_registry" in _cache:')
        lines.append("        return")
        lines.append("")

        # Import settings
        lines.append("    # Import Settings Lazily")
        lines.append("    from ..settings import (")
        
        manager_import_list = []
        for fs in filesystems:
            ref_prefix = fs["connection_ref"].replace("_conf", "")
            class_name = EnvGenerator._to_class_name(ref_prefix)
            manager_import_list.append(class_name)
        
        # Deduplicate and format
        manager_import_list = sorted(list(set(manager_import_list)))
        for i in range(0, len(manager_import_list), 3):
            chunk = manager_import_list[i : i + 3]
            comma = "," if i + 3 < len(manager_import_list) else ""
            lines.append(f"        {', '.join(chunk)}{comma}")
        lines.append("    )")
        lines.append("")

        # Initialize Managers and Registry
        lines.append("    # Initialize Registry")
        lines.append("    fs_registry = FsRegistry()")
        
        lines.append("    # Initialize Storage Managers (Factory Pattern)")
        for fs in filesystems:
            fs_id = fs["id"]
            ref_prefix = fs["connection_ref"].replace("_conf", "")
            class_name = EnvGenerator._to_class_name(ref_prefix)
            
            # Handle Single vs Multiple Layouts
            if "layout" in fs:
                lays = fs["layout"]
                if isinstance(lays, list):
                    # Deep Merge multiple layouts (combine subdirectory lists)
                    layout_ref = f"_merged_layout_{fs_id}"
                    lines.append(f'    {layout_ref} = {{}}')
                    lines.append(f'    for _lay_name in {repr(lays)}:')
                    lines.append(f'        _lay_def = s3_layouts.get(_lay_name, {{}})')
                    lines.append(f'        for _depot, _subdirs in _lay_def.items():')
                    lines.append(f'            if _depot not in {layout_ref}:')
                    lines.append(f'                {layout_ref}[_depot] = []')
                    lines.append(f'            # Combine and deduplicate subdirectory lists')
                    lines.append(f'            {layout_ref}[_depot] = sorted(list(set({layout_ref}[_depot] + _subdirs)))')
                else:
                    layout_ref = f's3_layouts.get({repr(lays)})'
            else:
                layout_ref = "None"
            
            lines.append(f'    {fs_id}_manager = create_storage_manager(')
            lines.append(f'        {class_name}, {layout_ref}, clear_existing=False, write_mode="read-only"')
            lines.append(f'    )')
            lines.append(f'    _cache[{repr(fs_id)}] = {fs_id}_manager')
            
            # Instantiate Layout Views for this filesystem
            if "layout" in fs:
                lays = fs["layout"]
                if isinstance(lays, str):
                    lays = [lays]
                for lay_name in lays:
                    # layout_name -> _cache['layout_name'] = LayoutView(manager, definition)
                    lines.append(f'    if {repr(lay_name)} not in _cache:')
                    lines.append(f'        _cache[{repr(lay_name)}] = LayoutView({fs_id}_manager, s3_layouts[{repr(lay_name)}])')

            # Register in registry
            registry_name = fs.get("registry_name", fs["id"])
            lines.append(f"    fs_registry.register({repr(registry_name)}, _cache[{repr(fs['id'])}])")
            lines.append("")

        # Flatten paths for easy import (mobile_storage, products_storage, etc.)
        lines.append("    # Flatten paths for easy import (mobile_storage, products_storage, etc.)")
        lines.append("    fs_registry.flatten(_cache)")
        lines.append("")
        
        lines.append('    _cache["fs_registry"] = fs_registry')
        lines.append("")

        # __getattr__ and helper
        lines.append("def __getattr__(name: str) -> Any:")
        lines.append('    """')
        lines.append("    Lazy load storage objects.")
        lines.append('    """')
        lines.append("    _setup()")
        lines.append("    if name in _cache:")
        lines.append("        return _cache[name]")
        lines.append(r'    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")')
        lines.append("")
        lines.append("def get_fs_instance(which: str = 'etl_source') -> Any:")
        lines.append('    """')
        lines.append("    Retrieve a filesystem instance from a registered storage manager.")
        lines.append('    """')
        lines.append("    _setup()")
        return "\n".join(lines) + '\n    return _cache["fs_registry"].get_fs_instance(which)\n\n__all__ = ' + repr(all_exports) + "\n"

    @staticmethod
    def generate_osmnx_code(discovery_data: Dict[str, Any], project_name: str = "solutions") -> str:
        """
        Generates the lazy-loading OSMnx configuration for conf/osmnx/__init__.py
        """
        project_name = project_name.replace("-", "_")
        osmnx_meta = discovery_data.get("osmnx", {})
        if not osmnx_meta:
            return ""

        storage_ref = osmnx_meta.get("storage_ref", "default_depot_storage")
        fs_ref = osmnx_meta.get("fs_ref", "source")

        lines = [
            "from __future__ import annotations",
            "",
            "from typing import Any, Tuple, TYPE_CHECKING",
            "from sibi_flux.osmnx_helper import GraphLoader",
            "from ..credentials import osmnx_conf",
            f"from ..storage import {storage_ref}, get_fs_instance",
            "",
            "if TYPE_CHECKING:",
            "    G: Any",
            "    g_nodes: Any",
            "    g_edges: Any",
            "    loader: GraphLoader",
            f"    {storage_ref}: Any",
            "",
            "# Cache for lazy-loaded objects",
            "_cache = {}",
            "",
            "def _get_loader():",
            "    if 'loader' not in _cache:",
            "        # Config setup",
            "        osmnx_options = osmnx_conf.copy()",
            "        # Ensure correct paths and FS are set",
            "        osmnx_options.update({",
            f"            'data_path': f'{{{storage_ref}}}/pbf_files',",
            "        })",
            "",
            "        # Derive pbf_path from pbf_url if present",
            "        if 'pbf_url' in osmnx_options and osmnx_options['pbf_url']:",
            "            from pathlib import Path",
            "            pbf_filename = Path(osmnx_options['pbf_url']).name",
            "            osmnx_options['pbf_path'] = f\"{osmnx_options['data_path']}/{pbf_filename}\"",
            "",
            "        _cache['loader'] = GraphLoader(",
            "            config=osmnx_options, ",
            f"            fs=get_fs_instance({repr(fs_ref)})",
            "        )",
            "    return _cache['loader']",
            "",
            "def ensure_initialized() -> None:",
            "    _get_loader().ensure_initialized()",
            "",
            "def get_graph_triplet() -> Tuple[Any, Any, Any]:",
            "    return _get_loader().get_graph_triplet()",
            "",
            "def __getattr__(name: str) -> Any:",
            "    _loader = _get_loader()",
            "    if name == 'G':",
            "        return _loader.G",
            "    if name == 'g_nodes':",
            "        return _loader.nodes",
            "    if name == 'g_edges':",
            "        return _loader.edges",
            "    if name == 'loader':",
            "        return _loader",
            "",
            "    raise AttributeError(f'module {__name__!r} has no attribute {name!r}')",
            "",
            f"__all__ = ['G', 'g_nodes', 'g_edges', 'ensure_initialized', 'get_graph_triplet', 'loader', {repr(storage_ref)}]",
        ]

        return "\n".join(lines)

    @staticmethod
    def _discover_routers(project_path: Path, router_module: str) -> List[Dict[str, str]]:
        """
        Discovers routers in the specified module directory.
        Looks for 'router' symbol in each .py file.
        """
        routers = []
        # Convert module to path relative to project root
        module_rel_path = router_module.replace(".", "/")
        router_dir = project_path / module_rel_path
        
        # print(f"DEBUG: project_path={project_path}, router_module={router_module}, router_dir={router_dir}")
        
        if not router_dir.exists() or not router_dir.is_dir():
            # Try to handle the case where the project_path name is repeated in the module path
            parts = router_module.split(".")
            if parts[0] == project_path.name:
                module_rel_path = "/".join(parts[1:])
                router_dir = project_path / module_rel_path
                # print(f"DEBUG RETRY: router_dir={router_dir}")

        if not router_dir.exists() or not router_dir.is_dir():
            return routers

        for entry in sorted(router_dir.iterdir()):
            if entry.is_file() and entry.suffix == ".py" and entry.stem != "__init__":
                module_name = entry.stem
                import_path = f"{router_module}.{module_name}"
                
                # We assume 'router' is the symbol name as per project pattern
                routers.append({
                    "name": module_name,
                    "import_path": import_path,
                    "symbol": "router"
                })
        
        return routers

    @staticmethod
    def generate_main_app_code(project_path: Path, discovery_data: Dict[str, Any], project_name: str = "solutions") -> str:
        """
        Generates the main FastAPI application for solutions/main.py
        """
        project_name = project_name.replace("-", "_")
        api_meta = discovery_data.get("api", {})
        if not api_meta:
            return ""

        title = api_meta.get("title", "Sibi Toolkit API")
        version = api_meta.get("version", "0.1.0")
        router_module = api_meta.get("router_module")
        enable_mcp = api_meta.get("enable_mcp", False)

        routers = []
        if router_module:
            from pathlib import Path
            routers = EnvGenerator._discover_routers(Path(project_path), router_module)
        
        # Modular Apps Discovery
        modular_apps = discovery_data.get("apps", [])
        app_routers = []
        for app_name in modular_apps:
            app_router_path = Path(project_path) / app_name / "api" / "main.py"
            if app_router_path.exists():
                app_routers.append({
                    "name": app_name,
                    "import_path": f"{app_name}.api.main",
                    "symbol": "router"
                })

        lines = [
            "import asyncio",
            "import os",
            "from contextlib import asynccontextmanager",
            "from typing import AsyncGenerator, Dict",
            "",
            "from fastapi import FastAPI, Depends, HTTPException, Security, status",
            "from fastapi.middleware.cors import CORSMiddleware",
            "from fastapi.middleware.trustedhost import TrustedHostMiddleware",
            "from fastapi.security import APIKeyHeader",
            "",
            "# Core integrations",
            "from sibi_flux.dask_cluster.client_manager import get_persistent_client, force_close_persistent_client",
            "from conf.osmnx import ensure_initialized",
            "from conf.credentials import dask_conf",
            "",
        ]

        # Add router imports
        for r in routers:
            import_path = r['import_path']
            if not import_path.startswith("."):
                import_path = f".{import_path}"
            lines.append(f"from {import_path} import {r['symbol']} as {r['name']}_router")
            
        for r in app_routers:
            lines.append(f"from {r['import_path']} import {r['symbol']} as {r['name']}_router")
        
        if enable_mcp:
            lines.append("from .mcp_server import mcp_router")
        
        lines.extend([
            "",
            "# Security Configuration",
            "API_KEY_NAME = 'access_token'",
            "api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)",
            "",
            "def get_api_key(",
            "    api_key_header: str = Security(api_key_header),",
            ") -> str:",
            "    expected_key = os.environ.get('SIBI_API_KEY', 'sibi-default-secret-key')",
            "    if api_key_header == expected_key:",
            "        return api_key_header",
            "    ",
            "    raise HTTPException(",
            "        status_code=status.HTTP_401_UNAUTHORIZED,",
            "        detail='Invalid or missing API Key',",
            "    )",
            "",
            "@asynccontextmanager",
            "async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:",
            "    # Bootstrap Dask Cluster Client",
            "    client = get_persistent_client(**dask_conf)",
            "    app.state.dask_client = client",
            "    ",
            "    print('Bootstrapping OSMnx data (with 10s timeout)...')",
            "    try:",
            "        await asyncio.wait_for(asyncio.to_thread(ensure_initialized), timeout=10.0)",
            "        print('OSMnx data ready.')",
            "    except asyncio.TimeoutError:",
            "        print('OSMnx bootstrap timed out - continuing in background...')",
            "    except Exception as e:",
            "        print(f'OSMnx bootstrap failed: {e} - continuing...')",
            "    ",
            "    yield",
            "    ",
            "    # Cleanup",
            "    force_close_persistent_client()",
            "",
            "app = FastAPI(",
            f"    title={repr(title)},",
            f"    description='API for Sibi Toolkit managed {project_name}',",
            f"    version={repr(version)},",
            "    lifespan=lifespan,",
            ")",
            "",
            "# CORS Middleware",
            "app.add_middleware(",
            "    CORSMiddleware,",
            "    allow_origins=os.environ.get('ALLOWED_ORIGINS', 'http://localhost:3000').split(','),",
            "    allow_credentials=True,",
            "    allow_methods=['GET', 'POST', 'OPTIONS'],",
            "    allow_headers=['*'],",
            ")",
            "",
            "# Trusted Host Middleware",
            "app.add_middleware(",
            "    TrustedHostMiddleware, ",
            "    allowed_hosts=os.environ.get('ALLOWED_HOSTS', '*').split(',')",
            ")",
            "",
            "# Security Headers Middleware",
            "@app.middleware('http')",
            "async def add_security_headers(request, call_next):",
            "    response = await call_next(request)",
            "    response.headers['X-Content-Type-Options'] = 'nosniff'",
            "    response.headers['X-Frame-Options'] = 'DENY'",
            "    response.headers['Strict-Transport-Security'] = 'max-age=31536000 ; includeSubDomains'",
            "    response.headers['X-XSS-Protection'] = '1; mode=block'",
            "    response.headers['Content-Security-Policy'] = \"default-src 'self'\"",
            "    return response",
            "",
            "# Apply security to routers",
            "security_dependency = [Depends(get_api_key)]",
            "",
        ])

        # Include discovered routers
        for r in routers:
            lines.append("app.include_router(")
            lines.append(f"    {r['name']}_router, prefix='/api', tags=['{r['name']}'], dependencies=security_dependency")
            lines.append(")")
            
        # Include modular app routers
        for r in app_routers:
            lines.append("app.include_router(")
            lines.append(f"    {r['name']}_router, prefix='/{r['name']}', tags=['{r['name']}'], dependencies=security_dependency")
            lines.append(")")
        
        if enable_mcp:
            lines.append("app.include_router(")
            lines.append("    mcp_router.router, prefix='/mcp', tags=['mcp'], dependencies=security_dependency")
            lines.append(")")

        lines.extend([
            "",
            "@app.get('/health')",
            "def health_check() -> Dict[str, str]:",
            "    return {'status': 'ok'}",
            ""
        ])

        return "\n".join(lines)

def __dir__():
    return __all__
