"""Module management endpoints."""

from __future__ import annotations

import os
from typing import Annotated, Any

from fastapi import APIRouter, Depends, HTTPException, Query

from ilum.api.chart_cache import get_chart_defaults
from ilum.api.deps import get_manager
from ilum.api.models import (
    ConfigFieldResponse,
    ConfigSchemaResponse,
    ConfigTabResponse,
    EnableModuleRequest,
    ModuleConfigUpdateRequest,
    ModuleDetail,
    ModuleLogsResponse,
    ModuleSummary,
    ModuleValuesResponse,
    OperationCreated,
    PodInfo,
)
from ilum.api.operations import OperationStore, get_operation_store
from ilum.api.routers.release import ensure_release_not_stuck
from ilum.core.config_schemas import get_config_schema
from ilum.core.modules import ModuleCategory, extract_module_version
from ilum.core.release import ReleaseManager
from ilum.core.schema_generator import generate_module_schema

router = APIRouter(prefix="/modules", tags=["modules"])


def _detect_enabled_and_values(
    mgr: ReleaseManager,
) -> tuple[set[str], dict[str, Any]]:
    """Return enabled module names and the computed values dict."""
    release = os.environ.get("ILUM_RELEASE_NAME", "ilum")
    try:
        computed = mgr.fetch_computed_values(release)
        return set(mgr.resolver.detect_enabled_modules(computed)), computed
    except Exception:
        return set(), {}


def _determine_status(
    name: str,
    enabled: bool,
    store: OperationStore,
    pods: list[PodInfo] | None = None,
) -> str:
    """Derive a human-friendly status for a module.

    Priority: active operation > pod health > enabled flag.
    """
    # Check active operations first
    for op in store.list_all():
        if name in op.modules and op.status in (
            "pending",
            "running",
            "awaiting_readiness",
        ):
            if op.operation == "enable":
                return "enabling"
            if op.operation == "disable":
                return "disabling"
            if op.operation == "restart":
                return "restarting"

    if not enabled:
        return "disabled"

    # If enabled, check pod health (best-effort)
    if pods is not None and len(pods) > 0:
        if all(p.ready for p in pods):
            return "enabled"
        return "error"

    # No pods to check — trust the enabled flag
    return "enabled"


def _get_chart_version(mgr: ReleaseManager) -> str:
    """Best-effort chart version from the release."""
    release = os.environ.get("ILUM_RELEASE_NAME", "ilum")
    try:
        info = mgr.get_release_info(release)
        return info.chart_version
    except Exception:
        return ""


@router.get("", response_model=list[ModuleSummary])
async def list_modules(
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
    store: Annotated[OperationStore, Depends(get_operation_store)],
    category: str | None = None,
    enabled: bool | None = None,
) -> list[ModuleSummary]:
    """List all modules with optional filtering."""
    if category is not None:
        try:
            cat = ModuleCategory(category)
        except ValueError:
            raise HTTPException(400, f"Unknown category: {category!r}") from None
        modules = mgr.resolver.by_category(cat)
    else:
        modules = mgr.resolver.all_modules()

    enabled_set, computed = _detect_enabled_and_values(mgr)
    chart_ver = _get_chart_version(mgr)

    results = [
        ModuleSummary(
            name=m.name,
            description=m.description,
            category=m.category.value,
            enabled=m.name in enabled_set,
            default_enabled=m.default_enabled,
            required=m.required,
            status=_determine_status(m.name, m.name in enabled_set, store),
            console_path=m.console_path,
            version=extract_module_version(computed.get(m.values_key, {})),
            chart_version=chart_ver,
        )
        for m in modules
    ]

    if enabled is not None:
        results = [r for r in results if r.enabled == enabled]

    return results


