using System.Text.Json;
using System.Text.Json.Serialization;
using Duplicati.Library.Interface;
using Duplicati.Library.RestAPI.Abstractions;
using Duplicati.Server;
using Duplicati.Server.Database;
using Duplicati.Server.Serialization;
using Duplicati.Server.Serialization.Interface;
using Duplicati.WebserverCore.Abstractions;
using Duplicati.WebserverCore.Exceptions;
using Microsoft.AspNetCore.Mvc;

namespace Duplicati.WebserverCore.Endpoints.V1.Backup;

public class BackupGet : IEndpointV1
{
    private record GetBackupResultDto(Dto.ScheduleDto? Schedule, Dto.BackupDto Backup, Dictionary<string, string> DisplayNames);

    public static void Map(RouteGroupBuilder group)
    {
        group.MapGet("/backup/{id}", ([FromServices] Connection connection, [FromRoute] string id)
            => ExecuteGet(connection, GetBackup(connection, id)))
            .RequireAuthorization();

        group.MapGet("/backup/{id}/files", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery] string? filter, [FromQuery] string? time, [FromQuery(Name = "all-versions")] bool? allVersions, [FromQuery(Name = "prefix-only")] bool? prefixOnly, [FromQuery(Name = "folder-contents")] bool? folderContents)
            => ExecuteGetFiles(GetBackup(connection, id), filter, time, allVersions ?? false, prefixOnly ?? false, folderContents ?? false, new Dictionary<string, string>()))
            .RequireAuthorization();

        group.MapGet("/backup/{id}/log", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery] long? offset, [FromQuery] long? pagesize)
            => ExecuteGetLog(connection, GetBackup(connection, id), offset, pagesize ?? 100))
            .RequireAuthorization();

        group.MapGet("/backup/{id}/remotelog", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery] long? offset, [FromQuery] long? pagesize)
            => ExecuteGetRemotelog(connection, GetBackup(connection, id), offset, pagesize ?? 100))
            .RequireAuthorization();

        group.MapGet("/backup/{id}/filesets", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "include-metadata")] bool? includeMetadata, [FromQuery(Name = "from-remote-only")] bool? fromRemoteOnly)
            => ExecuteGetFilesets(GetBackup(connection, id), includeMetadata ?? false, fromRemoteOnly ?? false))
            .RequireAuthorization();

        group.MapGet("/backup/{id}/export-argsonly", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase)
            => ExecuteGetExportArgsOnly(GetBackup(connection, id), exportPasswords ?? false))
            .RequireAuthorization();

        group.MapGet("/backup/{id}/export-cmdline", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase)
            => ExecuteGetExportCmdline(GetBackup(connection, id), exportPasswords ?? false))
            .RequireAuthorization();

        group.MapGet("/backup/{id}/export", ([FromServices] Connection connection, [FromServices] IHttpContextAccessor httpContextAccessor, [FromServices] IJWTTokenProvider jWTTokenProvider, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase, [FromQuery] string token, CancellationToken ct) =>
        {
            // Custom authorization check
            var singleOperationToken = jWTTokenProvider.ReadSingleOperationToken(token);
            if (singleOperationToken.Operation != "export")
                throw new UnauthorizedException("Invalid operation");

            var (data, filename) = ExecuteGetExport(connection, GetBackup(connection, id), exportPasswords ?? false, passphrase);
            var resp = httpContextAccessor.HttpContext!.Response;

            resp.ContentLength = data.Length;
            resp.ContentType = "application/octet-stream";
            resp.Headers.Append("Content-Disposition", $"attachment; filename={filename}");
            resp.Body.WriteAsync(data, ct);
        });

        group.MapGet("/backup/{id}/isdbusedelsewhere", ([FromServices] Connection connection, [FromRoute] string id)
            => ExecuteGetIsdbUsedElsewhere(GetBackup(connection, id)))
            .RequireAuthorization();

        group.MapGet("/backup/{id}/isactive", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id)
            => ExecuteGetIsActive(workerThreadsManager, GetBackup(connection, id)))
            .RequireAuthorization();
    }

    private static IBackup GetBackup(Connection connection, string id)
        => connection.GetBackup(id) ?? throw new NotFoundException("Backup not found");

    private static GetBackupResultDto ExecuteGet(Connection connection, IBackup bk)
    {
        var scheduleId = connection.GetScheduleIDsFromTags(new string[] { "ID=" + bk.ID });
        var schedule = scheduleId.Any() ? connection.GetSchedule(scheduleId.First()) : null;
        var sourcenames = SpecialFolders.GetSourceNames(bk);

        //TODO: Filter out the password in both settings and the target url

        return new GetBackupResultDto(
            schedule == null ? null : new Dto.ScheduleDto()
            {
                ID = schedule.ID,
                Tags = schedule.Tags,
                Time = schedule.Time,
                Repeat = schedule.Repeat,
                LastRun = schedule.LastRun,
                Rule = schedule.Rule,
                AllowedDays = schedule.AllowedDays
            },
            new Dto.BackupDto()
            {
                ID = bk.ID,
                Name = bk.Name,
                Sources = bk.Sources,
                Settings = bk.Settings?.Select(x => new Dto.SettingDto()
                {
                    Name = x.Name,
                    Value = x.Value,
                    Filter = x.Filter,
                    Argument = x.Argument
                }).ToArray(),
                Filters = bk.Filters?.Select(x => new Dto.FilterDto()
                {
                    Order = x.Order,
                    Include = x.Include,
                    Expression = x.Expression
                }).ToArray(),
                Metadata = bk.Metadata,
                Description = bk.Description,
                Tags = bk.Tags,
                TargetURL = bk.TargetURL,
                DBPath = bk.DBPath,
                IsTemporary = bk.IsTemporary,
                IsUnencryptedOrPassphraseStored = false,
            },
            sourcenames
        );
    }

    private static Dictionary<string, object> SearchFiles(IBackup backup, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary<string, string> extraValues)
    {
        if (string.IsNullOrWhiteSpace(timestring) && !allVersions)
            throw new BadRequestException("Invalid or missing time");

        var time = new DateTime();
        if (!allVersions)
            time = Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now);

        var r = Runner.Run(Runner.CreateListTask(backup, [filter], prefixOnly, allVersions, folderContents, time), false) as Duplicati.Library.Interface.IListResults;
        if (r == null)
            throw new ServerErrorException("No result from list operation");

        var result = new Dictionary<string, object>();

        foreach (var k in extraValues)
            result[k.Key] = k.Value;

        result["Filesets"] = r.Filesets;
        result["Files"] = r.Files
            // Group directories first - support either directory separator here as we may be restoring data from an alternate platform
            .OrderByDescending(f => (f.Path.StartsWith('/') && f.Path.EndsWith('/')) || (!f.Path.StartsWith('/') && f.Path.EndsWith('\\')))
            // Sort both groups (directories and files) alphabetically
            .ThenBy(f => f.Path);

        return result;
    }

    private static Dictionary<string, object> ExecuteGetFiles(IBackup bk, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary<string, string> extraValues)
        => SearchFiles(bk, filter, timestring, allVersions, prefixOnly, folderContents, extraValues);

    private static List<Dictionary<string, object>> ExecuteGetLog(Connection connection, IBackup bk, long? offset, long pagesize)
    {
        if (!File.Exists(bk.DBPath))
            return new List<Dictionary<string, object>>();

        using (var con = Library.SQLiteHelper.SQLiteLoader.LoadConnection(bk.DBPath))
        using (var cmd = con.CreateCommand())
            return LogData.DumpTable(cmd, "LogData", "ID", offset, pagesize);
    }

    private static List<Dictionary<string, object>> ExecuteGetRemotelog(Connection connection, IBackup bk, long? offset, long pagesize)
    {
        if (!File.Exists(bk.DBPath))
            return new List<Dictionary<string, object>>();

        using (var con = Library.SQLiteHelper.SQLiteLoader.LoadConnection(bk.DBPath))
        using (var cmd = con.CreateCommand())
        {
            var dt = LogData.DumpTable(cmd, "RemoteOperation", "ID", offset, pagesize);

            // Unwrap raw data to a string
            foreach (var n in dt)
                try { n["Data"] = System.Text.Encoding.UTF8.GetString((byte[])n["Data"]); }
                catch { }

            return dt;
        }
    }

    private static IEnumerable<IListResultFileset> ExecuteGetFilesets(IBackup bk, bool includeMetadata, bool fromRemoteOnly)
    {
        var extra = new Dictionary<string, string>
        {
            ["list-sets-only"] = "true"
        };
        if (includeMetadata)
            extra["list-sets-only"] = "false";
        if (fromRemoteOnly)
            extra["no-local-db"] = "true";

        var r = Runner.Run(Runner.CreateTask(DuplicatiOperation.List, bk, extra), false) as IListResults;
        if (r == null)
            throw new ServerErrorException("No result from list operation");

        if (r.EncryptedFiles && bk.Settings.Any(x => string.Equals("--no-encryption", x.Name, StringComparison.OrdinalIgnoreCase)))
            throw new ServerErrorException("encrypted-storage");

        return r.Filesets;
    }

    public static void RemovePasswords(IBackup backup)
    {
        backup.SanitizeSettings();
        backup.SanitizeTargetUrl();
    }

    private static Dto.ExportCommandlineDto ExecuteGetExportCmdline(IBackup backup, bool exportPasswords)
    {
        if (!exportPasswords)
            RemovePasswords(backup);

        return new Dto.ExportCommandlineDto(Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)));
    }

    private static Dto.ExportArgsOnlyDto ExecuteGetExportArgsOnly(IBackup backup, bool exportPasswords)
    {
        if (!exportPasswords)
            RemovePasswords(backup);

        var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup));
        return new Dto.ExportArgsOnlyDto(
            parts.First(),
            parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)),
            parts.Skip(1).Where(x => x.StartsWith("--", StringComparison.Ordinal))
        );
    }

    private static (byte[] Data, string Filename) ExecuteGetExport(Connection connection, IBackup backup, bool exportPasswords, string? passphrase)
    {
        if (!exportPasswords)
            RemovePasswords(backup);

        byte[] data = EncodeDataForExport(connection, backup, passphrase);

        string filename = Library.Utility.Uri.UrlEncode(backup.Name + "-duplicati-config.json");
        if (!string.IsNullOrWhiteSpace(passphrase))
            filename += ".aes";

        return (data, filename);
    }

    private static byte[] EncodeDataForExport(Connection connection, IBackup bk, string? passphrase)
    {
        var ipx = connection.PrepareBackupForExport(bk);

        using var ms = new MemoryStream();
        JsonSerializer.Serialize(ms, ipx, new JsonSerializerOptions()
        {
            WriteIndented = true,
            Converters = {
                new JsonStringEnumConverter(),
                new DayOfWeekStringEnumConverter()
            }
        });

        if (string.IsNullOrWhiteSpace(passphrase))
            return ms.ToArray();

        ms.Position = 0;

        using var ms2 = new MemoryStream();
        using (var m = new Library.Encryption.AESEncryption(passphrase, new Dictionary<string, string>()))
            m.Encrypt(ms, ms2);

        return ms2.ToArray();
    }

    private static Dto.IsDbUsedElsewhereDto ExecuteGetIsdbUsedElsewhere(IBackup bk)
        => new Dto.IsDbUsedElsewhereDto(Library.Main.DatabaseLocator.IsDatabasePathInUse(bk.DBPath));

    private static Dto.IsBackupActiveDto ExecuteGetIsActive(IWorkerThreadsManager workerThreadsManager, IBackup bk)
    {
        if (workerThreadsManager.WorkerThread == null)
            throw new InvalidOperationException("Worker thread not available");

        var t = workerThreadsManager.WorkerThread.CurrentTask;
        var bt = t?.Backup;
        if (bt != null && bk.ID == bt.ID)
            return new Dto.IsBackupActiveDto("OK", true);

        if (workerThreadsManager.WorkerThread.CurrentTasks.Any(x => x?.Backup == null || x.Backup.ID == bk.ID))
            return new Dto.IsBackupActiveDto("OK", true);

        return new Dto.IsBackupActiveDto("OK", false);
    }
}
