"""
Converter for deprecated raiconfig.toml to RAI Config format.
"""

from __future__ import annotations

from typing import Any

from .raiconfig_models import RAIConfigFile, RAIConfigSnowflakeProfile, RAIConfigLocalProfile


# Fields that are no longer configurable via YAML and are managed via the CLI only.
# These are excluded from profile.convert() and not written to raiconfig.yaml during migration.
CLI_MANAGED_FIELDS: frozenset[str] = frozenset({"auto_suspend_mins", "await_storage_vacuum"})

# Central field mapping configuration
# Format: "old_field_name": ("new_section", "new_field_name")
# where section=None means top-level field
FIELD_MAPPINGS = {
    # Top-level fields (stay at top level)
    "use_graph_index": (None, "use_graph_index"),
    "use_direct_access": (None, "direct_access"),
    "enable_otel_handler": (None, "enable_otel_handler"),
    "reuse_model": ("model", "reuse_model"),
    "skip_invalid_data": (None, "skip_invalid_data"),
    # Jobs fields (renamed from transaction terminology in v1)
    "print_txn_progress": ("jobs", "print_progress"),
    "print_txn_progress_internal": ("jobs", "print_progress_internal"),
    "enable_guard_rails": ("jobs", "enable_guard_rails"),
    # Engine fields (mapped to reasoners.logic - engine is actually the reasoner instance)
    "engine": ("reasoners.logic", "name"),
    "engine_size": ("reasoners.logic", "size"),
    # Note: auto_suspend_mins and await_storage_vacuum are intentionally excluded —
    # they are managed via CLI only (rai reasoners:alter / rai reasoners:create).
    # Data fields
    "wait_for_stream_sync": ("data", "wait_for_stream_sync"),
    "ensure_change_tracking": ("data", "ensure_change_tracking"),
    "data_freshness_mins": ("data", "data_freshness_mins"),
    "query_timeout_mins": ("data", "query_timeout_mins"),
    "download_url_type": ("data", "download_url_type"),
    # Debug fields
    "debug": ("debug", "enabled"),  # Note: maps to "enabled"!
    "show_full_traces": ("debug", "show_full_traces"),
    # Compiler fields
    "use_monotype_operators": ("compiler", "use_monotype_operators"),
    "show_corerel_errors": ("compiler", "show_corerel_errors"),
    "dry_run": ("compiler", "dry_run"),
    "use_value_types": ("compiler", "use_value_types"),
    "debug_hidden_keys": ("compiler", "debug_hidden_keys"),
    "wide_outputs": ("compiler", "wide_outputs"),
    "strict": ("compiler", "strict"),
    "safe_recursion": ("compiler", "safe_recursion"),
    "use_inlined_intermediates": ("compiler", "use_inlined_intermediates"),
    "inline_value_maps": ("compiler", "inline_value_maps"),
    "inline_entity_maps": ("compiler", "inline_entity_maps"),
    # Model fields
    "keep": ("model", "keep"),
    "isolated": ("model", "isolated"),
    "nowait_durable": ("model", "nowait_durable"),
}


def _dump_explicit_values(model: Any, *, excluded_fields: set[str] | None = None) -> dict[str, Any]:
    """Dump explicitly-set model fields while preserving allowed legacy extras."""
    excluded = excluded_fields or set()
    explicit_fields = set(getattr(model, "model_fields_set", set())) - excluded
    result = model.model_dump(include=explicit_fields, exclude_none=True)
    extras = getattr(model, "model_extra", None) or {}
    for key, value in extras.items():
        if key in excluded or value is None:
            continue
        result[key] = value
    return result

# Connection fields (stay in connection dict)
CONNECTION_FIELDS = {
    "account",
    "warehouse",
    "user",
    "password",
    "role",
    "database",
    "schema",
    "authenticator",
    "rai_app_name",
    "passcode",
    "private_key_path",
    "private_key_passphrase",
    "private_key_file",
    "private_key_file_pwd",
    "token",
    "token_file_path",
}

# Compatibility aliases for JWT connection fields.
CONNECTION_FIELD_MAPPINGS = {
    "private_key_file": "private_key_path",
    "private_key_file_pwd": "private_key_passphrase",
}


