import re
import yaml
from pathlib import Path
from typing import Literal, Optional, Any
from collections.abc import Mapping, Sequence
from pydantic import BaseModel

# --- Schema Definitions ---


class GlobalSettings(BaseModel):
    cubes_root_path: str
    fields_module_root: str
    default_db_conn: str
    env_file: Optional[str] = ".env.linux"
    exclusions: list[str] = []


class DiscoveryRule(BaseModel):
    pattern: str
    match_type: Literal["prefix", "exact", "regex"] = "prefix"
    domain: str
    output_template: str

    # Optional: Override the DB connection for specific tables
    db_conn_override: Optional[str] = None


class GeneratorConfig(BaseModel):
    version: float
    settings: GlobalSettings
    discovery_rules: list[DiscoveryRule]


# --- Logic Engine ---


class ConfigurationEngine:
    def __init__(
        self,
        params: dict,
        rules_path: Optional[str] = None,
        context_key: Optional[str] = None,
    ):
        """
        Initializes the engine with settings parameters and a text file path for rules.
        :param context_key: Optional key (e.g. 'replica_db_conf') to load specific rules from a dict-based Rule file.
        """
        self.params = params
        self.rules_path = rules_path
        self.context_key = context_key
        self._config_cache: Optional[GeneratorConfig] = None

    @property
    def config(self) -> GeneratorConfig:
        if self._config_cache is None:
            self._config_cache = self._load_config()
        return self._config_cache

    def _load_config(self) -> GeneratorConfig:
        # 1. Build GlobalSettings from params
        settings_data = {
            "cubes_root_path": self._resolve_cubes_root(),
            "fields_module_root": self.params.get("generation", {}).get(
                "fields_subpackage", "fields"
            ),
            "default_db_conn": "",  # Not easily resolved from list here, handled by rule/loop context usually
            "env_file": self.params.get("defaults", {}).get("env_file", ".env.linux"),
            "exclusions": self.params.get("generation", {}).get("exclusions", []),
        }

        # 2. Load Rules
        rules = []
        if self.rules_path and Path(self.rules_path).exists():
            with open(self.rules_path, "r") as f:
                raw_rules = yaml.safe_load(f)

                if isinstance(raw_rules, list):
                    # Legacy/Global List Mode
                    rules = raw_rules
                elif isinstance(raw_rules, dict):
                    # Scoped Dictionary Mode
                    if self.context_key and self.context_key in raw_rules:
                        rules = raw_rules[self.context_key]
                    elif "discovery_rules" in raw_rules:
                        # Fallback to key 'discovery_rules' if exists
                        rules = raw_rules["discovery_rules"]
                    else:
                        # Context not found and no default key
                        rules = []

        # Sort Rules by Specificity (Length Descending)
        # This prevents short prefixes (e.g. 'data_') from shadowing longer ones or exact matches if they overlap
        # though usually exact matches don't overlap with delimiter-based prefixes.
        # But for 'user_' vs 'use_', 'user_' (len 5) should come before 'use_' (len 4).
        sorted_rules = sorted(rules, key=lambda x: len(x.get("pattern", "")), reverse=True)

        return GeneratorConfig(
            version=1.2,
            settings=GlobalSettings(**settings_data),
            discovery_rules=[DiscoveryRule(**r) for r in sorted_rules],
        )

    def _resolve_cubes_root(self) -> str:
        # Simplified: target_datacubes_folder is a string (checked by Pydantic/CLI)
        # Deep Access: paths.target.datacubes_dir
        return self.params.get("paths", {}).get("target", {}).get("datacubes_dir", "")

    def resolve_table(
        self, table_name: str, db_config: Optional[dict] = None
    ) -> Optional[dict[str, Any]]:
        """
        Iterates through rules to find a match.
        Returns a dict with resolved paths and metadata.
        """
        # 1. Check Global Exclusions
        for pattern in self.config.settings.exclusions:
            # We assume exclusions are regex patterns for flexibility
            if re.search(pattern, table_name):
                return None

        # 2. Check Rules
        for rule in self.config.discovery_rules:
            match_data = self._check_match(rule, table_name)
            if match_data is not None:
                return self._build_result(rule, table_name, match_data, db_config)

        return None  # No match found

    def _check_match(
        self, rule: DiscoveryRule, table_name: str
    ) -> Optional[dict[str, str]]:
        """
        Checks if rule matches table_name.
        Returns capturing groups dict if match (empty dict for non-regex matches), None otherwise.
        """
        if rule.match_type == "exact":
            if table_name == rule.pattern:
                return {}

        elif rule.match_type == "prefix":
            if table_name.startswith(rule.pattern):
                return {}

        elif rule.match_type == "regex":
            m = re.search(rule.pattern, table_name)
            if m:
                # Combine numbered groups (as strings) and named groups
                groups = {str(i + 1): g for i, g in enumerate(m.groups())}
                groups.update(m.groupdict())
                return groups

        return None

    def _build_result(
        self,
        rule: DiscoveryRule,
        table_name: str,
        groups: dict[str, str],
        db_config: Optional[dict] = None,
    ) -> dict[str, Any]:
        """Constructs the final paths based on global settings + rule + captured groups"""

        # 1. Resolve Output Subpath with Template Substitution
        # Supports {feature}, {1}, etc.
        try:
            subpath = rule.output_template.format(**groups)
        except KeyError:
            # Fallback for safe substitution if keys missing? Or let it raise?
            # Let's try to be safe but warn? For now, strict formatting.
            subpath = rule.output_template

        # 2. Resolve Physical Path
        root = Path(self.config.settings.cubes_root_path)
        full_output_path = str(root / subpath)

        # 3. Resolve Field Map Path (Smart Guessing)
        # e.g., solutions.dataobjects.gencubes.istmo360n.fields.logistics.asm_tracking_X.field_map

        # Get base prefix (e.g., solutions.dataobjects.gencubes)
        # Get base prefix derived from target_datacubes_folder
        # e.g. solutions/dataobjects/gencubes/
        folder_prefix = (
            self.params.get("paths", {})
            .get("target", {})
            .get("datacubes_dir", "solutions/dataobjects/gencubes/")
        )
        base_pkg = folder_prefix.replace("/", ".").strip(".")
        if base_pkg.startswith("."):
            base_pkg = base_pkg[1:]

        # Get DB Domain
        db_domain = "common"
        if db_config:
            db_domain = db_config.get("db_domain", "common")

        field_map_module = (
            f"{base_pkg}.{db_domain}."
            f"{self.config.settings.fields_module_root}."
            f"{rule.domain}.{table_name}.field_map"
        )

        # 4. Determine Connection Object
        # Priority: Rule Override > DB Config (Active Loop) > Default Global
        conn_obj = rule.db_conn_override
        if not conn_obj and db_config:
            conn_obj = db_config.get("connection_obj") or db_config.get("connection_ref")
        if not conn_obj:
            conn_obj = self.config.settings.default_db_conn

        # 5. Generate Default Class Name
        # e.g. asm_empleados -> AsmEmpleadosDc
        class_name = "".join(x.title() for x in table_name.split("_")) + "Dc"

        return {
            "domain": rule.domain,
            "path": full_output_path,  # 'path' key is expected by generator
            "save_to_path": full_output_path,
            "field_map": field_map_module,  # 'field_map' template logic relies on discovery result often
            "connection_obj": conn_obj,
            "class_name": class_name,
            # Pass template through for downstream consumers (e.g. whitelist generator)
            "output_template": rule.output_template,
        }


# --- Module Level Interface ---

_ENGINE = None


def get_engine(params: Optional[dict] = None) -> ConfigurationEngine:
    global _ENGINE
    if _ENGINE is None:
        # Fallback empty initialization if accessed globally without context
        _ENGINE = ConfigurationEngine(params or {})
    return _ENGINE


def match_rule(table_name: str) -> Optional[dict[str, str]]:
    """
    Finds the first matching rule for a table name.
    """
    engine = get_engine()  # Warning: Relies on global state
    return engine.resolve_table(table_name)
