using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery;

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

public sealed class OciOpenVexAttestationConnectorOptions
{
    public const string HttpClientName = "excititor.connector.oci.openvex.attest";

    public IList<OciImageSubscriptionOptions> Images { get; } = new List<OciImageSubscriptionOptions>();

    public OciRegistryAuthenticationOptions Registry { get; } = new();

    public OciCosignVerificationOptions Cosign { get; } = new();

    public OciOfflineBundleOptions Offline { get; } = new();

    public TimeSpan DiscoveryCacheDuration { get; set; } = TimeSpan.FromMinutes(15);

    public int MaxParallelResolutions { get; set; } = 4;

    public bool AllowHttpRegistries { get; set; }

    public void Validate(IFileSystem? fileSystem = null)
    {
        if (Images.Count == 0)
        {
            throw new InvalidOperationException("At least one OCI image reference must be configured.");
        }

        foreach (var image in Images)
        {
            image.Validate();
        }

        if (MaxParallelResolutions <= 0 || MaxParallelResolutions > 32)
        {
            throw new InvalidOperationException("MaxParallelResolutions must be between 1 and 32.");
        }

        if (DiscoveryCacheDuration <= TimeSpan.Zero)
        {
            throw new InvalidOperationException("DiscoveryCacheDuration must be a positive time span.");
        }

        Registry.Validate();
        Cosign.Validate(fileSystem);
        Offline.Validate(fileSystem);

        if (!AllowHttpRegistries && Images.Any(i => i.Reference is not null && i.Reference.StartsWith("http://", StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException("HTTP (non-TLS) registries are disabled. Enable AllowHttpRegistries to permit them.");
        }
    }
}

public sealed class OciImageSubscriptionOptions
{
    private OciImageReference? _parsedReference;

    /// <summary>
    /// Gets or sets the OCI reference (e.g. registry.example.com/repository:tag or registry.example.com/repository@sha256:abcdef).
    /// </summary>
    public string? Reference { get; set; }

    /// <summary>
    /// Optional friendly name used in logs when referencing this subscription.
    /// </summary>
    public string? DisplayName { get; set; }

    /// <summary>
    /// Optional file path for an offline attestation bundle associated with this image.
    /// </summary>
    public string? OfflineBundlePath { get; set; }

    /// <summary>
    /// Optional override for the expected subject digest. When provided, discovery will verify resolved digests match.
    /// </summary>
    public string? ExpectedSubjectDigest { get; set; }

    internal OciImageReference? ParsedReference => _parsedReference;

    public void Validate()
    {
        if (string.IsNullOrWhiteSpace(Reference))
        {
            throw new InvalidOperationException("Image Reference is required for OCI OpenVEX attestation connector.");
        }

        _parsedReference = OciImageReferenceParser.Parse(Reference);

        if (!string.IsNullOrWhiteSpace(ExpectedSubjectDigest))
        {
            if (!ExpectedSubjectDigest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException("ExpectedSubjectDigest must start with 'sha256:'.");
            }

            if (ExpectedSubjectDigest.Length != "sha256:".Length + 64)
            {
                throw new InvalidOperationException("ExpectedSubjectDigest must contain a 64-character hexadecimal hash.");
            }
        }
    }
}

public sealed class OciRegistryAuthenticationOptions
{
    /// <summary>
    /// Optional registry authority filter (e.g. registry.example.com:5000). When set it must match image references.
    /// </summary>
    public string? RegistryAuthority { get; set; }

    public string? Username { get; set; }

    public string? Password { get; set; }

    public string? IdentityToken { get; set; }

    public string? RefreshToken { get; set; }

    public bool AllowAnonymousFallback { get; set; } = true;

    public void Validate()
    {
        var hasUser = !string.IsNullOrWhiteSpace(Username);
        var hasPassword = !string.IsNullOrWhiteSpace(Password);
        var hasIdentityToken = !string.IsNullOrWhiteSpace(IdentityToken);
        var hasRefreshToken = !string.IsNullOrWhiteSpace(RefreshToken);

        if (hasIdentityToken && (hasUser || hasPassword))
        {
            throw new InvalidOperationException("IdentityToken cannot be combined with Username/Password for OCI registry authentication.");
        }

        if (hasRefreshToken && (hasUser || hasPassword))
        {
            throw new InvalidOperationException("RefreshToken cannot be combined with Username/Password for OCI registry authentication.");
        }

        if (hasUser != hasPassword)
        {
            throw new InvalidOperationException("Username and Password must be provided together for OCI registry authentication.");
        }

        if (!string.IsNullOrWhiteSpace(RegistryAuthority) && RegistryAuthority.Contains('/', StringComparison.Ordinal))
        {
            throw new InvalidOperationException("RegistryAuthority must not contain path segments.");
        }
    }
}

public sealed class OciCosignVerificationOptions
{
    public CosignCredentialMode Mode { get; set; } = CosignCredentialMode.Keyless;

    public CosignKeylessOptions Keyless { get; } = new();

    public CosignKeyPairOptions KeyPair { get; } = new();

    public bool RequireSignature { get; set; } = true;

    public TimeSpan VerifyTimeout { get; set; } = TimeSpan.FromSeconds(30);

    public void Validate(IFileSystem? fileSystem = null)
    {
        if (VerifyTimeout <= TimeSpan.Zero)
        {
            throw new InvalidOperationException("VerifyTimeout must be a positive time span.");
        }

        switch (Mode)
        {
            case CosignCredentialMode.None:
                break;

            case CosignCredentialMode.Keyless:
                Keyless.Validate();
                break;

            case CosignCredentialMode.KeyPair:
                KeyPair.Validate(fileSystem);
                break;

            default:
                throw new InvalidOperationException($"Unsupported Cosign credential mode '{Mode}'.");
        }
    }
}

public enum CosignCredentialMode
{
    None = 0,
    Keyless = 1,
    KeyPair = 2,
}

public sealed class CosignKeylessOptions
{
    public string? Issuer { get; set; }

    public string? Subject { get; set; }

    public string? FulcioUrl { get; set; } = "https://fulcio.sigstore.dev";

    public string? RekorUrl { get; set; } = "https://rekor.sigstore.dev";

    public string? ClientId { get; set; }

    public string? ClientSecret { get; set; }

    public string? Audience { get; set; }

    public string? IdentityToken { get; set; }

    public void Validate()
    {
        if (string.IsNullOrWhiteSpace(Issuer))
        {
            throw new InvalidOperationException("Cosign keyless Issuer must be provided.");
        }

        if (string.IsNullOrWhiteSpace(Subject))
        {
            throw new InvalidOperationException("Cosign keyless Subject must be provided.");
        }

        if (!string.IsNullOrWhiteSpace(FulcioUrl) && !Uri.TryCreate(FulcioUrl, UriKind.Absolute, out var fulcio))
        {
            throw new InvalidOperationException("FulcioUrl must be an absolute URI when provided.");
        }

        if (!string.IsNullOrWhiteSpace(RekorUrl) && !Uri.TryCreate(RekorUrl, UriKind.Absolute, out var rekor))
        {
            throw new InvalidOperationException("RekorUrl must be an absolute URI when provided.");
        }

        if (!string.IsNullOrWhiteSpace(ClientSecret) && string.IsNullOrWhiteSpace(ClientId))
        {
            throw new InvalidOperationException("Cosign keyless ClientId must be provided when ClientSecret is specified.");
        }
    }
}

public sealed class CosignKeyPairOptions
{
    public string? PrivateKeyPath { get; set; }

    public string? Password { get; set; }

    public string? CertificatePath { get; set; }

    public string? RekorUrl { get; set; }

    public string? FulcioRootPath { get; set; }

    public void Validate(IFileSystem? fileSystem = null)
    {
        if (string.IsNullOrWhiteSpace(PrivateKeyPath))
        {
            throw new InvalidOperationException("PrivateKeyPath must be provided for Cosign key pair mode.");
        }

        var fs = fileSystem ?? new FileSystem();
        if (!fs.File.Exists(PrivateKeyPath))
        {
            throw new InvalidOperationException($"Cosign private key file not found: {PrivateKeyPath}");
        }

        if (!string.IsNullOrWhiteSpace(CertificatePath) && !fs.File.Exists(CertificatePath))
        {
            throw new InvalidOperationException($"Cosign certificate file not found: {CertificatePath}");
        }

        if (!string.IsNullOrWhiteSpace(FulcioRootPath) && !fs.File.Exists(FulcioRootPath))
        {
            throw new InvalidOperationException($"Cosign Fulcio root file not found: {FulcioRootPath}");
        }

        if (!string.IsNullOrWhiteSpace(RekorUrl) && !Uri.TryCreate(RekorUrl, UriKind.Absolute, out _))
        {
            throw new InvalidOperationException("RekorUrl must be an absolute URI when provided for Cosign key pair mode.");
        }
    }
}

public sealed class OciOfflineBundleOptions
{
    public string? RootDirectory { get; set; }

    public bool PreferOffline { get; set; }

    public bool AllowNetworkFallback { get; set; } = true;

    public string? DefaultBundleFileName { get; set; } = "openvex-attestations.tgz";

    public bool RequireBundles { get; set; }

    public void Validate(IFileSystem? fileSystem = null)
    {
        if (string.IsNullOrWhiteSpace(RootDirectory))
        {
            return;
        }

        var fs = fileSystem ?? new FileSystem();
        if (!fs.Directory.Exists(RootDirectory))
        {
            if (PreferOffline || RequireBundles)
            {
                throw new InvalidOperationException($"Offline bundle root directory '{RootDirectory}' does not exist.");
            }

            fs.Directory.CreateDirectory(RootDirectory);
        }
    }
}