def map_fields_to_nested_structure(merged: dict[str, Any]) -> dict[str, Any]:
    """
    Map flat config fields to nested structure using FIELD_MAPPINGS.

    This replaces all the manual if statements!

    Supports multi-level nesting: section can be "reasoners.logic" to create
    nested dicts like result["reasoners"]["logic"][field].

    Args:
        merged: Flat dictionary with all config fields

    Returns:
        Nested dictionary structured according to new config format
    """
    result = {}
    nested_sections = {}  # Collect fields for each section

    for old_field, (section, new_field) in FIELD_MAPPINGS.items():
        if old_field not in merged or merged[old_field] is None:
            continue  # Skip missing or None values

        value = merged[old_field]

        if section is None:
            # Top-level field
            result[new_field] = value
        else:
            # Nested section field (supports multi-level like "reasoners.logic")
            if section not in nested_sections:
                nested_sections[section] = {}
            nested_sections[section][new_field] = value

    # Prefer the canonical field name when both legacy and canonical aliases are present.
    if "direct_access" in merged and merged["direct_access"] is not None:
        result["direct_access"] = merged["direct_access"]

    # Add nested sections to result, handling multi-level paths
    for section, fields in nested_sections.items():
        if not fields:  # Skip empty sections
            continue

        # Handle multi-level paths like "reasoners.logic"
        path_parts = section.split(".")

        # Navigate/create nested structure
        current = result
        for part in path_parts[:-1]:
            if part not in current:
                current[part] = {}
            current = current[part]

        # Set or merge the final level
        final_key = path_parts[-1]
        if final_key in current:
            # Merge with existing dict
            current[final_key].update(fields)
        else:
            current[final_key] = fields

    return result


def merge_profile_with_toplevel(
    config_file: RAIConfigFile, profile: RAIConfigSnowflakeProfile | RAIConfigLocalProfile
) -> dict[str, Any]:
    """
    Merge profile over top-level config (profile takes precedence).

    This matches the v0 behavior where profile-specific settings override
    global defaults.
    """
    # Start with top-level config — only include fields explicitly set in the TOML,
    # not fields that are merely carrying Pydantic defaults.
    merged = _dump_explicit_values(config_file, excluded_fields={"profile", "active_profile"})

    # Overlay profile fields (they take precedence) — same: only explicitly set fields.
    profile_dict = _dump_explicit_values(profile)

    # Merge profile into config (profile overrides top-level)
    merged.update(profile_dict)

    return merged


def deep_merge(dst: dict[str, Any], src: dict[str, Any]) -> dict[str, Any]:
    """Deep-merge `src` into `dst` (src wins), returning `dst` for convenience."""
    for k, v in src.items():
        if isinstance(v, dict) and isinstance(dst.get(k), dict):
            deep_merge(dst[k], v)  # type: ignore[arg-type]
        else:
            dst[k] = v
    return dst


def _reject_nested_direct_access_overrides(merged: dict[str, Any]) -> None:
    """Reject deprecated TOML-only per-service direct_access overrides.

    The runtime config now accepts only a single top-level ``direct_access`` flag.
    If we silently carry nested ``jobs.direct_access`` / ``reasoners.direct_access``
    through conversion, validation later ignores them and can accidentally fall
    back to the top-level default of ``False``.
    """
    for section_name in ("jobs", "reasoners"):
        section = merged.get(section_name)
        if not isinstance(section, dict):
            continue
        if "direct_access" not in section:
            continue
        raise ValueError(
            f"Deprecated nested '{section_name}.direct_access' is not supported in raiconfig.toml. "
            "Use the top-level 'direct_access' field instead."
        )


