using EntKube.Web.Data;
using k8s;
using k8s.Models;
using Microsoft.EntityFrameworkCore;
using System.IO;

namespace EntKube.Web.Services;

/// <summary>
/// A simple result type for Kubernetes operations. Operations can fail for
/// many reasons (no kubeconfig, cluster unreachable, pod not found), so we
/// use Result rather than exceptions for expected failures.
/// </summary>
public class KubernetesOperationResult
{
    public bool IsSuccess { get; init; }
    public string? Error { get; init; }

    public static KubernetesOperationResult Success() => new() { IsSuccess = true };
    public static KubernetesOperationResult Failure(string error) => new() { IsSuccess = false, Error = error };
}

/// <summary>
/// Result type with a data payload for operations that return information
/// (e.g. pod lists, log content).
/// </summary>
public class KubernetesOperationResult<T>
{
    public bool IsSuccess { get; init; }
    public T? Data { get; init; }
    public string? Error { get; init; }

    public static KubernetesOperationResult<T> Success(T data) => new() { IsSuccess = true, Data = data };
    public static KubernetesOperationResult<T> Failure(string error) => new() { IsSuccess = false, Error = error };
}

/// <summary>
/// A snapshot of a Kubernetes pod's state — name, status, container count,
/// restarts, age. Rendered in the portal's deployment detail view.
/// </summary>
public class PodInfo
{
    public required string Name { get; set; }
    public required string Namespace { get; set; }
    public required string Status { get; set; }
    public int ReadyContainers { get; set; }
    public int TotalContainers { get; set; }
    public int Restarts { get; set; }
    public DateTime? StartTime { get; set; }
    public List<ContainerInfo> Containers { get; set; } = [];
}

/// <summary>
/// Per-container state within a pod. Lets the user pick which container
/// to view logs for in multi-container pods.
/// </summary>
public class ContainerInfo
{
    public required string Name { get; set; }
    public required string Image { get; set; }
    public bool Ready { get; set; }
    public int RestartCount { get; set; }
    public string? State { get; set; }
}

