using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO.Abstractions;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Authentication;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Configuration;

namespace StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery;

public sealed class OciAttestationDiscoveryService
{
    private readonly IMemoryCache _memoryCache;
    private readonly IFileSystem _fileSystem;
    private readonly ILogger<OciAttestationDiscoveryService> _logger;

    public OciAttestationDiscoveryService(
        IMemoryCache memoryCache,
        IFileSystem fileSystem,
        ILogger<OciAttestationDiscoveryService> logger)
    {
        _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
        _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public Task<OciAttestationDiscoveryResult> LoadAsync(
        OciOpenVexAttestationConnectorOptions options,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(options);

        cancellationToken.ThrowIfCancellationRequested();

        var cacheKey = CreateCacheKey(options);
        if (_memoryCache.TryGetValue(cacheKey, out OciAttestationDiscoveryResult? cached) && cached is not null)
        {
            _logger.LogDebug("Using cached OCI attestation discovery result for {ImageCount} images.", cached.Targets.Length);
            return Task.FromResult(cached);
        }

        var targets = new List<OciAttestationTarget>(options.Images.Count);
        foreach (var image in options.Images)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var parsed = image.ParsedReference ?? OciImageReferenceParser.Parse(image.Reference!);
            var offlinePath = ResolveOfflinePath(options, image, parsed);

            OciOfflineBundleReference? offline = null;
            if (!string.IsNullOrWhiteSpace(offlinePath))
            {
                var fullPath = _fileSystem.Path.GetFullPath(offlinePath!);
                var exists = _fileSystem.File.Exists(fullPath) || _fileSystem.Directory.Exists(fullPath);

                if (!exists && options.Offline.RequireBundles)
                {
                    throw new InvalidOperationException($"Required offline bundle '{fullPath}' for reference '{parsed.Canonical}' was not found.");
                }

                offline = new OciOfflineBundleReference(fullPath, exists, image.ExpectedSubjectDigest);
            }

            targets.Add(new OciAttestationTarget(parsed, image.ExpectedSubjectDigest, offline));
        }

        var authorization = OciRegistryAuthorization.Create(options.Registry);
        var cosignAuthority = OciCosignAuthorityFactory.Create(options.Cosign, _fileSystem);

        var result = new OciAttestationDiscoveryResult(
            targets.ToImmutableArray(),
            authorization,
            cosignAuthority,
            options.Offline.PreferOffline,
            options.Offline.AllowNetworkFallback);

        _memoryCache.Set(cacheKey, result, options.DiscoveryCacheDuration);

        return Task.FromResult(result);
    }

    private string? ResolveOfflinePath(
        OciOpenVexAttestationConnectorOptions options,
        OciImageSubscriptionOptions image,
        OciImageReference parsed)
    {
        if (!string.IsNullOrWhiteSpace(image.OfflineBundlePath))
        {
            return image.OfflineBundlePath;
        }

        if (string.IsNullOrWhiteSpace(options.Offline.RootDirectory))
        {
            return null;
        }

        var root = options.Offline.RootDirectory!;
        var segments = new List<string> { SanitizeSegment(parsed.Registry) };

        var repositoryParts = parsed.Repository.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (repositoryParts.Length == 0)
        {
            segments.Add(SanitizeSegment(parsed.Repository));
        }
        else
        {
            foreach (var part in repositoryParts)
            {
                segments.Add(SanitizeSegment(part));
            }
        }

        var versionSegment = parsed.Digest is not null
            ? SanitizeSegment(parsed.Digest)
            : SanitizeSegment(parsed.Tag ?? "latest");

        segments.Add(versionSegment);

        var combined = _fileSystem.Path.Combine(new[] { root }.Concat(segments).ToArray());

        if (!string.IsNullOrWhiteSpace(options.Offline.DefaultBundleFileName))
        {
            combined = _fileSystem.Path.Combine(combined, options.Offline.DefaultBundleFileName!);
        }

        return combined;
    }

    private static string SanitizeSegment(string value)
    {
        if (string.IsNullOrEmpty(value))
        {
            return "_";
        }

        var builder = new StringBuilder(value.Length);
        foreach (var ch in value)
        {
            if (char.IsLetterOrDigit(ch) || ch is '-' or '_' or '.')
            {
                builder.Append(ch);
            }
            else
            {
                builder.Append('_');
            }
        }

        return builder.Length == 0 ? "_" : builder.ToString();
    }

    private static string CreateCacheKey(OciOpenVexAttestationConnectorOptions options)
    {
        using var sha = SHA256.Create();
        var builder = new StringBuilder();
        builder.AppendLine("oci-openvex-attest");
        builder.AppendLine(options.MaxParallelResolutions.ToString());
        builder.AppendLine(options.AllowHttpRegistries.ToString());
        builder.AppendLine(options.Offline.PreferOffline.ToString());
        builder.AppendLine(options.Offline.AllowNetworkFallback.ToString());

        foreach (var image in options.Images)
        {
            builder.AppendLine(image.Reference ?? string.Empty);
            builder.AppendLine(image.ExpectedSubjectDigest ?? string.Empty);
            builder.AppendLine(image.OfflineBundlePath ?? string.Empty);
        }

        if (!string.IsNullOrWhiteSpace(options.Offline.RootDirectory))
        {
            builder.AppendLine(options.Offline.RootDirectory);
            builder.AppendLine(options.Offline.DefaultBundleFileName ?? string.Empty);
        }

        builder.AppendLine(options.Registry.RegistryAuthority ?? string.Empty);
        builder.AppendLine(options.Registry.AllowAnonymousFallback.ToString());

        var bytes = Encoding.UTF8.GetBytes(builder.ToString());
        var hashBytes = sha.ComputeHash(bytes);
        return Convert.ToHexString(hashBytes);
    }
}