def convert_raiconfig_to_rai(rai_config: dict[str, Any], profile_name: str | None = None) -> dict[str, Any]:
    """
    Convert deprecated raiconfig.toml to new RAI Config format.

    Flow:
    1. Validate with RAIConfigFile model
    2. Select active profile (parameter > active_profile > single profile)
    3. Merge profile over top-level config (profile takes precedence)
    4. Extract connection from profile
    5. Map flat fields to nested structure

    Args:
        rai_config: Parsed TOML config dict
        profile_name: Profile name to use (overrides active_profile)

    Returns:
        Dict in new config format ready for RAIConfig model
    """
    config_file = RAIConfigFile(**rai_config)

    # Profile selection logic
    if not profile_name:
        profile_name = config_file.active_profile

    # Supported platforms
    supported_platforms = {"snowflake", "local"}

    # Filter to supported profiles (Snowflake and Local)
    supported_profiles = {
        name: data
        for name, data in config_file.profile.items()
        if data.get("platform", "snowflake") in supported_platforms
    }

    if not profile_name and len(supported_profiles) == 1:
        # Auto-select single supported profile
        profile_name = next(iter(supported_profiles.keys()))

    if not profile_name:
        if not supported_profiles:
            raise ValueError(
                "No supported profiles found in raiconfig.toml. "
                "Supported platforms: snowflake, local. "
                "Please add a supported profile or migrate to raiconfig.yaml."
            )
        raise ValueError(
            f"No profile specified. Either set 'active_profile' in config, "
            f"provide profile_name parameter, or ensure only one supported profile exists. "
            f"Available profiles: {list(supported_profiles.keys())}"
        )

    if profile_name not in config_file.profile:
        available = list(config_file.profile.keys())
        raise ValueError(f"Profile '{profile_name}' not found. Available profiles: {available}")

    # Get profile data
    profile_data = config_file.profile[profile_name]
    platform = profile_data.get("platform", "snowflake")

    # Validate and convert based on platform
    if platform == "local":
        profile = RAIConfigLocalProfile(**profile_data)
        connection = profile.convert()
        connection_name = "local"
    elif platform == "snowflake":
        profile = RAIConfigSnowflakeProfile(**profile_data)
        connection = profile.convert()
        connection_name = "snowflake"
    else:
        raise ValueError(
            f"Profile '{profile_name}' uses platform '{platform}'. "
            f"Supported platforms: snowflake, local. "
            f"Please use a supported profile or migrate to raiconfig.yaml."
        )

    # Merge profile with top-level config
    merged = merge_profile_with_toplevel(config_file, profile)
    _reject_nested_direct_access_overrides(merged)

    # Build base result
    result = {
        "connections": {connection_name: connection},
        "default_connection": connection_name,
    }

    # Map all config fields automatically using FIELD_MAPPINGS
    mapped_fields = map_fields_to_nested_structure(merged)
    result.update(mapped_fields)
    # Support nested [reasoners.*] sections in deprecated TOML (top-level or profile).
    # These should override legacy flat fields (engine_size, auto_suspend_mins, etc.).
    if "reasoners" in merged and isinstance(merged["reasoners"], dict):
        if "reasoners" not in result or not isinstance(result.get("reasoners"), dict):
            result["reasoners"] = {}
        deep_merge(result["reasoners"], merged["reasoners"])  # type: ignore[arg-type]

    # Handle nested [compiler] section if present
    if "compiler" in merged and isinstance(merged["compiler"], dict):
        if "compiler" not in result:
            result["compiler"] = {}
        result["compiler"].update(merged["compiler"])

    # Handle nested [jobs] section if present.
    if "jobs" in merged and isinstance(merged["jobs"], dict):
        if "jobs" not in result:
            result["jobs"] = {}
        result["jobs"].update(merged["jobs"])

    # Transform old reasoner config to new reasoners.logic structure
    if "reasoner" in merged and merged["reasoner"]:
        # Old format: reasoner.rule.* → New format: reasoners.logic.*
        logic_config: dict[str, Any] = {}
        reasoner = merged["reasoner"]

        # Flatten rule section into logic config
        if "rule" in reasoner:
            logic_config.update(reasoner["rule"])

        # Merge without clobbering any existing `reasoners` config.
        if "reasoners" not in result or not isinstance(result.get("reasoners"), dict):
            result["reasoners"] = {}
        r = result["reasoners"]
        if not isinstance(r, dict):
            r = {}
            result["reasoners"] = r
        logic = r.get("logic")
        if not isinstance(logic, dict):
            logic = {}
            r["logic"] = logic
        # Prefer explicitly provided `reasoners.logic.*` over legacy `[reasoner.rule]`.
        for k, v in logic_config.items():
            logic.setdefault(k, v)

    return result