/// <summary>
/// Performs live Kubernetes cluster operations for the customer portal —
/// listing pods, viewing logs, restarting deployments, deleting pods, and
/// scaling. Uses the stored kubeconfig from KubernetesCluster to authenticate.
///
/// Each operation:
/// 1. Loads the deployment to find which cluster it targets
/// 2. Reads the cluster's kubeconfig
/// 3. Creates a K8s client and performs the operation
/// 4. Returns a Result so the UI can show success/failure gracefully
/// </summary>
public class KubernetesOperationsService(
    IDbContextFactory<ApplicationDbContext> dbFactory,
    AuditService auditService,
    ILogger<KubernetesOperationsService> logger)
{
    /// <summary>
    /// Loads a deployment with its cluster attached, so we know where
    /// to connect for Kubernetes API calls.
    /// </summary>
    public async Task<AppDeployment?> GetDeploymentWithClusterAsync(
        Guid deploymentId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        return await db.AppDeployments
            .Include(d => d.Cluster)
            .Include(d => d.Environment)
            .FirstOrDefaultAsync(d => d.Id == deploymentId, ct);
    }

    /// <summary>
    /// Lists all pods in the deployment's namespace, filtered by the deployment
    /// name. Gives the portal a real-time view of what's actually running.
    /// </summary>
    public async Task<KubernetesOperationResult<List<PodInfo>>> GetPodsAsync(
        Guid deploymentId, CancellationToken ct = default)
    {
        // First, load the deployment and its cluster so we know where to connect.
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);

        if (deployment is null)
        {
            return KubernetesOperationResult<List<PodInfo>>.Failure("Deployment not found.");
        }

        // The cluster needs a kubeconfig to connect.
        if (string.IsNullOrEmpty(deployment.Cluster.Kubeconfig))
        {
            return KubernetesOperationResult<List<PodInfo>>.Failure(
                "Cluster has no kubeconfig configured. Upload a kubeconfig to enable cluster operations.");
        }

        try
        {
            // Build a Kubernetes client from the stored kubeconfig.
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);

            // List pods in the deployment's namespace.
            V1PodList podList = await client.CoreV1.ListNamespacedPodAsync(
                deployment.Namespace, cancellationToken: ct);

            List<PodInfo> pods = podList.Items.Select(pod => new PodInfo
            {
                Name = pod.Metadata.Name,
                Namespace = pod.Metadata.NamespaceProperty ?? deployment.Namespace,
                Status = pod.Status?.Phase ?? "Unknown",
                ReadyContainers = pod.Status?.ContainerStatuses?.Count(cs => cs.Ready) ?? 0,
                TotalContainers = pod.Spec?.Containers?.Count ?? 0,
                Restarts = pod.Status?.ContainerStatuses?.Sum(cs => cs.RestartCount) ?? 0,
                StartTime = pod.Status?.StartTime,
                Containers = (pod.Status?.ContainerStatuses ?? []).Select(cs => new ContainerInfo
                {
                    Name = cs.Name,
                    Image = cs.Image,
                    Ready = cs.Ready,
                    RestartCount = cs.RestartCount,
                    State = cs.State?.Running is not null ? "Running"
                        : cs.State?.Waiting is not null ? $"Waiting: {cs.State.Waiting.Reason}"
                        : cs.State?.Terminated is not null ? $"Terminated: {cs.State.Terminated.Reason}"
                        : "Unknown"
                }).ToList()
            }).ToList();

            return KubernetesOperationResult<List<PodInfo>>.Success(pods);
        }
        catch (Exception ex)
        {
            return KubernetesOperationResult<List<PodInfo>>.Failure($"Failed to list pods: {ex.Message}");
        }
    }

    /// <summary>
    /// Retrieves log output from a specific pod. Optionally targets a
    /// specific container in multi-container pods. Returns the last
    /// tailLines lines to keep output manageable.
    /// </summary>
    public async Task<KubernetesOperationResult<string>> GetPodLogsAsync(
        Guid deploymentId,
        string podName,
        string? containerName = null,
        int tailLines = 500,
        CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);

        if (deployment is null)
        {
            return KubernetesOperationResult<string>.Failure("Deployment not found.");
        }

        if (string.IsNullOrEmpty(deployment.Cluster.Kubeconfig))
        {
            return KubernetesOperationResult<string>.Failure(
                "Cluster has no kubeconfig configured. Upload a kubeconfig to enable cluster operations.");
        }

        try
        {
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);

            // Fetch the last N lines of logs from the pod.
            using Stream logStream = await client.CoreV1.ReadNamespacedPodLogAsync(
                podName,
                deployment.Namespace,
                container: containerName,
                tailLines: tailLines,
                cancellationToken: ct);

            using StreamReader reader = new(logStream);
            string logs = await reader.ReadToEndAsync(ct);

            return KubernetesOperationResult<string>.Success(logs);
        }
        catch (Exception ex)
        {
            return KubernetesOperationResult<string>.Failure($"Failed to fetch logs: {ex.Message}");
        }
    }

    /// <summary>
    /// Restarts a Kubernetes Deployment by patching its pod template annotation
    /// with the current timestamp. This triggers a rolling restart — Kubernetes
    /// creates new pods and terminates old ones gradually.
    /// </summary>
    public async Task<KubernetesOperationResult> RestartDeploymentAsync(
        Guid deploymentId,
        string k8sDeploymentName,
        string? performedBy = null,
        CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);

        if (deployment is null)
        {
            return KubernetesOperationResult.Failure("Deployment not found.");
        }

        if (string.IsNullOrEmpty(deployment.Cluster.Kubeconfig))
        {
            return KubernetesOperationResult.Failure(
                "Cluster has no kubeconfig configured. Upload a kubeconfig to enable cluster operations.");
        }

        try
        {
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);

            // Patching the pod template annotation forces a rollout restart,
            // identical to "kubectl rollout restart deployment/<name>".
            V1Patch patch = new(
                "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":" +
                $"{{\"kubectl.kubernetes.io/restartedAt\":\"{DateTime.UtcNow:O}\"}}" +
                "}}}}",
                V1Patch.PatchType.MergePatch);

            await client.AppsV1.PatchNamespacedDeploymentAsync(
                patch, k8sDeploymentName, deployment.Namespace, cancellationToken: ct);

            logger.LogInformation(
                "Deployment {K8sName} in {Namespace} restarted by {User}",
                k8sDeploymentName, deployment.Namespace, performedBy ?? "system");

            await auditService.RecordAsync(deploymentId, "RestartDeployment", "Deployment",
                k8sDeploymentName, performedBy: performedBy, ct: ct);

            return KubernetesOperationResult.Success();
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex,
                "Failed to restart deployment {K8sName} in {Namespace}",
                k8sDeploymentName, deployment.Namespace);
            return KubernetesOperationResult.Failure($"Failed to restart deployment: {ex.Message}");
        }
    }

    /// <summary>
    /// Deletes a specific pod, which triggers Kubernetes to create a replacement
    /// (assuming the pod is managed by a Deployment/ReplicaSet). This is the
    /// equivalent of "kubectl delete pod <name>".
    /// </summary>
    public async Task<KubernetesOperationResult> DeletePodAsync(
        Guid deploymentId,
        string podName,
        string? performedBy = null,
        CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);

        if (deployment is null)
        {
            return KubernetesOperationResult.Failure("Deployment not found.");
        }

        if (string.IsNullOrEmpty(deployment.Cluster.Kubeconfig))
        {
            return KubernetesOperationResult.Failure(
                "Cluster has no kubeconfig configured. Upload a kubeconfig to enable cluster operations.");
        }

        try
        {
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);

            await client.CoreV1.DeleteNamespacedPodAsync(
                podName, deployment.Namespace, cancellationToken: ct);

            logger.LogInformation(
                "Pod {PodName} in {Namespace} deleted by {User}",
                podName, deployment.Namespace, performedBy ?? "system");

            await auditService.RecordAsync(deploymentId, "DeletePod", "Pod",
                podName, performedBy: performedBy, ct: ct);

            return KubernetesOperationResult.Success();
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to delete pod {PodName} in {Namespace}",
                podName, deployment.Namespace);
            return KubernetesOperationResult.Failure($"Failed to delete pod: {ex.Message}");
        }
    }

    /// <summary>
    /// Scales a Kubernetes Deployment to the specified number of replicas.
    /// Useful for scaling up during traffic spikes or scaling down to save resources.
    /// </summary>
    public async Task<KubernetesOperationResult> ScaleDeploymentAsync(
        Guid deploymentId,
        string k8sDeploymentName,
        int replicas,
        string? performedBy = null,
        CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);

        if (deployment is null)
        {
            return KubernetesOperationResult.Failure("Deployment not found.");
        }

        if (string.IsNullOrEmpty(deployment.Cluster.Kubeconfig))
        {
            return KubernetesOperationResult.Failure(
                "Cluster has no kubeconfig configured. Upload a kubeconfig to enable cluster operations.");
        }

        try
        {
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);

            V1Patch patch = new(
                $"{{\"spec\":{{\"replicas\":{replicas}}}}}",
                V1Patch.PatchType.MergePatch);

            await client.AppsV1.PatchNamespacedDeploymentAsync(
                patch, k8sDeploymentName, deployment.Namespace, cancellationToken: ct);

            logger.LogInformation(
                "Deployment {K8sName} in {Namespace} scaled to {Replicas} by {User}",
                k8sDeploymentName, deployment.Namespace, replicas, performedBy ?? "system");

            await auditService.RecordAsync(deploymentId, "ScaleDeployment", "Deployment",
                k8sDeploymentName, $"replicas={replicas}", performedBy, ct);

            return KubernetesOperationResult.Success();
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to scale deployment {K8sName} in {Namespace}",
                k8sDeploymentName, deployment.Namespace);
            return KubernetesOperationResult.Failure($"Failed to scale deployment: {ex.Message}");
        }
    }

    // ──────── Resource tree actions ────────

    /// <summary>
    /// Returns the container names for a specific pod — used to populate the
    /// container picker in the log viewer for multi-container pods.
    /// </summary>
    public async Task<KubernetesOperationResult<List<string>>> GetPodContainersAsync(
        Guid deploymentId, string podName, CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);
        if (deployment is null)
            return KubernetesOperationResult<List<string>>.Failure("Deployment not found.");
        if (string.IsNullOrEmpty(deployment.Cluster?.Kubeconfig))
            return KubernetesOperationResult<List<string>>.Failure(
                "Cluster has no kubeconfig configured.");

        try
        {
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);
            V1Pod pod = await client.CoreV1.ReadNamespacedPodAsync(
                podName, deployment.Namespace, cancellationToken: ct);
            List<string> containers = pod.Spec?.Containers?
                .Select(c => c.Name).ToList() ?? [];
            return KubernetesOperationResult<List<string>>.Success(containers);
        }
        catch (Exception ex)
        {
            return KubernetesOperationResult<List<string>>.Failure(
                $"Failed to read pod: {ex.Message}");
        }
    }

    /// <summary>
    /// Retrieves recent Kubernetes Events for a named resource. Events surface
    /// scheduling failures, image pull errors, and readiness probe results —
    /// the same information shown by "kubectl describe".
    /// </summary>
    public async Task<KubernetesOperationResult<List<string>>> GetResourceEventsAsync(
        Guid deploymentId, string kind, string name, CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);
        if (deployment is null)
            return KubernetesOperationResult<List<string>>.Failure("Deployment not found.");
        if (string.IsNullOrEmpty(deployment.Cluster?.Kubeconfig))
            return KubernetesOperationResult<List<string>>.Failure(
                "Cluster has no kubeconfig configured.");

        try
        {
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);
            var events = await client.CoreV1.ListNamespacedEventAsync(
                deployment.Namespace, cancellationToken: ct);

            List<string> lines = events.Items
                .Where(e => e.InvolvedObject?.Name == name
                         && e.InvolvedObject?.Kind == kind)
                .OrderBy(e => e.LastTimestamp ?? e.EventTime)
                .Select(e =>
                {
                    string ts = (e.LastTimestamp ?? e.EventTime)?.ToString("HH:mm:ss") ?? "?";
                    string type = e.Type ?? "Normal";
                    string reason = e.Reason ?? "";
                    string msg = e.Message ?? "";
                    return $"{ts}  {type,-8}  {reason,-24}  {msg}";
                })
                .ToList();

            return KubernetesOperationResult<List<string>>.Success(lines);
        }
        catch (Exception ex)
        {
            return KubernetesOperationResult<List<string>>.Failure(
                $"Failed to list events: {ex.Message}");
        }
    }

    /// <summary>
    /// Scales a workload (Deployment, StatefulSet, or ReplicaSet) to the specified
    /// replica count. Unified entry point so callers don't branch on kind.
    /// </summary>
    public async Task<KubernetesOperationResult> ScaleWorkloadAsync(
        Guid deploymentId, string kind, string name, int replicas,
        string? performedBy = null, CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);
        if (deployment is null)
            return KubernetesOperationResult.Failure("Deployment not found.");
        if (string.IsNullOrEmpty(deployment.Cluster?.Kubeconfig))
            return KubernetesOperationResult.Failure("Cluster has no kubeconfig configured.");

        try
        {
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);
            V1Patch patch = new(
                $"{{\"spec\":{{\"replicas\":{replicas}}}}}",
                V1Patch.PatchType.MergePatch);

            switch (kind)
            {
                case "Deployment":
                    await client.AppsV1.PatchNamespacedDeploymentAsync(
                        patch, name, deployment.Namespace, cancellationToken: ct);
                    break;
                case "StatefulSet":
                    await client.AppsV1.PatchNamespacedStatefulSetAsync(
                        patch, name, deployment.Namespace, cancellationToken: ct);
                    break;
                case "ReplicaSet":
                    await client.AppsV1.PatchNamespacedReplicaSetAsync(
                        patch, name, deployment.Namespace, cancellationToken: ct);
                    break;
                default:
                    return KubernetesOperationResult.Failure($"Cannot scale {kind}.");
            }

            logger.LogInformation("{Kind} {Name} scaled to {Replicas} in {Namespace} by {User}",
                kind, name, replicas, deployment.Namespace, performedBy ?? "system");

            await auditService.RecordAsync(deploymentId, "ScaleWorkload", kind,
                name, $"replicas={replicas}", performedBy, ct);

            return KubernetesOperationResult.Success();
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Scale failed for {Kind} {Name} in {Namespace}", kind, name, deployment.Namespace);
            return KubernetesOperationResult.Failure($"Scale failed: {ex.Message}");
        }
    }

    /// <summary>
    /// Restarts a workload (Deployment, StatefulSet, or DaemonSet) by patching
    /// the pod-template annotation — identical to "kubectl rollout restart".
    /// </summary>
    public async Task<KubernetesOperationResult> RestartWorkloadAsync(
        Guid deploymentId, string kind, string name,
        string? performedBy = null, CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);
        if (deployment is null)
            return KubernetesOperationResult.Failure("Deployment not found.");
        if (string.IsNullOrEmpty(deployment.Cluster?.Kubeconfig))
            return KubernetesOperationResult.Failure("Cluster has no kubeconfig configured.");

        try
        {
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);
            V1Patch patch = new(
                "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":" +
                $"{{\"kubectl.kubernetes.io/restartedAt\":\"{DateTime.UtcNow:O}\"}}" +
                "}}}}",
                V1Patch.PatchType.MergePatch);

            switch (kind)
            {
                case "Deployment":
                    await client.AppsV1.PatchNamespacedDeploymentAsync(
                        patch, name, deployment.Namespace, cancellationToken: ct);
                    break;
                case "StatefulSet":
                    await client.AppsV1.PatchNamespacedStatefulSetAsync(
                        patch, name, deployment.Namespace, cancellationToken: ct);
                    break;
                case "DaemonSet":
                    await client.AppsV1.PatchNamespacedDaemonSetAsync(
                        patch, name, deployment.Namespace, cancellationToken: ct);
                    break;
                default:
                    return KubernetesOperationResult.Failure($"Cannot restart {kind}.");
            }

            logger.LogInformation("{Kind} {Name} restarted in {Namespace} by {User}",
                kind, name, deployment.Namespace, performedBy ?? "system");

            await auditService.RecordAsync(deploymentId, "RestartWorkload", kind,
                name, performedBy: performedBy, ct: ct);

            return KubernetesOperationResult.Success();
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Restart failed for {Kind} {Name} in {Namespace}", kind, name, deployment.Namespace);
            return KubernetesOperationResult.Failure($"Restart failed: {ex.Message}");
        }
    }

    /// <summary>
    /// Deletes a resource by kind — Pod (triggers replacement) or Job.
    /// </summary>
    public async Task<KubernetesOperationResult> DeleteResourceAsync(
        Guid deploymentId, string kind, string name,
        string? performedBy = null, CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);
        if (deployment is null)
            return KubernetesOperationResult.Failure("Deployment not found.");
        if (string.IsNullOrEmpty(deployment.Cluster?.Kubeconfig))
            return KubernetesOperationResult.Failure("Cluster has no kubeconfig configured.");

        try
        {
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);

            switch (kind)
            {
                case "Pod":
                    await client.CoreV1.DeleteNamespacedPodAsync(
                        name, deployment.Namespace, cancellationToken: ct);
                    break;
                case "Job":
                    await client.BatchV1.DeleteNamespacedJobAsync(
                        name, deployment.Namespace,
                        body: new V1DeleteOptions { PropagationPolicy = "Background" },
                        cancellationToken: ct);
                    break;
                default:
                    return KubernetesOperationResult.Failure($"Delete not supported for {kind}.");
            }

            logger.LogInformation("{Kind} {Name} deleted in {Namespace} by {User}",
                kind, name, deployment.Namespace, performedBy ?? "system");

            await auditService.RecordAsync(deploymentId, $"Delete{kind}", kind,
                name, performedBy: performedBy, ct: ct);

            return KubernetesOperationResult.Success();
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Delete failed for {Kind} {Name} in {Namespace}", kind, name, deployment.Namespace);
            return KubernetesOperationResult.Failure($"Delete failed: {ex.Message}");
        }
    }

    // ──────── YAML / Manual apply ────────

    /// <summary>
    /// Applies all manifests for a YAML or Manual AppDeployment to the cluster
    /// using "kubectl apply". Idempotent: creates resources on first run, patches
    /// them on subsequent runs (standard server-side apply semantics).
    ///
    /// A bare Namespace manifest is prepended automatically so the target namespace
    /// is created if it doesn't exist — equivalent to Helm's --create-namespace.
    ///
    /// Manifests are applied in SortOrder sequence so foundational resources
    /// (PVCs, ConfigMaps) land before workloads (Deployments, Services).
    /// </summary>
    public async Task<KubernetesOperationResult<string>> ApplyYamlDeploymentAsync(
        Guid deploymentId, string? performedBy = null, CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);

        if (deployment is null)
            return KubernetesOperationResult<string>.Failure("Deployment not found.");

        if (string.IsNullOrWhiteSpace(deployment.Cluster.Kubeconfig))
            return KubernetesOperationResult<string>.Failure(
                "Cluster has no kubeconfig configured. Upload a kubeconfig to enable cluster operations.");

        // Load manifests in apply order.
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        List<DeploymentManifest> manifests = await db.DeploymentManifests
            .Where(m => m.DeploymentId == deploymentId)
            .OrderBy(m => m.SortOrder)
            .ToListAsync(ct);

        if (manifests.Count == 0)
            return KubernetesOperationResult<string>.Failure(
                "No manifests defined for this deployment. Add YAML manifests before applying.");

        // Prepend a Namespace manifest so the namespace is created automatically
        // if it doesn't exist yet — mirrors Helm's --create-namespace behaviour.
        string nsManifest = $"apiVersion: v1\nkind: Namespace\nmetadata:\n  name: {deployment.Namespace}";
        string combined = nsManifest + "\n---\n" + string.Join("\n---\n", manifests.Select(m => m.YamlContent));

        string tempKubeconfig = Path.Combine(Path.GetTempPath(), $"entkube-{Guid.NewGuid()}.kubeconfig");
        string tempManifest = Path.Combine(Path.GetTempPath(), $"entkube-manifest-{Guid.NewGuid()}.yaml");

        try
        {
            await File.WriteAllTextAsync(tempKubeconfig, deployment.Cluster.Kubeconfig, ct);
            await File.WriteAllTextAsync(tempManifest, combined, ct);

            HelmExecutionResult result = await RunCliAsync(
                "kubectl",
                $"apply -f {tempManifest} --kubeconfig {tempKubeconfig}",
                ct);

            if (result.Success)
            {
                logger.LogInformation("YAML deployment {DeploymentId} applied to {Namespace} by {User}",
                    deploymentId, deployment.Namespace, performedBy ?? "system");
                await auditService.RecordAsync(deploymentId, "ApplyYaml", "Deployment",
                    deployment.Name, performedBy: performedBy, ct: ct);
            }
            else
            {
                logger.LogWarning("YAML apply failed for deployment {DeploymentId}: {Output}",
                    deploymentId, result.Output);
            }

            return result.Success
                ? KubernetesOperationResult<string>.Success(result.Output)
                : KubernetesOperationResult<string>.Failure(result.Output);
        }
        finally
        {
            if (File.Exists(tempKubeconfig)) File.Delete(tempKubeconfig);
            if (File.Exists(tempManifest)) File.Delete(tempManifest);
        }
    }

    // ──────── Helm install / upgrade ────────

    /// <summary>
    /// Runs "helm upgrade --install" for a HelmChart AppDeployment.
    /// Idempotent: installs on first run, upgrades on subsequent ones.
    /// Always passes --create-namespace so the target namespace is created
    /// automatically if it doesn't exist yet.
    ///
    /// If a HelmRepoUrl is configured the repo is registered (helm repo add)
    /// and updated before the install. The release name is derived from the
    /// deployment name, sanitised to Helm's DNS-compatible format.
    ///
    /// Returns the combined stdout + stderr from the helm process so the UI
    /// can show exactly what Helm reported.
    /// </summary>
    public async Task<KubernetesOperationResult<string>> HelmInstallOrUpgradeAsync(
        Guid deploymentId, string? performedBy = null, CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);

        if (deployment is null)
            return KubernetesOperationResult<string>.Failure("Deployment not found.");

        if (string.IsNullOrWhiteSpace(deployment.Cluster.Kubeconfig))
            return KubernetesOperationResult<string>.Failure(
                "Cluster has no kubeconfig configured. Upload a kubeconfig to enable cluster operations.");

        if (string.IsNullOrWhiteSpace(deployment.HelmChartName))
            return KubernetesOperationResult<string>.Failure(
                "No Helm chart name configured. Set the chart name in the deployment settings before installing.");

        string releaseName = ToHelmReleaseName(deployment.Name);
        string tempKubeconfig = Path.Combine(Path.GetTempPath(), $"entkube-{Guid.NewGuid()}.kubeconfig");
        string? tempValues = null;

        try
        {
            await File.WriteAllTextAsync(tempKubeconfig, deployment.Cluster.Kubeconfig, ct);

            // Register and update the Helm repo if a URL is given.
            string chartRef = deployment.HelmChartName;

            if (!string.IsNullOrWhiteSpace(deployment.HelmRepoUrl))
            {
                string repoAlias = $"entkube-{releaseName}";

                await RunCliAsync("helm",
                    $"repo add {repoAlias} {deployment.HelmRepoUrl} --force-update --kubeconfig {tempKubeconfig}", ct);
                await RunCliAsync("helm",
                    $"repo update {repoAlias} --kubeconfig {tempKubeconfig}", ct);

                chartRef = $"{repoAlias}/{deployment.HelmChartName}";
            }

            // Build the main helm upgrade --install command.
            List<string> args =
            [
                "upgrade", "--install",
                releaseName,
                chartRef,
                "--namespace", deployment.Namespace,
                "--create-namespace",
            ];

            if (!string.IsNullOrWhiteSpace(deployment.HelmChartVersion))
            {
                args.Add("--version");
                args.Add(deployment.HelmChartVersion);
            }

            if (!string.IsNullOrWhiteSpace(deployment.HelmValues))
            {
                tempValues = Path.Combine(Path.GetTempPath(), $"entkube-values-{Guid.NewGuid()}.yaml");
                await File.WriteAllTextAsync(tempValues, deployment.HelmValues, ct);
                args.Add("--values");
                args.Add(tempValues);
            }

            args.Add("--kubeconfig");
            args.Add(tempKubeconfig);
            args.Add("--wait");
            args.Add("--timeout");
            args.Add("10m0s");

            HelmExecutionResult result = await RunCliAsync("helm", string.Join(" ", args), ct);

            if (result.Success)
            {
                logger.LogInformation(
                    "Helm install/upgrade {Chart} {Version} to {Namespace} succeeded for deployment {DeploymentId} by {User}",
                    deployment.HelmChartName, deployment.HelmChartVersion,
                    deployment.Namespace, deploymentId, performedBy ?? "system");
                await auditService.RecordAsync(deploymentId, "HelmInstallOrUpgrade", "HelmRelease",
                    deployment.Name, $"{deployment.HelmChartName}@{deployment.HelmChartVersion}", performedBy, ct);
            }
            else
            {
                logger.LogWarning(
                    "Helm install/upgrade failed for deployment {DeploymentId}: {Output}",
                    deploymentId, result.Output);
            }

            return result.Success
                ? KubernetesOperationResult<string>.Success(result.Output)
                : KubernetesOperationResult<string>.Failure(result.Output);
        }
        finally
        {
            if (File.Exists(tempKubeconfig)) File.Delete(tempKubeconfig);
            if (tempValues is not null && File.Exists(tempValues)) File.Delete(tempValues);
        }
    }

    /// <summary>
    /// Queries the cluster live and returns a proper ownership tree of resources
    /// found in the deployment's namespace. OwnerReferences are used to wire up
    /// parent-child relationships (e.g. Deployment → ReplicaSet → Pod).
    /// Each resource type is fetched independently so a missing API group
    /// (e.g. no Ingress controller) doesn't block the others.
    /// </summary>
    public async Task<KubernetesOperationResult<List<DeploymentResource>>> GetLiveResourcesAsync(
        Guid deploymentId, CancellationToken ct = default)
    {
        AppDeployment? deployment = await GetDeploymentWithClusterAsync(deploymentId, ct);

        if (deployment is null)
            return KubernetesOperationResult<List<DeploymentResource>>.Failure("Deployment not found.");

        if (string.IsNullOrEmpty(deployment.Cluster?.Kubeconfig))
            return KubernetesOperationResult<List<DeploymentResource>>.Failure(
                "Cluster has no kubeconfig configured.");

        string ns = deployment.Namespace;

        // k8s UID → our resource node (for O(1) parent lookup during tree wiring)
        Dictionary<string, DeploymentResource> byUid = new();
        // Resources whose OwnerReferences need to be resolved after all types are fetched.
        List<(DeploymentResource Node, string[] OwnerUids)> pendingWire = [];

        // Create a resource node, record its UID, and register its owner refs for later wiring.
        DeploymentResource MakeNode(string? uid, IList<V1OwnerReference>? owners,
            string group, string version, string kind, string name,
            HealthStatus health, string? message)
        {
            DeploymentResource r = new()
            {
                Id = Guid.NewGuid(),
                DeploymentId = deploymentId,
                Group = group, Version = version, Kind = kind,
                Name = name, Namespace = ns,
                SyncStatus = SyncStatus.Synced,
                HealthStatus = health,
                StatusMessage = message,
                ChildResources = []
            };

            if (!string.IsNullOrEmpty(uid))
                byUid[uid] = r;

            string[] ownerUids = owners?
                .Select(o => o.Uid).Where(u => !string.IsNullOrEmpty(u)).ToArray() ?? [];

            if (ownerUids.Length > 0)
                pendingWire.Add((r, ownerUids));

            return r;
        }

        try
        {
            using Kubernetes client = CreateClient(deployment.Cluster.Kubeconfig);

            // ── Deployments ───────────────────────────────────────────────
            await TryFetch(async () =>
            {
                V1DeploymentList list = await client.AppsV1.ListNamespacedDeploymentAsync(ns, cancellationToken: ct);
                foreach (V1Deployment d in list.Items)
                {
                    int desired = d.Spec?.Replicas ?? 0;
                    int ready   = d.Status?.ReadyReplicas ?? 0;
                    HealthStatus h = desired == 0 ? HealthStatus.Suspended
                        : ready >= desired ? HealthStatus.Healthy
                        : ready > 0        ? HealthStatus.Degraded
                                           : HealthStatus.Progressing;
                    MakeNode(d.Metadata.Uid, d.Metadata.OwnerReferences,
                        "apps", "v1", "Deployment", d.Metadata.Name, h, $"{ready}/{desired} ready");
                }
            });

            // ── StatefulSets ─────────────────────────────────────────────
            await TryFetch(async () =>
            {
                V1StatefulSetList list = await client.AppsV1.ListNamespacedStatefulSetAsync(ns, cancellationToken: ct);
                foreach (V1StatefulSet s in list.Items)
                {
                    int desired = s.Spec?.Replicas ?? 0;
                    int ready   = s.Status?.ReadyReplicas ?? 0;
                    HealthStatus h = desired == 0 ? HealthStatus.Suspended
                        : ready >= desired ? HealthStatus.Healthy
                        : ready > 0        ? HealthStatus.Degraded
                                           : HealthStatus.Progressing;
                    MakeNode(s.Metadata.Uid, s.Metadata.OwnerReferences,
                        "apps", "v1", "StatefulSet", s.Metadata.Name, h, $"{ready}/{desired} ready");
                }
            });

            // ── DaemonSets ───────────────────────────────────────────────
            await TryFetch(async () =>
            {
                V1DaemonSetList list = await client.AppsV1.ListNamespacedDaemonSetAsync(ns, cancellationToken: ct);
                foreach (V1DaemonSet ds in list.Items)
                {
                    int desired = ds.Status?.DesiredNumberScheduled ?? 0;
                    int ready   = ds.Status?.NumberReady ?? 0;
                    HealthStatus h = desired == 0 ? HealthStatus.Suspended
                        : ready >= desired ? HealthStatus.Healthy
                        : ready > 0        ? HealthStatus.Degraded
                                           : HealthStatus.Progressing;
                    MakeNode(ds.Metadata.Uid, ds.Metadata.OwnerReferences,
                        "apps", "v1", "DaemonSet", ds.Metadata.Name, h, $"{ready}/{desired} ready");
                }
            });

            // ── ReplicaSets (ALL — including 0-replica old ones for tree completeness) ─
            await TryFetch(async () =>
            {
                V1ReplicaSetList list = await client.AppsV1.ListNamespacedReplicaSetAsync(ns, cancellationToken: ct);
                foreach (V1ReplicaSet rs in list.Items)
                {
                    int desired = rs.Spec?.Replicas ?? 0;
                    int ready   = rs.Status?.ReadyReplicas ?? 0;
                    HealthStatus h = desired == 0 ? HealthStatus.Suspended
                        : ready >= desired ? HealthStatus.Healthy
                        : ready > 0        ? HealthStatus.Degraded
                                           : HealthStatus.Progressing;
                    MakeNode(rs.Metadata.Uid, rs.Metadata.OwnerReferences,
                        "apps", "v1", "ReplicaSet", rs.Metadata.Name, h, $"{ready}/{desired} ready");
                }
            });

            // ── Services ────────────────────────────────────────────────
            await TryFetch(async () =>
            {
                V1ServiceList list = await client.CoreV1.ListNamespacedServiceAsync(ns, cancellationToken: ct);
                foreach (V1Service svc in list.Items)
                {
                    MakeNode(svc.Metadata.Uid, svc.Metadata.OwnerReferences,
                        "", "v1", "Service", svc.Metadata.Name, HealthStatus.Healthy,
                        $"{svc.Spec?.Type ?? "ClusterIP"}  {svc.Spec?.ClusterIP}");
                }
            });

            // ── Ingresses ───────────────────────────────────────────────
            await TryFetch(async () =>
            {
                V1IngressList list = await client.NetworkingV1.ListNamespacedIngressAsync(ns, cancellationToken: ct);
                foreach (V1Ingress ing in list.Items)
                {
                    string? lb = ing.Status?.LoadBalancer?.Ingress?.FirstOrDefault()?.Ip
                              ?? ing.Status?.LoadBalancer?.Ingress?.FirstOrDefault()?.Hostname;
                    string hosts = string.Join(", ", (ing.Spec?.Rules?.Select(r => r.Host) ?? [])
                        .Where(h => !string.IsNullOrEmpty(h))!);
                    MakeNode(ing.Metadata.Uid, ing.Metadata.OwnerReferences,
                        "networking.k8s.io", "v1", "Ingress", ing.Metadata.Name, HealthStatus.Healthy,
                        lb ?? hosts);
                }
            });

            // ── PersistentVolumeClaims ───────────────────────────────────
            await TryFetch(async () =>
            {
                V1PersistentVolumeClaimList list = await client.CoreV1.ListNamespacedPersistentVolumeClaimAsync(ns, cancellationToken: ct);
                foreach (V1PersistentVolumeClaim pvc in list.Items)
                {
                    HealthStatus h = pvc.Status?.Phase == "Bound"   ? HealthStatus.Healthy
                                   : pvc.Status?.Phase == "Pending" ? HealthStatus.Progressing
                                                                     : HealthStatus.Degraded;
                    string? size = pvc.Spec?.Resources?.Requests is { } req &&
                                   req.TryGetValue("storage", out ResourceQuantity? qty)
                                   ? qty?.ToString() : null;
                    MakeNode(pvc.Metadata.Uid, pvc.Metadata.OwnerReferences,
                        "", "v1", "PersistentVolumeClaim", pvc.Metadata.Name, h,
                        string.Join("  ", new[] { pvc.Status?.Phase, size }.Where(s => s != null)));
                }
            });

            // ── ConfigMaps (exclude system-generated and owned ones) ─────
            await TryFetch(async () =>
            {
                V1ConfigMapList list = await client.CoreV1.ListNamespacedConfigMapAsync(ns, cancellationToken: ct);
                foreach (V1ConfigMap cm in list.Items.Where(c =>
                    c.Metadata.Name != "kube-root-ca.crt" &&
                    c.Metadata.OwnerReferences?.Any() != true))
                {
                    MakeNode(cm.Metadata.Uid, null,
                        "", "v1", "ConfigMap", cm.Metadata.Name, HealthStatus.Healthy, null);
                }
            });

            // ── Jobs ─────────────────────────────────────────────────────
            await TryFetch(async () =>
            {
                V1JobList list = await client.BatchV1.ListNamespacedJobAsync(ns, cancellationToken: ct);
                foreach (V1Job job in list.Items)
                {
                    bool active    = (job.Status?.Active    ?? 0) > 0;
                    bool succeeded = (job.Status?.Succeeded ?? 0) > 0;
                    bool failed    = (job.Status?.Failed    ?? 0) > 0;
                    HealthStatus h = failed    ? HealthStatus.Degraded
                                   : active    ? HealthStatus.Progressing
                                   : succeeded ? HealthStatus.Healthy
                                               : HealthStatus.Suspended;
                    MakeNode(job.Metadata.Uid, job.Metadata.OwnerReferences,
                        "batch", "v1", "Job", job.Metadata.Name, h,
                        $"✓{job.Status?.Succeeded ?? 0}  ✗{job.Status?.Failed ?? 0}");
                }
            });

            // ── CronJobs ──────────────────────────────────────────────────
            await TryFetch(async () =>
            {
                V1CronJobList list = await client.BatchV1.ListNamespacedCronJobAsync(ns, cancellationToken: ct);
                foreach (V1CronJob cj in list.Items)
                {
                    MakeNode(cj.Metadata.Uid, null,
                        "batch", "v1", "CronJob", cj.Metadata.Name,
                        cj.Spec?.Suspend == true ? HealthStatus.Suspended : HealthStatus.Healthy,
                        cj.Spec?.Schedule);
                }
            });

            // ── Pods ──────────────────────────────────────────────────────
            await TryFetch(async () =>
            {
                V1PodList list = await client.CoreV1.ListNamespacedPodAsync(ns, cancellationToken: ct);
                foreach (V1Pod pod in list.Items)
                {
                    int ready = pod.Status?.ContainerStatuses?.Count(cs => cs.Ready) ?? 0;
                    int total = pod.Spec?.Containers?.Count ?? 0;
                    HealthStatus h = pod.Status?.Phase switch
                    {
                        "Running"   => pod.Status.ContainerStatuses?.All(cs => cs.Ready) == true
                                        ? HealthStatus.Healthy : HealthStatus.Degraded,
                        "Pending"   => HealthStatus.Progressing,
                        "Succeeded" => HealthStatus.Healthy,
                        "Failed"    => HealthStatus.Degraded,
                        _           => HealthStatus.Unknown
                    };
                    MakeNode(pod.Metadata.Uid, pod.Metadata.OwnerReferences,
                        "", "v1", "Pod", pod.Metadata.Name, h,
                        $"{pod.Status?.Phase}  {ready}/{total}");
                }
            });

            // ── Wire parent-child using OwnerReferences ───────────────────
            foreach ((DeploymentResource node, string[] ownerUids) in pendingWire)
            {
                foreach (string ownerUid in ownerUids)
                {
                    if (byUid.TryGetValue(ownerUid, out DeploymentResource? parent))
                    {
                        node.ParentResourceId = parent.Id;
                        parent.ChildResources.Add(node);
                        break;
                    }
                }
            }

            // ── Return root nodes sorted by display order then name ───────
            List<DeploymentResource> roots = byUid.Values
                .Where(r => r.ParentResourceId == null)
                .OrderBy(r => KindDisplayOrder(r.Kind))
                .ThenBy(r => r.Name)
                .ToList();

            SortChildResources(roots);

            return KubernetesOperationResult<List<DeploymentResource>>.Success(roots);
        }
        catch (Exception ex)
        {
            return KubernetesOperationResult<List<DeploymentResource>>.Failure(
                $"Failed to query cluster: {ex.Message}");
        }
    }

    /// <summary>
    /// Derives the overall sync + health status from a live resource tree.
    /// Called after GetLiveResourcesAsync so the UI can persist and display
    /// up-to-date status badges without a second cluster round-trip.
    ///
    /// Rules:
    /// - Empty tree → Unknown / Unknown
    /// - Any resources present → Synced
    /// - Health = worst-case across all root workload nodes
    ///   (Degraded > Progressing > Suspended > Healthy > Unknown)
    /// </summary>
    public static (SyncStatus Sync, HealthStatus Health) ComputeStatusFromResources(
        List<DeploymentResource> roots)
    {
        if (roots.Count == 0)
            return (SyncStatus.Unknown, HealthStatus.Unknown);

        // Prefer workload kinds for the aggregate; fall back to all roots.
        List<DeploymentResource> workloads = roots
            .Where(r => r.Kind is "Deployment" or "StatefulSet" or "DaemonSet")
            .ToList();

        if (workloads.Count == 0)
            workloads = roots;

        static int HealthSeverity(HealthStatus h) => h switch
        {
            HealthStatus.Degraded    => 4,
            HealthStatus.Missing     => 4,
            HealthStatus.Progressing => 3,
            HealthStatus.Suspended   => 2,
            HealthStatus.Healthy     => 1,
            _                        => 0,
        };

        HealthStatus worst = workloads
            .OrderByDescending(w => HealthSeverity(w.HealthStatus))
            .First()
            .HealthStatus;

        // Map Missing → Degraded for the top-level badge (Missing is a resource-level concept).
        if (worst == HealthStatus.Missing)
            worst = HealthStatus.Degraded;

        return (SyncStatus.Synced, worst);
    }

    /// <summary>
    /// Derives sync + health status from a list of live pods.
    /// Used in portal views that already have the pod list available.
    /// </summary>
    public static (SyncStatus Sync, HealthStatus Health) ComputeStatusFromPods(
        List<PodInfo> pods)
    {
        if (pods.Count == 0)
            return (SyncStatus.Unknown, HealthStatus.Unknown);

        bool allReady  = pods.All(p =>
            p.Status == "Running" && p.ReadyContainers == p.TotalContainers);
        bool anyFailed  = pods.Any(p => p.Status == "Failed");
        bool anyPending = pods.Any(p => p.Status == "Pending");
        bool anyRunning = pods.Any(p => p.Status == "Running");

        HealthStatus health =
            anyFailed            ? HealthStatus.Degraded
            : allReady           ? HealthStatus.Healthy
            : anyPending         ? HealthStatus.Progressing
            : anyRunning         ? HealthStatus.Degraded   // running but not all ready
                                 : HealthStatus.Unknown;

        return (SyncStatus.Synced, health);
    }

    private static void SortChildResources(IEnumerable<DeploymentResource> nodes)
    {
        foreach (DeploymentResource node in nodes)
        {
            if (node.ChildResources is List<DeploymentResource> kids && kids.Count > 1)
            {
                kids.Sort((a, b) =>
                {
                    int ko = KindDisplayOrder(a.Kind).CompareTo(KindDisplayOrder(b.Kind));
                    return ko != 0 ? ko : string.Compare(a.Name, b.Name, StringComparison.Ordinal);
                });
                SortChildResources(kids);
            }
        }
    }

    private static int KindDisplayOrder(string kind) => kind switch
    {
        "Deployment"            => 0,
        "StatefulSet"           => 1,
        "DaemonSet"             => 2,
        "CronJob"               => 3,
        "Job"                   => 4,
        "Service"               => 5,
        "Ingress"               => 6,
        "PersistentVolumeClaim" => 7,
        "ConfigMap"             => 8,
        "ReplicaSet"            => 9,
        "Pod"                   => 10,
        _                      => 99
    };

    /// Runs a fetch action and swallows errors — a missing API group
    /// (e.g. no Ingress controller) should not abort the entire resource list.
    private async Task TryFetch(Func<Task> fetch, string resourceType = "")
    {
        try { await fetch(); }
        catch (Exception ex)
        {
            logger.LogDebug(ex, "Non-fatal: could not fetch {ResourceType} from cluster", resourceType);
        }
    }

    // ──────── Internal ────────

    /// <summary>
    /// Creates a Kubernetes client from a raw kubeconfig YAML string.
    /// The kubeconfig is parsed in-memory — never written to disk.
    /// </summary>
    private static Kubernetes CreateClient(string kubeconfig)
    {
        using MemoryStream stream = new(System.Text.Encoding.UTF8.GetBytes(kubeconfig));
        KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(stream);
        return new Kubernetes(config);
    }

    /// <summary>
    /// Converts an arbitrary deployment name into a Helm-compatible release name:
    /// lowercase, alphanumeric + hyphens only, max 53 characters.
    /// </summary>
    private static string ToHelmReleaseName(string name)
    {
        string s = name.ToLowerInvariant();
        s = System.Text.RegularExpressions.Regex.Replace(s, @"[^a-z0-9]+", "-");
        s = s.Trim('-');
        return s.Length > 53 ? s[..53].TrimEnd('-') : s;
    }

    /// <summary>
    /// Runs an external CLI process (helm, kubectl) and returns the combined
    /// stdout + stderr. Exit code 0 → Success; non-zero → Failure.
    /// </summary>
    private static async Task<HelmExecutionResult> RunCliAsync(
        string program, string arguments, CancellationToken ct)
    {
        System.Diagnostics.ProcessStartInfo psi = new()
        {
            FileName = program,
            Arguments = arguments,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };

        using System.Diagnostics.Process process = new() { StartInfo = psi };

        try
        {
            process.Start();

            Task<string> stdout = process.StandardOutput.ReadToEndAsync(ct);
            Task<string> stderr = process.StandardError.ReadToEndAsync(ct);
            await process.WaitForExitAsync(ct);

            string combined = (await stdout).Trim();
            string err = (await stderr).Trim();
            if (!string.IsNullOrEmpty(err)) combined = string.IsNullOrEmpty(combined) ? err : combined + "\n" + err;

            return new HelmExecutionResult
            {
                Success = process.ExitCode == 0,
                ExitCode = process.ExitCode,
                Output = combined
            };
        }
        catch (Exception ex)
        {
            return new HelmExecutionResult
            {
                Success = false,
                Output = $"Failed to run {program}: {ex.Message}"
            };
        }
    }
}
