using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text.Json.Nodes;
using System.Threading.Tasks;

namespace Elwig.Helpers.Export {
    public static class Database {

        private static async Task<(long? ApplicationId, string? UserVersion, long? SchemaVersion, long FileSize)> GetMeta() {
            long size = new FileInfo(App.Config.DatabaseFile).Length;
            using var cnx = await AppDbContext.ConnectAsync();
            var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id");
            var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version");
            var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version");
            return (applId, userVers != null ? $"{userVers >> 24}.{(userVers >> 16) & 0xFF}.{(userVers >> 8) & 0xFF}.{userVers & 0xFF}" : null, schemaVers, size);
        }

        public static async Task ExportSqlite(string filename, bool zipFile) {
            if (zipFile) {
                File.Delete(filename);
                using var zip = ZipFile.Open(filename, ZipArchiveMode.Create);

                var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
                using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
                    await writer.WriteAsync("elwig:1");
                }

                var (applId, userVers, schemaVers, size) = await GetMeta();
                var meta = zip.CreateEntry("meta.json", CompressionLevel.NoCompression);
                using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) {
                    var obj = new JsonObject {
                        ["timestamp"] = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}",
                        ["zwstid"] = App.ZwstId,
                        ["device"] = Environment.MachineName,
                        ["database"] = new JsonObject {
                            ["application_id"] = applId,
                            ["user_version"] = userVers,
                            ["schema_version"] = schemaVers,
                            ["file_size"] = size,
                        },
                    };
                    await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
                }