@router.get("/{name}", response_model=ModuleDetail)
async def get_module(
    name: str,
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
    store: Annotated[OperationStore, Depends(get_operation_store)],
) -> ModuleDetail:
    """Get module detail with live pod health."""
    mod = mgr.resolver.get(name)  # raises ModuleError → 400
    enabled_set, computed = _detect_enabled_and_values(mgr)

    # Fetch pod health if module has a pod_label
    pods: list[PodInfo] = []
    if mod.pod_label:
        namespace = os.environ.get("ILUM_NAMESPACE", mgr.helm.namespace)
        try:
            pod_statuses = mgr.k8s.list_pods_by_label(namespace, mod.pod_label)
            pods = [
                PodInfo(
                    name=p.name,
                    phase=p.phase,
                    ready=p.ready,
                    restart_count=p.restart_count,
                )
                for p in pod_statuses
                # Skip completed/failed Job pods (migrations, init jobs)
                if p.phase not in ("Succeeded", "Failed")
            ]
        except Exception:
            pass  # Pod info is best-effort

    is_enabled = mod.name in enabled_set
    return ModuleDetail(
        name=mod.name,
        description=mod.description,
        category=mod.category.value,
        enabled=is_enabled,
        default_enabled=mod.default_enabled,
        required=mod.required,
        status=_determine_status(mod.name, is_enabled, store, pods),
        console_path=mod.console_path,
        version=extract_module_version(computed.get(mod.values_key, {})),
        chart_version=_get_chart_version(mgr),
        requires=list(mod.requires),
        conflicts_with=list(mod.conflicts_with),
        chart_condition=mod.chart_condition,
        values_key=mod.values_key,
        pods=pods,
    )


@router.post("/{name}/enable", response_model=OperationCreated, status_code=202)
async def enable_module(
    name: str,
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
    store: Annotated[OperationStore, Depends(get_operation_store)],
    body: EnableModuleRequest | None = None,
) -> OperationCreated:
    """Enable a module (async operation via K8s Job)."""
    from ilum.api.job_runner import build_helm_job, check_no_active_helm_operation

    mgr.resolver.get(name)

    release = os.environ.get("ILUM_RELEASE_NAME", "ilum")
    namespace = os.environ.get("ILUM_NAMESPACE", "default")
    chart = os.environ.get("ILUM_CHART_REF", "ilum/ilum")
    user_flags = body.set_flags if body else []

    check_no_active_helm_operation(store, mgr.k8s, namespace)
    ensure_release_not_stuck(mgr)

    # CRDs stay in-process (quick, idempotent)
    mgr.ensure_module_crds([name])

    # Validate conflicts and collect warnings
    warnings: list[str] = []
    auto_resolved: list[str] = []
    try:
        computed = mgr.fetch_computed_values(release)
        enabled_set = set(mgr.resolver.detect_enabled_modules(computed))
    except Exception:
        enabled_set = set()

    # Resolve transitive dependencies for this module
    all_flags = mgr.resolver.resolve_enables(name)
    for mod in mgr.resolver.all_modules():
        for flag in mod.enable_flags:
            if flag in all_flags and mod.name != name and mod.name not in enabled_set:
                auto_resolved.append(mod.name)

    # Check for conflicts in the resulting module set
    projected = enabled_set | {name} | set(auto_resolved)
    conflict_warnings = mgr.resolver.validate_combination(frozenset(projected))
    warnings.extend(conflict_warnings)

    # Planning stays in-process (read-only)
    plan = mgr.plan_enable(release=release, chart=chart, module_names=[name], set_flags=user_flags)

    # Pin chart version to avoid accidental upgrades after helm repo update
    version = plan.chart_version or _get_chart_version(mgr)

    op_id = store.create(operation="enable", modules=[name])
    job = build_helm_job(
        op_id=op_id,
        operation="enable",
        release=release,
        namespace=namespace,
        chart_ref=plan.chart,
        set_flags=plan.set_flags,
        modules=[name],
        version=version,
        reset_defaults=plan.reset_defaults,
    )
    mgr.k8s.create_job(namespace, job)

    op = store.get(op_id)
    if op:
        op.job_name = f"ilum-helm-{op_id}"
        op.status = "running"

    return OperationCreated(
        id=op_id,
        status="running",
        message=f"Enabling module '{name}'",
        warnings=warnings,
        auto_resolved=auto_resolved,
    )


