// Copyright (C) 2026, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System.Security.Cryptography.X509Certificates;
using Duplicati.Library.AutoUpdater;
using Duplicati.Library.Logging;

namespace Duplicati.Library.Certificates;

/// <summary>
/// Result of a certificate renewal check operation.
/// </summary>
public class CertificateRenewalResult
{
    /// <summary>
    /// Gets or sets whether renewal was performed.
    /// </summary>
    public bool Renewed { get; set; }

    /// <summary>
    /// Gets or sets the renewed server certificate data.
    /// </summary>
    public ServerCertificateData? RenewedCertificate { get; set; }

    /// <summary>
    /// Gets or sets the renewal failure reason, if any.
    /// </summary>
    public string? RenewalFailedReason { get; set; }
}

/// <summary>
/// Result of certificate generation operation.
/// </summary>
public class CertificateGenerationResult
{
    /// <summary>
    /// Gets or sets whether the operation was successful.
    /// </summary>
    public bool Success { get; set; }

    /// <summary>
    /// Gets or sets the generated CA certificate data.
    /// </summary>
    public CACertificateData? CACertificate { get; set; }

    /// <summary>
    /// Gets or sets the generated server certificate data.
    /// </summary>
    public ServerCertificateData? ServerCertificate { get; set; }

    /// <summary>
    /// Gets or sets the trust installation status, if trust installation was attempted.
    /// </summary>
    public CATrustInstallationStatus? TrustInstallationStatus { get; set; }

    /// <summary>
    /// Gets or sets an error message if the operation failed.
    /// </summary>
    public string? ErrorMessage { get; set; }
}

/// <summary>
/// Data transfer object for CA certificate data.
/// </summary>
public class CACertificateData
{
    /// <summary>
    /// The CA certificate in Base64 format.
    /// </summary>
    public string CACertificate { get; set; } = string.Empty;

    /// <summary>
    /// The encrypted CA private key.
    /// </summary>
    public string CAKey { get; set; } = string.Empty;

    /// <summary>
    /// The password for the CA key.
    /// </summary>
    public string CAPassword { get; set; } = string.Empty;
}

/// <summary>
/// Data transfer object for server certificate data.
/// </summary>
public class ServerCertificateData
{
    /// <summary>
    /// The server certificate in Base64 PFX format.
    /// </summary>
    public string ServerCertificate { get; set; } = string.Empty;

    /// <summary>
    /// The password for the server certificate PFX.
    /// </summary>
    public string Password { get; set; } = string.Empty;

    /// <summary>
    /// Whether the certificate was autogenerated.
    /// </summary>
    public bool Autogenerated { get; set; }
}

/// <summary>
/// Checks and performs automatic certificate renewal for HTTPS certificates.
/// </summary>
public static class CertificateRenewalChecker
{
    /// <summary>
    /// The number of days before expiration to trigger renewal.
    /// </summary>
    public const int RENEWAL_THRESHOLD_DAYS = 30;

    /// <summary>
    /// The log tag for messages from this class.
    /// </summary>
    private static readonly string LOGTAG = Log.LogTagFromType(typeof(CertificateRenewalChecker));

