// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;

namespace NuGet.Packaging.Signing
{
    public class PackageSignatureVerifier : IPackageSignatureVerifier
    {
        private readonly List<ISignatureVerificationProvider> _verificationProviders;

        public PackageSignatureVerifier(IEnumerable<ISignatureVerificationProvider> verificationProviders)
        {
            _verificationProviders = verificationProviders?.ToList() ?? throw new ArgumentNullException(nameof(verificationProviders));
        }

        public async Task<VerifySignaturesResult> VerifySignaturesAsync(ISignedPackageReader package, SignedPackageVerifierSettings settings, CancellationToken token, Guid parentId = default(Guid))
        {
            if (package == null)
            {
                throw new ArgumentNullException(nameof(package));
            }

            if (settings == null)
            {
                throw new ArgumentNullException(nameof(settings));
            }

            var valid = false;
            var trustResults = new List<PackageVerificationResult>();

            var packageSigningTelemetryEvent = new PackageSigningTelemetryEvent();
            using (var telemetry = TelemetryActivity.Create(parentId, packageSigningTelemetryEvent))
            {
                var isSigned = await package.IsSignedAsync(token);
                if (isSigned)
                {
                    try
                    {
                        var signature = await package.GetPrimarySignatureAsync(token);

                        if (signature != null)
                        {
                            // Verify that the signature is trusted
                            var sigTrustResults = await Task.WhenAll(_verificationProviders.Select(e => e.GetTrustResultAsync(package, signature, settings, token)));
                            valid = IsValid(sigTrustResults, settings);
                            trustResults.AddRange(sigTrustResults);
                        }
                        else
                        {
                            valid = false;
                        }
                    }
                    catch (SignatureException e)
                    {
                        // SignatureException generated while parsing signatures
                        var issues = new[] {
                            SignatureLog.Issue(!settings.AllowIllegal, e.Code, e.Message),
                            SignatureLog.DebugLog(e.ToString())
                        };
                        trustResults.Add(new InvalidSignaturePackageVerificationResult(
                            settings.AllowIllegal ? SignatureVerificationStatus.Valid : SignatureVerificationStatus.Disallowed,
                            issues));
                        valid = settings.AllowIllegal;
                    }
                    catch (CryptographicException e)
                    {
                        // CryptographicException generated while parsing the SignedCms object
                        var issues = new[] {
                            SignatureLog.Issue(!settings.AllowIllegal, NuGetLogCode.NU3003, Strings.ErrorPackageSignatureInvalid),
                            SignatureLog.DebugLog(e.ToString())
                        };
                        trustResults.Add(new InvalidSignaturePackageVerificationResult(
                            settings.AllowIllegal ? SignatureVerificationStatus.Valid : SignatureVerificationStatus.Disallowed,
                            issues));
                        valid = settings.AllowIllegal;
                    }
                }
                else if (settings.AllowUnsigned)
                {
                    // An unsigned package is valid only if unsigned packages are allowed.
                    valid = true;
                }
                else
                {
                    var issues = new[] { SignatureLog.Issue(fatal: true, code: NuGetLogCode.NU3004, message: Strings.ErrorPackageNotSigned) };
                    trustResults.Add(new UnsignedPackageVerificationResult(
                        settings.AllowIllegal ? SignatureVerificationStatus.Valid : SignatureVerificationStatus.Disallowed,
                        issues));
                    valid = false;
                }

                var status = valid ? NuGetOperationStatus.Succeeded : NuGetOperationStatus.Failed;
                packageSigningTelemetryEvent.SetResult(isSigned ? PackageSignType.Signed : PackageSignType.Unsigned, status);

                return new VerifySignaturesResult(valid, isSigned, trustResults);
            }
        }

        /// <summary>
        /// True if a provider trusts the package signature.
        /// </summary>
        private static bool IsValid(IEnumerable<PackageVerificationResult> verificationResults, SignedPackageVerifierSettings settings)
        {
            return verificationResults.Any() &&
                verificationResults.All(e => e.Trust == SignatureVerificationStatus.Valid);
        }
    }
}