@router.post("/{name}/disable", response_model=OperationCreated, status_code=202)
async def disable_module(
    name: str,
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
    store: Annotated[OperationStore, Depends(get_operation_store)],
) -> OperationCreated:
    """Disable a module (async operation via K8s Job)."""
    from ilum.api.job_runner import build_helm_job, check_no_active_helm_operation

    mod = mgr.resolver.get(name)
    if mod.required:
        raise HTTPException(400, f"Module '{name}' is required and cannot be disabled.")

    release = os.environ.get("ILUM_RELEASE_NAME", "ilum")
    namespace = os.environ.get("ILUM_NAMESPACE", "default")
    chart = os.environ.get("ILUM_CHART_REF", "ilum/ilum")

    check_no_active_helm_operation(store, mgr.k8s, namespace)
    ensure_release_not_stuck(mgr)

    # Planning stays in-process (read-only)
    computed = mgr.fetch_computed_values(release)
    active = frozenset(mgr.resolver.detect_enabled_modules(computed))

    # Check for modules that depend on the one being disabled
    warnings: list[str] = []
    for other_name in active:
        if other_name == name:
            continue
        other = mgr.resolver.get(other_name)
        if name in other.requires:
            warnings.append(f"Module '{other_name}' depends on '{name}' and may stop working")

    plan = mgr.plan_disable(
        release=release, chart=chart, module_names=[name], active_modules=active
    )

    # Pin chart version to avoid accidental upgrades after helm repo update
    version = plan.chart_version or _get_chart_version(mgr)

    op_id = store.create(operation="disable", modules=[name])
    job = build_helm_job(
        op_id=op_id,
        operation="disable",
        release=release,
        namespace=namespace,
        chart_ref=plan.chart,
        set_flags=plan.set_flags,
        modules=[name],
        version=version,
        reset_defaults=plan.reset_defaults,
    )
    mgr.k8s.create_job(namespace, job)

    op = store.get(op_id)
    if op:
        op.job_name = f"ilum-helm-{op_id}"
        op.status = "running"

    return OperationCreated(
        id=op_id,
        status="running",
        message=f"Disabling module '{name}'",
        warnings=warnings,
    )


@router.get("/{name}/values", response_model=ModuleValuesResponse)
async def get_module_values(
    name: str,
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
) -> ModuleValuesResponse:
    """Return the current Helm values for a specific module."""
    mod = mgr.resolver.get(name)
    release = os.environ.get("ILUM_RELEASE_NAME", "ilum")
    computed = mgr.fetch_computed_values(release)
    module_values = computed.get(mod.values_key, {})
    return ModuleValuesResponse(values_key=mod.values_key, values=module_values)


@router.get("/{name}/config-schema", response_model=ConfigSchemaResponse)
async def get_module_config_schema(
    name: str,
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
) -> ConfigSchemaResponse:
    """Return the config schema for a module with tabs."""
    mod = mgr.resolver.get(name)  # validates module exists → 400 if unknown

    release = os.environ.get("ILUM_RELEASE_NAME", "ilum")
    chart_ref = os.environ.get("ILUM_CHART_REF", "ilum/ilum")

    # Fetch live computed values and chart defaults
    try:
        computed = mgr.fetch_computed_values(release)
    except Exception:
        computed = {}

    defaults = get_chart_defaults(mgr.helm, chart_ref)
    registry_schema = get_config_schema(name)

    schema = generate_module_schema(
        module_name=name,
        values_key=mod.values_key,
        computed_values=computed,
        chart_defaults=defaults,
        registry_schema=registry_schema,
    )

    # Extract current values for populating current_value
    module_computed = computed.get(mod.values_key, {})
    if not isinstance(module_computed, dict):
        module_computed = {}

    # Group fields by tab
    tab_order = ("general", "resources", "storage", "advanced")
    tab_labels = {
        "general": "General",
        "resources": "Resources",
        "storage": "Storage",
        "advanced": "Advanced",
    }
    tabs_map: dict[str, list[ConfigFieldResponse]] = {t: [] for t in tab_order}

    for f in schema.fields:
        current_value = _get_nested_value(module_computed, f.path)
        field_resp = ConfigFieldResponse(
            path=f.path,
            label=f.label,
            field_type=f.field_type,
            description=f.description,
            tab=f.tab,
            default=f.default,
            current_value=current_value,
            min_value=f.min_value,
            max_value=f.max_value,
            options=list(f.options),
        )
        bucket = f.tab if f.tab in tabs_map else "advanced"
        tabs_map[bucket].append(field_resp)

    tabs = [
        ConfigTabResponse(name=t, label=tab_labels[t], fields=tabs_map[t])
        for t in tab_order
        if tabs_map[t]  # only include non-empty tabs
    ]

    return ConfigSchemaResponse(
        module_name=name,
        values_key=mod.values_key,
        tabs=tabs,
    )


def _get_nested_value(data: dict[str, Any], path: str) -> Any:
    """Retrieve a value from a nested dict using a dotted path."""
    current: Any = data
    for segment in path.split("."):
        if not isinstance(current, dict):
            return None
        current = current.get(segment)
    return current


