using System.Globalization;
using System.Text;
using System.Text.Json;
using EntKube.Web.Data;
using k8s;
using k8s.Models;
using Microsoft.EntityFrameworkCore;

namespace EntKube.Web.Services;

public class LokiConfig
{
    public string Namespace { get; set; } = "monitoring";
    public string ServiceName { get; set; } = "loki";
    public int ServicePort { get; set; } = 3100;
}

public class LokiLogStream
{
    public Dictionary<string, string> Labels { get; set; } = new();
    public List<LokiLogEntry> Entries { get; set; } = [];
}

public class LokiLogEntry
{
    public DateTime Timestamp { get; set; }
    public string Line { get; set; } = "";
    public LogLevel DetectedLevel { get; set; } = LogLevel.None;
}

public enum LogLevel { None, Debug, Info, Warn, Error, Fatal }

/// <summary>
/// Queries Grafana Loki for logs via the Kubernetes API proxy endpoint,
/// using the same pattern as PrometheusService. Detects Loki automatically
/// by looking for a ClusterComponent with HelmChartName containing "loki".
///
/// Also handles S3 chunk storage configuration: WriteStorageHelmValuesAsync
/// injects non-sensitive S3 values (region, bucket, endpoint) into HelmValues
/// and stores credentials as vault secrets for injection at install time.
/// </summary>
public class LokiService(
    IDbContextFactory<ApplicationDbContext> dbFactory,
    ILogger<LokiService> logger,
    VaultService vaultService,
    StorageService storageService)
{
    public async Task<KubernetesOperationResult<List<string>>> GetNamespacesAsync(
        Guid clusterId, CancellationToken ct = default)
    {
        var (info, error) = await ResolveLokiInfoAsync(clusterId, ct);
        if (info is null) return KubernetesOperationResult<List<string>>.Failure(error!);

        return await WithLokiAsync<List<string>>(info,
            async (http, baseUrl, token) =>
            {
                string json = await http.GetStringAsync($"{baseUrl}/loki/api/v1/label/namespace/values", token);
                return ParseLabelValues(json);
            },
            $"loki namespaces {clusterId}", ct);
    }

    public async Task<KubernetesOperationResult<List<string>>> GetPodsAsync(
        Guid clusterId, string namespaceName, CancellationToken ct = default)
    {
        var (info, error) = await ResolveLokiInfoAsync(clusterId, ct);
        if (info is null) return KubernetesOperationResult<List<string>>.Failure(error!);

        string query = string.IsNullOrEmpty(namespaceName)
            ? "/loki/api/v1/label/pod/values"
            : $"/loki/api/v1/label/pod/values?query={Uri.EscapeDataString($"{{namespace=\"{namespaceName}\"}}")}";

        return await WithLokiAsync<List<string>>(info,
            async (http, baseUrl, token) =>
            {
                string json = await http.GetStringAsync($"{baseUrl}{query}", token);
                return ParseLabelValues(json);
            },
            $"loki pods {clusterId} ns={namespaceName}", ct);
    }

    public async Task<KubernetesOperationResult<List<LokiLogStream>>> QueryRangeAsync(
        Guid clusterId,
        string namespaceName,
        string? podName,
        string? containerName,
        string? textFilter,
        DateTime from,
        DateTime to,
        int limit = 200,
        CancellationToken ct = default)
    {
        var (info, error) = await ResolveLokiInfoAsync(clusterId, ct);
        if (info is null) return KubernetesOperationResult<List<LokiLogStream>>.Failure(error!);

        string logQuery = BuildLogQL(namespaceName, podName, containerName, textFilter);
        long fromNs = new DateTimeOffset(from.ToUniversalTime()).ToUnixTimeMilliseconds() * 1_000_000;
        long toNs   = new DateTimeOffset(to.ToUniversalTime()).ToUnixTimeMilliseconds() * 1_000_000;

        string url = $"/loki/api/v1/query_range?query={Uri.EscapeDataString(logQuery)}" +
                     $"&start={fromNs}&end={toNs}&limit={limit}&direction=backward";

        return await WithLokiAsync<List<LokiLogStream>>(info,
            async (http, baseUrl, token) =>
            {
                string json = await http.GetStringAsync($"{baseUrl}{url}", token);
                return ParseQueryRange(json);
            },
            $"loki query {clusterId}", ct);
    }

    public async Task<bool> IsAvailableAsync(Guid clusterId, CancellationToken ct = default)
    {
        var (info, _) = await ResolveLokiInfoAsync(clusterId, ct);
        return info is not null;
    }

    // ──────── Storage configuration ────────

    /// <summary>
    /// Injects S3-compatible storage configuration into the component's Helm values
    /// and stores the access/secret key as vault secrets for injection at install time.
    ///
    /// Non-sensitive values (region, bucket names, endpoint, storage type) are written
    /// directly to HelmValues. Credentials are stored encrypted in the vault and
    /// injected by InjectSecretsIntoValuesAsync via the hidden catalog fields.
    /// </summary>
    public async Task WriteStorageHelmValuesAsync(
        Guid tenantId, Guid clusterComponentId, Guid storageLinkId, CancellationToken ct = default)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        ClusterComponent component = await db.ClusterComponents
            .Include(c => c.Cluster)
            .FirstOrDefaultAsync(c => c.Id == clusterComponentId && c.Cluster.TenantId == tenantId, ct)
            ?? throw new InvalidOperationException("Component not found.");

        StorageLink link = await db.StorageLinks
            .FirstOrDefaultAsync(s => s.Id == storageLinkId && s.TenantId == tenantId, ct)
            ?? throw new InvalidOperationException("Storage link not found.");

        string region = link.Region ?? "us-east-1";
        string bucket = link.BucketName ?? "";

        Dictionary<string, string> s3Values = new()
        {
            ["loki.storage.type"] = "s3",
            ["loki.storage.s3.region"] = region,
            ["loki.storage.s3.s3ForcePathStyle"] = "true",
            ["loki.storage.bucketNames.chunks"] = bucket,
            ["loki.storage.bucketNames.ruler"] = bucket,
            ["loki.storage.bucketNames.admin"] = bucket
        };

        if (!string.IsNullOrWhiteSpace(link.Endpoint))
        {
            s3Values["loki.storage.s3.endpoint"] = link.Endpoint;
        }

        component.HelmValues = YamlFormMerger.MergeFormValues(component.HelmValues ?? "", s3Values);
        await db.SaveChangesAsync(ct);

        // Store credentials as vault secrets — injected at install time via hidden catalog fields
        // loki-s3-access-key (→ loki.storage.s3.accessKeyId) and
        // loki-s3-secret-key (→ loki.storage.s3.secretAccessKey).
        await vaultService.InitializeVaultAsync(tenantId, ct);
        (string accessKey, string secretKey) = await storageService.GetStoredCredentialsInternalAsync(tenantId, storageLinkId, ct);

        if (!string.IsNullOrEmpty(accessKey))
        {
            await vaultService.SetComponentSecretAsync(tenantId, clusterComponentId, "loki-s3-access-key", accessKey, ct);
        }

        if (!string.IsNullOrEmpty(secretKey))
        {
            await vaultService.SetComponentSecretAsync(tenantId, clusterComponentId, "loki-s3-secret-key", secretKey, ct);
        }
    }

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

    private static string BuildLogQL(string ns, string? pod, string? container, string? text)
    {
        StringBuilder sb = new("{");
        sb.Append($"namespace=\"{ns}\"");
        if (!string.IsNullOrEmpty(pod))       sb.Append($", pod=~\"{pod}.*\"");
        if (!string.IsNullOrEmpty(container)) sb.Append($", container=\"{container}\"");
        sb.Append('}');

        if (!string.IsNullOrEmpty(text))
            sb.Append($" |= \"{text.Replace("\"", "\\\"")}\"");

        return sb.ToString();
    }

    private static List<string> ParseLabelValues(string json)
    {
        try
        {
            using JsonDocument doc = JsonDocument.Parse(json);
            if (doc.RootElement.GetProperty("status").GetString() != "success") return [];
            return [.. doc.RootElement.GetProperty("data")
                .EnumerateArray()
                .Select(e => e.GetString() ?? "")
                .Where(s => !string.IsNullOrEmpty(s))
                .OrderBy(s => s)];
        }
        catch { return []; }
    }

    private static List<LokiLogStream> ParseQueryRange(string json)
    {
        List<LokiLogStream> results = [];
        try
        {
            using JsonDocument doc = JsonDocument.Parse(json);
            JsonElement root = doc.RootElement;

            if (root.GetProperty("status").GetString() != "success") return results;

            JsonElement data = root.GetProperty("data");
            if (!data.GetProperty("resultType").GetString()!.Equals("streams", StringComparison.OrdinalIgnoreCase))
                return results;

            foreach (JsonElement stream in data.GetProperty("result").EnumerateArray())
            {
                LokiLogStream ls = new();

                if (stream.TryGetProperty("stream", out JsonElement labels))
                    foreach (JsonProperty p in labels.EnumerateObject())
                        ls.Labels[p.Name] = p.Value.GetString() ?? "";

                if (stream.TryGetProperty("values", out JsonElement values))
                    foreach (JsonElement entry in values.EnumerateArray())
                    {
                        if (entry.GetArrayLength() < 2) continue;
                        string tsNs = entry[0].GetString() ?? "0";
                        string line = entry[1].GetString() ?? "";

                        DateTime ts = long.TryParse(tsNs, out long ns)
                            ? DateTimeOffset.FromUnixTimeMilliseconds(ns / 1_000_000).UtcDateTime
                            : DateTime.UtcNow;

                        ls.Entries.Add(new LokiLogEntry
                        {
                            Timestamp = ts,
                            Line = line,
                            DetectedLevel = DetectLevel(line)
                        });
                    }

                if (ls.Entries.Count > 0) results.Add(ls);
            }
        }
        catch { }
        return results;
    }

    private static LogLevel DetectLevel(string line)
    {
        if (line.Contains("FATAL",    StringComparison.OrdinalIgnoreCase) ||
            line.Contains("CRITICAL", StringComparison.OrdinalIgnoreCase)) return LogLevel.Fatal;
        if (line.Contains("ERROR",    StringComparison.OrdinalIgnoreCase) ||
            line.Contains(" ERR ",    StringComparison.OrdinalIgnoreCase)) return LogLevel.Error;
        if (line.Contains("WARN",     StringComparison.OrdinalIgnoreCase)) return LogLevel.Warn;
        if (line.Contains("DEBUG",    StringComparison.OrdinalIgnoreCase)) return LogLevel.Debug;
        if (line.Contains("INFO",     StringComparison.OrdinalIgnoreCase)) return LogLevel.Info;
        return LogLevel.None;
    }

    private sealed record ResolvedLokiInfo(string Kubeconfig, LokiConfig Config);

    private async Task<(ResolvedLokiInfo? Info, string? Error)> ResolveLokiInfoAsync(
        Guid clusterId, CancellationToken ct)
    {
        using ApplicationDbContext db = dbFactory.CreateDbContext();

        KubernetesCluster? cluster = await db.KubernetesClusters
            .Include(c => c.Components)
            .FirstOrDefaultAsync(c => c.Id == clusterId, ct);

        if (cluster is null) return (null, "Cluster not found.");
        if (string.IsNullOrWhiteSpace(cluster.Kubeconfig)) return (null, "No kubeconfig configured.");

        ClusterComponent? lokiComponent = cluster.Components.FirstOrDefault(c =>
            (c.HelmChartName ?? "").Contains("loki", StringComparison.OrdinalIgnoreCase) ||
            c.Name.Contains("loki", StringComparison.OrdinalIgnoreCase));

        if (lokiComponent is null) return (null, "No Loki component found on this cluster.");
        if (lokiComponent.Status != ComponentStatus.Installed) return (null, "Loki is not installed.");

        LokiConfig config = BuildLokiConfig(lokiComponent);
        return (new ResolvedLokiInfo(cluster.Kubeconfig, config), null);
    }

    private static LokiConfig BuildLokiConfig(ClusterComponent component)
    {
        string ns = component.Namespace ?? "monitoring";
        string releaseName = component.ReleaseName ?? component.Name;

        LokiConfig derived = new()
        {
            Namespace   = ns,
            ServiceName = releaseName.Contains("loki", StringComparison.OrdinalIgnoreCase)
                ? releaseName : $"{releaseName}-loki"
        };

        if (string.IsNullOrWhiteSpace(component.Configuration)) return derived;
        try
        {
            LokiConfig? explicit_ = JsonSerializer.Deserialize<LokiConfig>(component.Configuration,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
            if (explicit_ is null) return derived;
            if (!string.IsNullOrWhiteSpace(explicit_.Namespace))   derived.Namespace   = explicit_.Namespace;
            if (!string.IsNullOrWhiteSpace(explicit_.ServiceName)) derived.ServiceName = explicit_.ServiceName;
            if (explicit_.ServicePort != 3100)                     derived.ServicePort = explicit_.ServicePort;
        }
        catch { }
        return derived;
    }

    private async Task<KubernetesOperationResult<T>> WithLokiAsync<T>(
        ResolvedLokiInfo info,
        Func<HttpClient, string, CancellationToken, Task<T>> action,
        string logContext,
        CancellationToken ct)
    {
        try
        {
            using MemoryStream stream = new(Encoding.UTF8.GetBytes(info.Kubeconfig));
            KubernetesClientConfiguration k8sConfig = KubernetesClientConfiguration.BuildConfigFromConfigFile(stream);
            using Kubernetes k8s = new(k8sConfig);

            V1EndpointAddress? addr = await FindLokiEndpointAsync(k8s, info.Config, ct);
            if (addr?.TargetRef is null)
                throw new InvalidOperationException(
                    $"No ready pods for Loki service {info.Config.ServiceName} in {info.Config.Namespace}.");

            string podName = addr.TargetRef.Name;
            string podNs   = addr.TargetRef.NamespaceProperty ?? info.Config.Namespace;
            string baseUrl = k8s.BaseUri.ToString().TrimEnd('/')
                + $"/api/v1/namespaces/{podNs}/pods/{podName}:{info.Config.ServicePort}/proxy";

            logger.LogDebug("Loki proxy → pod {Pod} ({PodNs}) at {BaseUrl}", podName, podNs, baseUrl);

            T result = await action(k8s.HttpClient, baseUrl, ct);
            return KubernetesOperationResult<T>.Success(result);
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Loki query failed ({Context})", logContext);
            return KubernetesOperationResult<T>.Failure(ex.Message);
        }
    }

    private async Task<V1EndpointAddress?> FindLokiEndpointAsync(
        Kubernetes k8s, LokiConfig config, CancellationToken ct)
    {
        try
        {
            V1Endpoints ep = await k8s.CoreV1.ReadNamespacedEndpointsAsync(
                config.ServiceName, config.Namespace, cancellationToken: ct);
            V1EndpointAddress? direct = ReadyPodAddress(ep, config.ServicePort);
            if (direct is not null) return direct;
        }
        catch (k8s.Autorest.HttpOperationException ex)
            when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound) { }

        // Fallback: scan namespace for anything named "loki"
        V1EndpointsList all = await k8s.CoreV1.ListNamespacedEndpointsAsync(
            config.Namespace, cancellationToken: ct);

        foreach (V1Endpoints candidate in all.Items.OrderBy(e => e.Metadata.Name))
        {
            if (!candidate.Metadata.Name.Contains("loki", StringComparison.OrdinalIgnoreCase)) continue;
            V1EndpointAddress? addr = ReadyPodAddress(candidate, config.ServicePort);
            if (addr is null) continue;
            logger.LogInformation("Auto-discovered Loki endpoint {Found}", candidate.Metadata.Name);
            return addr;
        }
        return null;
    }

    private static V1EndpointAddress? ReadyPodAddress(V1Endpoints ep, int port)
    {
        if (ep.Subsets is null) return null;
        foreach (V1EndpointSubset subset in ep.Subsets)
        {
            bool portMatches = subset.Ports?.Any(p => p.Port == port) ?? true;
            if (!portMatches) continue;
            V1EndpointAddress? addr = (subset.Addresses ?? [])
                .FirstOrDefault(a => a.TargetRef?.Kind == "Pod");
            if (addr is not null) return addr;
        }
        return null;
    }
}
