// Copyright (C) 2025, 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 Duplicati.Library.Interface;
using Duplicati.Library.Main.Volumes;
using Duplicati.Server.Database;
using Duplicati.WebserverCore.Abstractions;
using Duplicati.WebserverCore.Dto.V2;
using Duplicati.WebserverCore.Endpoints.Shared;
using Microsoft.AspNetCore.Mvc;
using InvalidCertificateException = Duplicati.Library.Utility.SslCertificateValidator.InvalidCertificateException;

namespace Duplicati.WebserverCore.Endpoints.V2.Backup;

public class DestinationVerify : IEndpointV2
{
    public static void Map(RouteGroupBuilder group)
    {
        group.MapPost("/destination/test", ([FromServices] Connection connection, [FromServices] IApplicationSettings applicationSettings, [FromBody] Dto.V2.DestinationTestRequestDto input, CancellationToken cancelToken)
            => ExecuteTest(connection, applicationSettings, input, cancelToken))
            .RequireAuthorization();
    }

    private static async Task<DestinationTestResponseDto> ExecuteTest(Connection connection, IApplicationSettings applicationSettings, DestinationTestRequestDto input, CancellationToken cancelToken)
    {
        var destinationType = input.DestinationType ?? RemoteDestinationType.Backend;

        try
        {
            if (destinationType == RemoteDestinationType.SourceProvider)
            {
                using var wrapper = await SharedRemoteOperation.GetSourceProviderForTesting(connection, applicationSettings, input.DestinationUrl, input.BackupId, input.ConnectionStringId ?? -1, input.SourcePrefix, cancelToken);

                // We do not call TestAsync here, because we may have read-only access to the source, and test will verify write access
                // Instead we just check if we can enumerate the files, which is the main thing we need to verify for a source provider

                // Technically we also count folders as files here, but really we just want to know if there is data to backup
                var anyFiles = await wrapper.SourceProvider.Enumerate(cancelToken).AnyAsync(cancelToken);

                return DestinationTestResponseDto.Create(
                    anyFiles: anyFiles,
                    anyBackups: false,
                    anyEncryptedFiles: false
                );
            }
            else if (destinationType == RemoteDestinationType.RestoreDestinationProvider)
            {
                using var wrapper = await SharedRemoteOperation.GetRestoreDestinationProviderForTesting(connection, applicationSettings, input.DestinationUrl, input.BackupId, input.ConnectionStringId ?? -1, input.SourcePrefix, cancelToken);

                // Here we do call TestAsync, because we need to verify write access
                await wrapper.RestoreDestinationProvider.Test(cancelToken);

                return DestinationTestResponseDto.Create(
                    anyFiles: true,
                    anyBackups: false,
                    anyEncryptedFiles: false
                );
            }
            else
            {
                using var wrapper = await SharedRemoteOperation.GetBackend(connection, applicationSettings, input.DestinationUrl, input.BackupId, input.ConnectionStringId ?? -1, cancelToken);

                using (var b = wrapper.Backend)
                {
                    try { await b.TestAsync(cancelToken).ConfigureAwait(false); }
                    catch (Exception ex) when (SharedRemoteOperation.GetInnerException<FolderMissingException>(ex) is FolderMissingException)
                    {
                        if (!input.AutoCreate)
                            throw;

                        await b.CreateFolderAsync(cancelToken).ConfigureAwait(false);
                        await b.TestAsync(cancelToken).ConfigureAwait(false);
                    }

                    var anyFiles = false;
                    var anyBackups = false;
                    var anyEncryptedFiles = false;
                    await foreach (var f in b.ListAsync(cancelToken).ConfigureAwait(false))
                    {
                        if (f.IsFolder)
                            continue;

                        anyFiles = true;
                        var parsed = VolumeBase.ParseFilename(f.Name);
                        if (parsed != null)
                        {
                            anyBackups = true;
                            anyEncryptedFiles = !string.IsNullOrWhiteSpace(parsed.EncryptionModule);
                            break;
                        }
                    }

                    return DestinationTestResponseDto.Create(
                        anyFiles,
                        anyBackups,
                        anyEncryptedFiles
                    );
                }
            }
        }
        catch (Exception ex) when (SharedRemoteOperation.GetInnerException<FolderMissingException>(ex) is FolderMissingException)
        {
            if (input.AutoCreate)
                return DestinationTestResponseDto.Failure(
                    "Failed to create folder",
                    "error-creating-folder",
                    folderExists: false,
                    afterConnect: ex is TestAfterConnectException
                );

            return DestinationTestResponseDto.Failure(
                "Folder does not exist",
                "missing-folder",
                folderExists: false,
                afterConnect: ex is TestAfterConnectException
            );
        }
        catch (Exception ex) when (SharedRemoteOperation.GetInnerException<InvalidCertificateException>(ex) is InvalidCertificateException icex)
        {
            if (string.IsNullOrWhiteSpace(icex.Certificate))
                return DestinationTestResponseDto.Failure(
                    icex.Message,
                    "CertError",
                    afterConnect: ex is TestAfterConnectException
                );

            return DestinationTestResponseDto.Failure(
                "Host certificate error",
                "incorrect-cert",
                afterConnect: ex is TestAfterConnectException,
                hostCertificate: icex.Certificate
            );
        }
        catch (Exception ex) when (SharedRemoteOperation.GetInnerException<Library.Utility.HostKeyException>(ex) is Library.Utility.HostKeyException hex)
        {
            if (string.IsNullOrWhiteSpace(hex.ReportedHostKey))
                return DestinationTestResponseDto.Failure(
                    hex.Message,
                    "KeyError",
                    afterConnect: ex is TestAfterConnectException
                );

            return DestinationTestResponseDto.Failure(
                "Host key error",
                "incorrect-host-key",
                afterConnect: ex is TestAfterConnectException,
                reportedHostKey: hex.ReportedHostKey,
                acceptedHostKey: hex.AcceptedHostKey
            );
        }
        catch (TestAfterConnectException tcex)
        {
            return DestinationTestResponseDto.Failure(
                tcex.Message,
                tcex.HelpID,
                afterConnect: true
            );
        }
        catch (UserInformationException uex)
        {
            return DestinationTestResponseDto.Failure(
                uex.Message,
                uex.HelpID
            );
        }
        catch (Exception ex)
        {
            return DestinationTestResponseDto.Failure(
                ex.Message,
                "error"
            );
        }
    }
}