    /// <summary>
    /// Checks if the server certificate needs renewal and performs renewal if necessary.
    /// Uses <see cref="CertificateConfigurationHelper.RenewServerCertificate"/> for the actual renewal.
    /// </summary>
    /// <param name="isAutogenerated">Whether the certificate was autogenerated.</param>
    /// <param name="serverCert">The server certificate collection.</param>
    /// <param name="caData">The CA certificate data.</param>
    /// <param name="logger">The logger to use for output. If null, uses the default log.</param>
    /// <returns>A result object containing renewal status and new certificate data if renewed.</returns>
    public static CertificateRenewalResult CheckAndRenewIfNeeded(
        bool isAutogenerated,
        X509Certificate2Collection? serverCert,
        CACertificateData? caData,
        ILogDestination? logger = null)
    {
        var result = new CertificateRenewalResult();

        try
        {
            // Check if certificate was autogenerated
            if (!isAutogenerated)
            {
                Log.WriteVerboseMessage(LOGTAG, "CertificateNotAutogenerated", "SSL certificate was not autogenerated, skipping renewal check.");
                return result;
            }

            // Get the server certificate
            if (serverCert == null || serverCert.Count == 0)
            {
                Log.WriteVerboseMessage(LOGTAG, "NoCertificateFound", "No SSL certificate found in database, skipping renewal check.");
                return result;
            }

            // Get the first certificate with a private key from the collection
            var serverCertWithKey = serverCert.Cast<X509Certificate2>().FirstOrDefault(c => c.HasPrivateKey);
            if (serverCertWithKey == null)
            {
                Log.WriteWarningMessage(LOGTAG, "CertificateWithoutPrivateKey", null, "SSL certificate found but it does not have a private key, skipping renewal check.");
                return result;
            }

            // Check if certificate is expiring soon
            if (!CertificateGenerator.IsExpiringSoon(serverCertWithKey, RENEWAL_THRESHOLD_DAYS))
            {
                var daysUntilExpiry = (serverCertWithKey.NotAfter - DateTime.UtcNow).TotalDays;
                Log.WriteVerboseMessage(LOGTAG, "CertificateValid", $"SSL certificate is valid (expires in {daysUntilExpiry:F0} days), no renewal needed.");
                return result;
            }

            Log.WriteInformationMessage(LOGTAG, "CertificateExpiring", $"SSL certificate expires in {(serverCertWithKey.NotAfter - DateTime.UtcNow).TotalDays:F0} days, attempting renewal...");

            // Check CA data
            if (caData == null)
            {
                Log.WriteErrorMessage(LOGTAG, "CANotFound", null, "Cannot renew certificate: CA certificate data not found in database.");
                result.RenewalFailedReason = "CA certificate data not found in database.";
                return result;
            }

            // Delegate to the shared renewal logic
            return CertificateConfigurationHelper.RenewServerCertificate(caData);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "RenewalFailed", ex, $"Failed to renew SSL certificate: {ex.Message}");
            result.RenewalFailedReason = ex.Message;
            return result;
        }
    }

    /// <summary>
    /// Checks if certificates exist and are valid (not expiring within the threshold).
    /// </summary>
    /// <param name="isAutogenerated">Whether the certificate was autogenerated.</param>
    /// <param name="serverCert">The server certificate collection.</param>
    /// <param name="caData">The CA certificate data.</param>
    /// <returns>True if valid certificates exist, false otherwise.</returns>
    public static bool HasValidCertificates(bool isAutogenerated, X509Certificate2Collection? serverCert, CACertificateData? caData)
    {
        try
        {
            // Check if certificate was autogenerated
            if (!isAutogenerated)
                return false;

            // Get the server certificate
            if (serverCert == null || serverCert.Count == 0)
                return false;

            // Get the first certificate with a private key from the collection
            var serverCertWithKey = serverCert.Cast<X509Certificate2>().FirstOrDefault(c => c.HasPrivateKey);
            if (serverCertWithKey == null)
                return false;

            // Check if CA data exists
            if (caData == null)
                return false;

            // Check if certificate is expiring soon
            if (CertificateGenerator.IsExpiringSoon(serverCertWithKey, RENEWAL_THRESHOLD_DAYS))
                return false;

            return true;
        }
        catch
        {
            return false;
        }
    }

    /// <summary>
    /// Ensures HTTPS certificates are available and valid.
    /// If valid certificates already exist, returns success without changes.
    /// If a CA exists but the server certificate is expiring, renews the server certificate using the existing CA.
    /// If no CA exists, generates new CA and server certificates.
    /// Delegates to <see cref="CertificateConfigurationHelper.EnsureCertificates"/>.
    /// </summary>
    /// <param name="existingIsAutogenerated">Whether existing certificate was autogenerated.</param>
    /// <param name="existingServerCert">The existing server certificate collection.</param>
    /// <param name="existingCaData">The existing CA certificate data.</param>
    /// <param name="hostnames">Optional comma-separated list of hostnames. If null, auto-detected hostnames are used.</param>
    /// <param name="skipTrustInstallation">If true, skips installing the CA in the system trust store.</param>
    /// <returns>A result object containing success status and certificate data.</returns>
    public static CertificateGenerationResult GenerateAndInstallCertificates(
        bool existingIsAutogenerated,
        X509Certificate2Collection? existingServerCert,
        CACertificateData? existingCaData,
        string? hostnames = null,
        bool skipTrustInstallation = false
    )
    {
        return CertificateConfigurationHelper.EnsureCertificates(
            existingIsAutogenerated,
            existingServerCert,
            existingCaData,
            hostnames,
            skipTrustInstallation);
    }

    /// <summary>
    /// Parses the hostnames option into a list of hostnames.
    /// When custom hostnames are provided, always includes localhost variants.
    /// </summary>
    /// <param name="hostnames">Comma-separated list of hostnames, or null to auto-detect.</param>
    /// <returns>List of hostnames.</returns>
    public static List<string> ParseHostnames(string? hostnames)
    {
        if (!string.IsNullOrWhiteSpace(hostnames))
        {
            var parsed = hostnames.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                .Where(h => !string.IsNullOrWhiteSpace(h))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();

            // Always include localhost variants
            foreach (var hostname in HostnameDetector.AlwaysIncludedHostnames)
                if (!parsed.Contains(hostname, StringComparer.OrdinalIgnoreCase))
                    parsed.Add(hostname);

            return parsed;
        }

        return HostnameDetector.DetectHostnames();
    }
}