                var db = zip.CreateEntryFromFile(App.Config.DatabaseFile, "database.sqlite3", CompressionLevel.SmallestSize);
            } else {
                File.Copy(App.Config.DatabaseFile, filename, true);
            }
        }

        public static async Task ExportSql(string filename, bool zipFile) {
            if (zipFile) {
                File.Delete(filename);
                using var zip = ZipFile.Open(filename, ZipArchiveMode.Create);

                var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
                using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
                    await writer.WriteAsync("elwig:1");
                }

                var (applId, userVers, schemaVers, size) = await GetMeta();
                var meta = zip.CreateEntry("meta.json", CompressionLevel.NoCompression);
                using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) {
                    var obj = new JsonObject {
                        ["timestamp"] = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}",
                        ["zwstid"] = App.ZwstId,
                        ["device"] = Environment.MachineName,
                        ["database"] = new JsonObject {
                            ["application_id"] = applId,
                            ["user_version"] = userVers,
                            ["schema_version"] = schemaVers,
                            ["file_size"] = size,
                        },
                    };
                    await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
                }

                var sql = zip.CreateEntry("database.sql", CompressionLevel.SmallestSize);
                using (var writer = new StreamWriter(sql.Open(), Utils.UTF8)) {
                    await ExportSql(writer);
                }
            } else {
                using var stream = File.OpenWrite(filename);
                using var writer = new StreamWriter(stream, Utils.UTF8);
                await ExportSql(writer);
            }
        }

        public static async Task ExportSql(StreamWriter writer) {
            using var cnx = await AppDbContext.ConnectAsync();

            var tables = new List<(string Name, string Sql)>();
            using (var cmd = cnx.CreateCommand()) {
                cmd.CommandText = "SELECT name, sql FROM sqlite_schema WHERE type = 'table'";
                using var reader = await cmd.ExecuteReaderAsync();
                while (await reader.ReadAsync()) {
                    tables.Add((reader.GetString(0), reader.GetString(1)));
                }
            }

            var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
            var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0;
            var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;

            await writer.WriteLineAsync($"-- Elwig database dump, {DateTime.Now:yyyy-MM-dd, HH:mm:ss}");
            await writer.WriteLineAsync($"-- {Environment.MachineName}, Zwst. {App.BranchName}, {App.Client.Name}");
            await writer.WriteLineAsync("BEGIN TRANSACTION;");
            await writer.WriteLineAsync("PRAGMA foreign_keys=OFF;");
            await writer.WriteLineAsync($"PRAGMA application_id=0x{applId:X8};");
            await writer.WriteLineAsync($"PRAGMA user_version=0x{userVers:X8};");

            foreach (var t in tables) {
                await writer.WriteAsync(t.Sql);
                await writer.WriteLineAsync(";");

                var columnNames = new List<string>();
                using (var cmd = cnx.CreateCommand()) {
                    cmd.CommandText = $"PRAGMA table_info({t.Name})";
                    using var reader = await cmd.ExecuteReaderAsync();
                    while (await reader.ReadAsync()) {
                        columnNames.Add(reader.GetString(1));
                    }
                }

                using (var cmd = cnx.CreateCommand()) {
                    cmd.CommandText = $"SELECT {string.Join(',', columnNames)} FROM {t.Name}";
                    using var reader = await cmd.ExecuteReaderAsync();
                    var columns = await reader.GetColumnSchemaAsync();
                    var values = new object[reader.FieldCount];
                    while (await reader.ReadAsync()) {
                        await writer.WriteAsync($"INSERT INTO {t.Name} VALUES (");

                        reader.GetValues(values);
                        for (int i = 0; i < columns.Count; i++) {
                            var c = columns[i];
                            var v = values[i];
                            if (i > 0) await writer.WriteAsync(",");
                            if (v == null || v is DBNull) {
                                await writer.WriteAsync("NULL");
                            } else if (c.DataTypeName == "TEXT") {
                                await writer.WriteAsync($"'{v.ToString()?.Replace("'", "''")}'");
                            } else {
                                await writer.WriteAsync(v.ToString()?.Replace(',', '.'));
                            }
                        }

                        await writer.WriteLineAsync(");");
                    }
                }
            }

            using (var cmd = cnx.CreateCommand()) {
                cmd.CommandText = "SELECT sql FROM sqlite_schema WHERE type != 'table' AND sql IS NOT NULL";
                using var reader = await cmd.ExecuteReaderAsync();
                while (await reader.ReadAsync()) {
                    await writer.WriteAsync(reader.GetString(0));
                    await writer.WriteLineAsync(";");
                }
            }

            await writer.WriteLineAsync($"PRAGMA schema_version={schemaVers};");
            await writer.WriteLineAsync("PRAGMA foreign_keys=ON;");
            await writer.WriteLineAsync("COMMIT;");
            await writer.WriteLineAsync("VACUUM;");
        }

        public static async Task Import(string filename) {
            if (filename.EndsWith(".sql")) {
                await ImportSql(filename, false);
            } else if (filename.EndsWith(".sql.zip")) {
                await ImportSql(filename, true);
            } else if (filename.EndsWith(".sqlite3")) {
                await ImportSqlite(filename, false);
            } else if (filename.EndsWith(".sqlite3.zip")) {
                await ImportSqlite(filename, true);
            } else {
                throw new ArgumentException($"Unknown file extension for importing: '{filename}'");
            }
        }

        public static async Task ImportSql(string filename, bool zipFile = false) {
            if (zipFile) {
                using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
                await zip.CheckIntegrity();
                foreach (var entry in zip.Entries) {
                    if (entry.Name.EndsWith(".sql")) {
                        using var stream = entry.Open();
                        using var reader = new StreamReader(stream, Utils.UTF8);
                        await ImportSql(reader);
                        return;
                    }
                }
                throw new FileFormatException("ZIP archive has to contain at least one .sql file");
            } else {
                using var stream = File.Open(filename, FileMode.Open);
                using var reader = new StreamReader(stream, Utils.UTF8);
                await ImportSql(reader);
            }
        }

        public static async Task ImportSqlite(string filename, bool zipFile = false) {
            if (zipFile) {
                var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3");
                try {
                    using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
                    await zip.CheckIntegrity();
                    foreach (var entry in zip.Entries) {
                        if (entry.Name.EndsWith(".sqlite3")) {
                            entry.ExtractToFile(newName);
                            await ImportSqlite(newName);
                            return;
                        }
                    }
                    throw new FileFormatException("ZIP archive has to contain at least one .sqlite3 file");
                } finally {
                    if (File.Exists(newName)) File.Delete(newName);
                }
            }

            var oldName = Path.ChangeExtension(App.Config.DatabaseFile, ".old.sqlite3");
            File.Move(App.Config.DatabaseFile, oldName, true);
            File.Move(filename, App.Config.DatabaseFile, false);

            using var cnx = await AppDbContext.ConnectAsync();
            await cnx.ExecuteBatch("VACUUM");
        }

        public static async Task ImportSql(StreamReader reader) {
            var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3");
            File.Delete(newName);
            try {
                using (var cnx = await AppDbContext.ConnectAsync($"Data Source=\"{newName}\"; Mode=ReadWriteCreate; Foreign Keys=False; Cache=Default; Pooling=False")) {
                    await cnx.ExecuteBatch(await reader.ReadToEndAsync());
                }
                await ImportSqlite(newName);
            } finally {
                if (File.Exists(newName)) File.Delete(newName);
            }
        }
    }
}