@router.put("/{name}/config", response_model=OperationCreated, status_code=202)
async def update_module_config(
    name: str,
    body: ModuleConfigUpdateRequest,
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
    store: Annotated[OperationStore, Depends(get_operation_store)],
) -> OperationCreated:
    """Apply configuration changes to a module (async operation via K8s Job)."""
    from ilum.api.job_runner import build_helm_job, check_no_active_helm_operation

    mod = mgr.resolver.get(name)  # validates module exists → 400 if unknown

    if not body.values:
        raise HTTPException(400, "No values provided.")

    release = os.environ.get("ILUM_RELEASE_NAME", "ilum")
    namespace = os.environ.get("ILUM_NAMESPACE", "default")
    chart = os.environ.get("ILUM_CHART_REF", "ilum/ilum")

    check_no_active_helm_operation(store, mgr.k8s, namespace)
    ensure_release_not_stuck(mgr)

    # Convert values dict to --set flags, prepending values_key
    set_flags: list[str] = []
    for key, value in body.values.items():
        if value is None:
            set_flags.append(f"{mod.values_key}.{key}=null")
        else:
            set_flags.append(f"{mod.values_key}.{key}={value}")

    # Planning stays in-process (read-only)
    plan = mgr.plan_upgrade(release=release, chart=chart, set_flags=set_flags)

    # Pin chart version to avoid accidental upgrades after helm repo update
    version = plan.chart_version or _get_chart_version(mgr)

    op_id = store.create(operation="upgrade", modules=[name])
    job = build_helm_job(
        op_id=op_id,
        operation="upgrade",
        release=release,
        namespace=namespace,
        chart_ref=plan.chart,
        set_flags=plan.set_flags,
        modules=[name],
        version=version,
        reset_defaults=plan.reset_defaults,
    )
    mgr.k8s.create_job(namespace, job)

    op = store.get(op_id)
    if op:
        op.job_name = f"ilum-helm-{op_id}"
        op.status = "running"

    return OperationCreated(
        id=op_id,
        status="running",
        message=f"Updating config for module '{name}'",
    )


@router.post("/{name}/restart", response_model=OperationCreated, status_code=202)
async def restart_module(
    name: str,
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
    store: Annotated[OperationStore, Depends(get_operation_store)],
) -> OperationCreated:
    """Restart a module by deleting its pods (async operation).

    Note: restart does not run ``helm upgrade`` — it only deletes pods.
    The guard is still applied because a stuck release may indicate an
    unstable cluster state where pod deletions could cause further issues.
    """
    mod = mgr.resolver.get(name)
    if not mod.pod_label:
        raise HTTPException(400, f"Module '{name}' has no pods to restart.")

    op_id = store.create(operation="restart", modules=[name])

    def _run(op_id: str, store: OperationStore) -> None:
        namespace = os.environ.get("ILUM_NAMESPACE", mgr.helm.namespace)
        store.update_progress(op_id, 30, f"Deleting pods for '{name}'")
        deleted = mgr.k8s.delete_pods_by_label(namespace, mod.pod_label)
        store.update_progress(op_id, 90, f"Deleted {len(deleted)} pod(s), waiting for recreation")

    store.run_in_thread(op_id, _run)
    return OperationCreated(id=op_id, status="pending", message=f"Restarting module '{name}'")


@router.get("/{name}/logs", response_model=ModuleLogsResponse)
async def get_module_logs(
    name: str,
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
    tail: int = Query(default=100, ge=1, le=10000),
    pod: str | None = None,
    previous: bool = False,
) -> ModuleLogsResponse:
    """Return recent log lines from a module's pod."""
    mod = mgr.resolver.get(name)
    if not mod.pod_label:
        raise HTTPException(400, f"Module '{name}' has no pods.")

    namespace = os.environ.get("ILUM_NAMESPACE", mgr.helm.namespace)
    pod_statuses = mgr.k8s.list_pods_by_label(namespace, mod.pod_label)

    if not pod_statuses:
        return ModuleLogsResponse(module_name=name, pod_name="")

    # Pick the requested pod, or first unhealthy, or first pod
    if pod:
        target = next((p for p in pod_statuses if p.name == pod), None)
        if target is None:
            raise HTTPException(
                404,
                f"Pod '{pod}' not found for module '{name}'.",
            )
    else:
        unhealthy = [p for p in pod_statuses if not p.ready]
        target = unhealthy[0] if unhealthy else pod_statuses[0]

    try:
        log_text = mgr.k8s.read_pod_log(namespace, target.name, tail_lines=tail, previous=previous)
    except Exception:
        # previous=true fails when no previous container exists
        if previous:
            return ModuleLogsResponse(module_name=name, pod_name=target.name)
        raise
    lines = log_text.splitlines() if log_text else []
    return ModuleLogsResponse(module_name=name, pod_name=target.name, lines=lines)
