"""Auto-generate config schemas from Helm values for module configuration.

Walks the live Helm values tree, infers field types and tab assignments,
and merges with any hand-curated overrides from the Python registry.
"""

from __future__ import annotations

import re
from collections.abc import Iterator
from typing import Any

from ilum.core.config_schemas import ConfigField, ConfigSchema

# ---------------------------------------------------------------------------
# Sensitive-path filter
# ---------------------------------------------------------------------------

_SENSITIVE_RE = re.compile(r"(password|secret|token|key|credential)", re.IGNORECASE)


def _is_sensitive(path: str) -> bool:
    """Return True if *path* likely contains a secret."""
    return bool(_SENSITIVE_RE.search(path))


# ---------------------------------------------------------------------------
# Tree walker
# ---------------------------------------------------------------------------


def _walk_values(data: dict[str, Any], prefix: str = "") -> Iterator[tuple[str, Any]]:
    """Yield ``(dotted_path, scalar_value)`` for every leaf in *data*.

    Skips lists and dicts whose children contain lists (to avoid
    exposing complex structures that don't map to simple form fields).
    """
    for key, value in data.items():
        path = f"{prefix}.{key}" if prefix else key
        if isinstance(value, dict):
            yield from _walk_values(value, path)
        elif isinstance(value, list):
            continue  # skip lists — not representable as simple fields
        else:
            yield path, value


# ---------------------------------------------------------------------------
# Type inference
# ---------------------------------------------------------------------------


def _infer_field_type(value: Any) -> str:
    """Map a Python scalar to a config field type string."""
    if isinstance(value, bool):
        return "boolean"
    if isinstance(value, (int, float)):
        return "number"
    return "text"


# ---------------------------------------------------------------------------
# Tab assignment
# ---------------------------------------------------------------------------

_GENERAL_PATTERNS: tuple[str, ...] = (
    "image.",
    "nameOverride",
    "fullnameOverride",
    "replicaCount",
    "serviceAccount.",
    "enabled",
    "tag",
    "repository",
)

_RESOURCES_PATTERNS: tuple[str, ...] = ("resources.",)

_STORAGE_PATTERNS: tuple[str, ...] = (
    "persistence.",
    "storage.",
    "pvc.",
    "dataDir",
    "volumeMount",
)


def _assign_tab(path: str) -> str:
    """Assign a tab based on the dotted *path*."""
    # Check each segment and the full path
    for pattern in _RESOURCES_PATTERNS:
        if pattern in path or path.startswith(pattern.rstrip(".")):
            return "resources"
    for pattern in _STORAGE_PATTERNS:
        if pattern in path or path.startswith(pattern.rstrip(".")):
            return "storage"
    for pattern in _GENERAL_PATTERNS:
        if pattern in path or path == pattern.rstrip("."):
            return "general"
    return "advanced"


# ---------------------------------------------------------------------------
# Label generation
# ---------------------------------------------------------------------------


def _path_to_label(path: str) -> str:
    """Convert a dotted path to a human-readable label.

    ``"image.tag"`` → ``"Image Tag"``
    ``"resources.requests.memory"`` → ``"Memory Request"``
    """
    # Special cases for common resource paths
    label_map: dict[str, str] = {
        "resources.requests.memory": "Memory Request",
        "resources.requests.cpu": "CPU Request",
        "resources.limits.memory": "Memory Limit",
        "resources.limits.cpu": "CPU Limit",
        "replicaCount": "Replica Count",
        "image.tag": "Image Tag",
        "image.repository": "Image Repository",
        "image.pullPolicy": "Image Pull Policy",
        "persistence.size": "Storage Size",
        "persistence.enabled": "Persistence Enabled",
        "persistence.storageClass": "Storage Class",
        "serviceAccount.create": "Create Service Account",
        "serviceAccount.name": "Service Account Name",
    }
    if path in label_map:
        return label_map[path]

    # Generic: take last segment(s) and title-case
    parts = path.split(".")
    # Use last part, split camelCase
    last = parts[-1]
    # Split camelCase: "replicaCount" → "replica Count"
    spaced = re.sub(r"([a-z])([A-Z])", r"\1 \2", last)
    return spaced.title()


# ---------------------------------------------------------------------------
# Value extraction helper
# ---------------------------------------------------------------------------


def _get_nested(data: dict[str, Any], path: str) -> Any:
    """Retrieve a value from *data* using a dotted *path*.

    Returns ``None`` if any segment is missing.
    """
    current: Any = data
    for segment in path.split("."):
        if not isinstance(current, dict):
            return None
        current = current.get(segment)
    return current


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def generate_module_schema(
    module_name: str,
    values_key: str,
    computed_values: dict[str, Any],
    chart_defaults: dict[str, Any],
    registry_schema: ConfigSchema | None,
) -> ConfigSchema:
    """Build a comprehensive :class:`ConfigSchema` for a module.

    Parameters
    ----------
    module_name:
        Module identifier (e.g. ``"jupyter"``).
    values_key:
        Top-level key in ``values.yaml`` (e.g. ``"ilum-jupyter"``).
    computed_values:
        Full computed values from ``helm get values --all``.
    chart_defaults:
        Full chart defaults from ``helm show values``.
    registry_schema:
        Optional hand-curated schema from the Python registry.

    Returns a :class:`ConfigSchema` with fields from auto-generation merged
    with any registry overrides.
    """
    # Extract module subtrees
    module_computed = computed_values.get(values_key, {})
    module_defaults = chart_defaults.get(values_key, {})

    if not isinstance(module_computed, dict):
        module_computed = {}
    if not isinstance(module_defaults, dict):
        module_defaults = {}

    # Build registry lookup for merge
    registry_by_path: dict[str, ConfigField] = {}
    if registry_schema:
        for f in registry_schema.fields:
            registry_by_path[f.path] = f

    # Walk computed values to discover all fields
    auto_fields: dict[str, ConfigField] = {}
    source = module_computed or module_defaults
    for path, value in _walk_values(source):
        # Skip sensitive paths (registry can explicitly re-add)
        if _is_sensitive(path) and path not in registry_by_path:
            continue

        default_value = _get_nested(module_defaults, path)

        auto_fields[path] = ConfigField(
            path=path,
            label=_path_to_label(path),
            field_type=_infer_field_type(value),
            description="",
            default=default_value,
            tab=_assign_tab(path),
        )

    # Merge: registry overrides auto-generated fields
    merged: dict[str, ConfigField] = dict(auto_fields)
    for path, reg_field in registry_by_path.items():
        merged[path] = reg_field

    # Remove hidden fields
    final_fields = [f for f in merged.values() if not f.hidden]

    # Sort fields: by tab order, then by path
    tab_order = {"general": 0, "resources": 1, "storage": 2, "advanced": 3}
    final_fields.sort(key=lambda f: (tab_order.get(f.tab, 99), f.path))

    return ConfigSchema(
        module_name=module_name,
        fields=tuple(final_fields),
    )
