using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json.Serialization;
using StellaOps.Scanner.Sbomer.BuildXPlugin.Attestation;
using StellaOps.Scanner.Sbomer.BuildXPlugin.Cas;
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
using StellaOps.Scanner.Sbomer.BuildXPlugin.Manifest;

namespace StellaOps.Scanner.Sbomer.BuildXPlugin;

internal static class Program
{
    private static readonly JsonSerializerOptions ManifestPrintOptions = new(JsonSerializerDefaults.Web)
    {
        WriteIndented = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    private static readonly JsonSerializerOptions DescriptorJsonOptions = new(JsonSerializerDefaults.Web)
    {
        WriteIndented = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    private static async Task<int> Main(string[] args)
    {
        using var cancellation = new CancellationTokenSource();
        Console.CancelKeyPress += (_, eventArgs) =>
        {
            eventArgs.Cancel = true;
            cancellation.Cancel();
        };

        var command = args.Length > 0 ? args[0].ToLowerInvariant() : "handshake";
        var commandArgs = args.Skip(1).ToArray();

        try
        {
            return command switch
            {
                "handshake" => await RunHandshakeAsync(commandArgs, cancellation.Token).ConfigureAwait(false),
                "manifest" => await RunManifestAsync(commandArgs, cancellation.Token).ConfigureAwait(false),
                "descriptor" or "annotate" => await RunDescriptorAsync(commandArgs, cancellation.Token).ConfigureAwait(false),
                "version" => RunVersion(),
                "help" or "--help" or "-h" => PrintHelp(),
                _ => UnknownCommand(command)
            };
        }
        catch (OperationCanceledException)
        {
            Console.Error.WriteLine("Operation cancelled.");
            return 130;
        }
        catch (BuildxPluginException ex)
        {
            Console.Error.WriteLine(ex.Message);
            return 2;
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"Unhandled error: {ex}");
            return 1;
        }
    }

    private static async Task<int> RunHandshakeAsync(string[] args, CancellationToken cancellationToken)
    {
        var manifestDirectory = ResolveManifestDirectory(args);
        var loader = new BuildxPluginManifestLoader(manifestDirectory);
        var manifest = await loader.LoadDefaultAsync(cancellationToken).ConfigureAwait(false);

        var casRoot = ResolveCasRoot(args, manifest);
        var casClient = new LocalCasClient(new LocalCasOptions
        {
            RootDirectory = casRoot,
            Algorithm = "sha256"
        });

        var result = await casClient.VerifyWriteAsync(cancellationToken).ConfigureAwait(false);

        Console.WriteLine($"handshake ok: {manifest.Id}@{manifest.Version} → {result.Algorithm}:{result.Digest}");
        Console.WriteLine(result.Path);
        return 0;
    }

    private static async Task<int> RunManifestAsync(string[] args, CancellationToken cancellationToken)
    {
        var manifestDirectory = ResolveManifestDirectory(args);
        var loader = new BuildxPluginManifestLoader(manifestDirectory);
        var manifest = await loader.LoadDefaultAsync(cancellationToken).ConfigureAwait(false);

        var json = JsonSerializer.Serialize(manifest, ManifestPrintOptions);
        Console.WriteLine(json);
        return 0;
    }

    private static int RunVersion()
    {
        var assembly = Assembly.GetExecutingAssembly();
        var version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
                      ?? assembly.GetName().Version?.ToString()
                      ?? "unknown";
        Console.WriteLine(version);
        return 0;
    }

    private static int PrintHelp()
    {
        Console.WriteLine("StellaOps BuildX SBOM generator");
        Console.WriteLine("Usage:");
        Console.WriteLine("  stellaops-buildx [handshake|manifest|descriptor|version]");
        Console.WriteLine();
        Console.WriteLine("Commands:");
        Console.WriteLine("  handshake   Probe the local CAS and ensure manifests are discoverable.");
        Console.WriteLine("  manifest    Print the resolved manifest JSON.");
        Console.WriteLine("  descriptor  Emit OCI descriptor + provenance placeholder for the provided SBOM.");
        Console.WriteLine("  version     Print the plug-in version.");
        Console.WriteLine();
        Console.WriteLine("Options:");
        Console.WriteLine("  --manifest <path>          Override the manifest directory.");
        Console.WriteLine("  --cas <path>               Override the CAS root directory.");
        Console.WriteLine("  --image <digest>           (descriptor) Image digest the SBOM belongs to.");
        Console.WriteLine("  --sbom <path>              (descriptor) Path to the SBOM file to describe.");
        Console.WriteLine("  --attestor <url>           (descriptor) Optional Attestor endpoint for provenance placeholders.");
        Console.WriteLine("  --attestor-token <token>   Bearer token for Attestor requests (or STELLAOPS_ATTESTOR_TOKEN).");
        Console.WriteLine("  --attestor-insecure        Skip TLS verification for Attestor requests (dev/test only).");
        return 0;
    }

    private static int UnknownCommand(string command)
    {
        Console.Error.WriteLine($"Unknown command '{command}'. Use 'help' for usage.");
        return 1;
    }

    private static string ResolveManifestDirectory(string[] args)
    {
        var explicitPath = GetOption(args, "--manifest")
            ?? Environment.GetEnvironmentVariable("STELLAOPS_BUILDX_MANIFEST_DIR");

        if (!string.IsNullOrWhiteSpace(explicitPath))
        {
            return Path.GetFullPath(explicitPath);
        }

        var defaultDirectory = Path.Combine(AppContext.BaseDirectory, "plugins", "scanner", "buildx");
        if (Directory.Exists(defaultDirectory))
        {
            return defaultDirectory;
        }

        return AppContext.BaseDirectory;
    }

    private static string ResolveCasRoot(string[] args, BuildxPluginManifest manifest)
    {
        var overrideValue = GetOption(args, "--cas")
            ?? Environment.GetEnvironmentVariable("STELLAOPS_SCANNER_CAS_ROOT");

        if (!string.IsNullOrWhiteSpace(overrideValue))
        {
            return Path.GetFullPath(overrideValue);
        }

        var manifestDefault = manifest.Cas.DefaultRoot;
        if (!string.IsNullOrWhiteSpace(manifestDefault))
        {
            if (Path.IsPathRooted(manifestDefault))
            {
                return Path.GetFullPath(manifestDefault);
            }

            var baseDirectory = manifest.SourceDirectory ?? AppContext.BaseDirectory;
            return Path.GetFullPath(Path.Combine(baseDirectory, manifestDefault));
        }

        return Path.Combine(AppContext.BaseDirectory, "cas");
    }

    private static async Task<int> RunDescriptorAsync(string[] args, CancellationToken cancellationToken)
    {
        var imageDigest = RequireOption(args, "--image");
        var sbomPath = RequireOption(args, "--sbom");

        var sbomMediaType = GetOption(args, "--media-type") ?? "application/vnd.cyclonedx+json";
        var sbomFormat = GetOption(args, "--sbom-format") ?? "cyclonedx-json";
        var sbomKind = GetOption(args, "--sbom-kind") ?? "inventory";
        var artifactType = GetOption(args, "--artifact-type") ?? "application/vnd.stellaops.sbom.layer+json";
        var subjectMediaType = GetOption(args, "--subject-media-type") ?? "application/vnd.oci.image.manifest.v1+json";
        var predicateType = GetOption(args, "--predicate-type") ?? "https://slsa.dev/provenance/v1";
        var licenseId = GetOption(args, "--license-id") ?? Environment.GetEnvironmentVariable("STELLAOPS_LICENSE_ID");
        var repository = GetOption(args, "--repository");
        var buildRef = GetOption(args, "--build-ref");
        var sbomName = GetOption(args, "--sbom-name") ?? Path.GetFileName(sbomPath);

        var attestorUriText = GetOption(args, "--attestor") ?? Environment.GetEnvironmentVariable("STELLAOPS_ATTESTOR_URL");
        var attestorToken = GetOption(args, "--attestor-token") ?? Environment.GetEnvironmentVariable("STELLAOPS_ATTESTOR_TOKEN");
        var attestorInsecure = GetFlag(args, "--attestor-insecure")
            || string.Equals(Environment.GetEnvironmentVariable("STELLAOPS_ATTESTOR_INSECURE"), "true", StringComparison.OrdinalIgnoreCase);
        Uri? attestorUri = null;
        if (!string.IsNullOrWhiteSpace(attestorUriText))
        {
            attestorUri = new Uri(attestorUriText, UriKind.Absolute);
        }

        var assembly = Assembly.GetExecutingAssembly();
        var version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
                      ?? assembly.GetName().Version?.ToString()
                      ?? "0.0.0";

        var request = new DescriptorRequest
        {
            ImageDigest = imageDigest,
            SbomPath = sbomPath,
            SbomMediaType = sbomMediaType,
            SbomFormat = sbomFormat,
            SbomKind = sbomKind,
            SbomArtifactType = artifactType,
            SubjectMediaType = subjectMediaType,
            PredicateType = predicateType,
            GeneratorVersion = version,
            GeneratorName = assembly.GetName().Name,
            LicenseId = licenseId,
            SbomName = sbomName,
            Repository = repository,
            BuildRef = buildRef,
            AttestorUri = attestorUri?.ToString()
        }.Validate();

        var generator = new DescriptorGenerator(TimeProvider.System);
        var document = await generator.CreateAsync(request, cancellationToken).ConfigureAwait(false);

        if (attestorUri is not null)
        {
            using var httpClient = CreateAttestorHttpClient(attestorUri, attestorToken, attestorInsecure);
            var attestorClient = new AttestorClient(httpClient);
            await attestorClient.SendPlaceholderAsync(attestorUri, document, cancellationToken).ConfigureAwait(false);
        }

        var json = JsonSerializer.Serialize(document, DescriptorJsonOptions);
        Console.WriteLine(json);
        return 0;
    }

    private static string? GetOption(string[] args, string optionName)
    {
        for (var i = 0; i < args.Length; i++)
        {
            var argument = args[i];
            if (string.Equals(argument, optionName, StringComparison.OrdinalIgnoreCase))
            {
                if (i + 1 >= args.Length)
                {
                    throw new BuildxPluginException($"Option '{optionName}' requires a value.");
                }

                return args[i + 1];
            }

            if (argument.StartsWith(optionName + "=", StringComparison.OrdinalIgnoreCase))
            {
                return argument[(optionName.Length + 1)..];
            }
        }

        return null;
    }

    private static bool GetFlag(string[] args, string optionName)
    {
        foreach (var argument in args)
        {
            if (string.Equals(argument, optionName, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }

    private static string RequireOption(string[] args, string optionName)
    {
        var value = GetOption(args, optionName);
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new BuildxPluginException($"Option '{optionName}' is required.");
        }

        return value;
    }

    private static HttpClient CreateAttestorHttpClient(Uri attestorUri, string? bearerToken, bool insecure)
    {
        var handler = new HttpClientHandler
        {
            CheckCertificateRevocationList = true,
        };

        if (insecure && string.Equals(attestorUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
        {
#pragma warning disable S4830 // Explicitly gated by --attestor-insecure flag/env for dev/test usage.
            handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
#pragma warning restore S4830
        }

        var client = new HttpClient(handler, disposeHandler: true)
        {
            Timeout = TimeSpan.FromSeconds(30)
        };
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        if (!string.IsNullOrWhiteSpace(bearerToken))
        {
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
        }

        return client;
    }
}
