using System.Data;
using System.Text.Json;
using Duplicati.Library.AutoUpdater;
using Duplicati.Library.Main;
using Duplicati.Library.Main.Database;
using Duplicati.Library.SQLiteHelper;

namespace Duplicati.CommandLine.DatabaseTool;

public static class Helper
{
    /// <summary>
    /// Creates a backup of the file
    /// </summary>
    /// <param name="filename">The filename to backup</param>
    public static void CreateFileBackup(string path)
    {
        path = Path.GetFullPath(path);
        var dir = Path.GetDirectoryName(path) ?? "";
        var filename = Path.GetFileNameWithoutExtension(path);

        var newname = $"{filename}-{DateTime.Now:yyyMMddhhmmss}.bak";
        var backup = Path.Combine(dir, newname);
        var retry = 0;
        while (File.Exists(backup))
        {
            if (retry > 100)
                throw new IOException($"Cannot create backup file {backup} - too many retries");

            retry++;
            newname = $"{filename}-{DateTime.Now:yyyMMddhhmmss}-{retry}.bak";
            backup = Path.Combine(dir, newname);
        }

        File.Copy(path, backup);
    }

    /// <summary>
    /// Find all databases in the data folder
    /// </summary>
    /// <param name="databases">The input databases</param>
    /// <param name="datafolder">The folder to scan</param>
    /// <param name="scanExtra">Whether to scan for extra databases</param>
    /// <returns>A task that when awaited contains the list of databases.</returns>
    public static async Task<string[]> FindAllDatabases(string[]? databases, string datafolder, bool scanExtra)
    {
        databases ??= [];
        if (databases.Length != 0)
            return databases;

        // No explicit paths given, so we find all databases in the folder
        var serverdb = Path.Combine(datafolder, DataFolderManager.SERVER_DATABASE_FILENAME);
        var dbpaths =
            CLIDatabaseLocator.GetAllDatabasePaths()
            .Prepend(serverdb)
            .ToList();

        // Append any database paths from the server database
        if (File.Exists(serverdb))
        {
            try
            {
                await using var con = await SQLiteLoader.LoadConnectionAsync(serverdb)
                    .ConfigureAwait(false);
                await using var cmd = con.CreateCommand(@"
                    SELECT ""DBPath""
                    FROM ""Backup""
                ");
                await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(CancellationToken.None).ConfigureAwait(false))
                    dbpaths.Add(rd.ConvertValueToString(0) ?? "");
            }
            catch
            {
            }
        }

        if (scanExtra)
            dbpaths.AddRange(
                Directory.EnumerateFiles(datafolder, "*.sqlite", SearchOption.AllDirectories)
                .Where(x => CLIDatabaseLocator.IsRandomlyGeneratedName(x))
                .Select(x => Path.GetFullPath(x)));

        return dbpaths
            .Where(x => File.Exists(x))
            .Distinct()
            .ToArray();
    }

    /// <summary>
    /// Examines the database and returns the version and whether it is a server database
    /// </summary>
    /// <param name="db">The path to the database</param>
    /// <returns>A task that when awaited contains a tuple with the version and whether it is a server database</returns>
    public static async Task<(int Version, bool isserver)> ExamineDatabase(string db)
    {
        await using var con = await SQLiteLoader.LoadConnectionAsync(db)
            .ConfigureAwait(false);
        await using var cmd = con.CreateCommand();

        var isserverdb = await cmd.ExecuteScalarInt64Async(@"
                SELECT COUNT(*)
                FROM sqlite_master
                WHERE
                    type='table'
                    AND name='Backup'
                    OR name='Schedule'
            ", CancellationToken.None)
            .ConfigureAwait(false) == 2;

        var version = (int)await cmd.ExecuteScalarInt64Async(@"
                SELECT MAX(Version)
                FROM Version
            ", CancellationToken.None)
            .ConfigureAwait(false);

        return (version, isserverdb);
    }

    /// <summary>
    /// Prints the data reader to the console
    /// </summary>
    /// <param name="reader">The data reader to print</param>
    /// <param name="useJson">Whether to use JSON or not</param>
    public static void Print(this IDataReader reader, bool useJson)
    {
        if (useJson)
            PrintJson(reader);
        else
            PrintReader(reader);
    }

    /// <summary>
    /// Prints the data reader to the console in JSON format
    /// </summary>
    /// <param name="reader">The data reader to print</param>
    public static void PrintJson(this IDataReader reader)
    {
        using var writer = new Utf8JsonWriter(Console.OpenStandardOutput(), new JsonWriterOptions { Indented = true });

        writer.WriteStartArray();

        // Write the header row
        writer.WriteStartArray();
        for (int i = 0; i < reader.FieldCount; i++)
            writer.WriteStringValue(reader.GetName(i));
        writer.WriteEndArray();

        // Write each data row
        while (reader.Read())
        {
            writer.WriteStartArray();
            for (int i = 0; i < reader.FieldCount; i++)
            {
                var value = reader.GetValue(i);
                if (value == DBNull.Value)
                    writer.WriteNullValue();
                else if (value is string s)
                    writer.WriteStringValue(s);
                else if (value is int i32)
                    writer.WriteNumberValue(i32);
                else if (value is long i64)
                    writer.WriteNumberValue(i64);
                else if (value is double d)
                    writer.WriteNumberValue(d);
                else if (value is float f)
                    writer.WriteNumberValue(f);
                else if (value is bool b)
                    writer.WriteBooleanValue(b);
                else if (value is DateTime dt)
                    writer.WriteStringValue(dt); // ISO 8601 format
                else
                    writer.WriteStringValue(value.ToString()); // Fallback
            }
            writer.WriteEndArray();
        }

        writer.WriteEndArray();
        writer.Flush();
    }

    /// <summary>
    /// Prints the data reader to the console in a tabular format
    /// </summary>
    /// <param name="reader">The data reader to print</param>
    public static void PrintReader(this IDataReader reader)
    {
        // Print the header row
        for (int i = 0; i < reader.FieldCount; i++)
            Console.Write($"{reader.GetName(i)}\t");
        Console.WriteLine();

        // Print each data row
        while (reader.Read())
        {
            for (int i = 0; i < reader.FieldCount; i++)
            {
                var value = reader.GetValue(i);
                if (value == DBNull.Value)
                    Console.Write("<null>\t");
                else
                    Console.Write($"{value}\t");
            }
            Console.WriteLine();
        }
    }
}