using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace Elwig.Helpers {
    public static class AppDbUpdater {

        // Don't forget to update value in Tests/fetch-resources.bat!
        public static readonly int RequiredSchemaVersion = 18;

        private static int VersionOffset = 0;

        public static async Task<string> CheckDb() {
            using var cnx = AppDbContext.Connect();

            var applId = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA application_id") ?? 0;
            if (applId != 0x454C5747) throw new Exception("Invalid application_id of database");

            var schemaVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA schema_version") ?? 0;
            VersionOffset = (int)(schemaVers % 100);
            if (VersionOffset != 0) {
                // schema was modified manually/externally
                // TODO issue warning
            }
            await UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion);

            var userVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA user_version") ?? 0;
            var major = userVers >> 24;
            var minor = (userVers >> 16) & 0xFF;
            var patch = userVers & 0xFFFF;

            if (App.VersionMajor > major ||
                (App.VersionMajor == major && App.VersionMinor > minor) ||
                (App.VersionMajor == major && App.VersionMinor == minor && App.VersionPatch > patch)) {
                long vers = (App.VersionMajor << 24) | (App.VersionMinor << 16) | App.VersionPatch;
                await AppDbContext.ExecuteBatch(cnx, $"PRAGMA user_version = {vers}");
            }

            return $"{major}.{minor}.{patch}";
        }

        private static async Task UpdateDbSchema(SqliteConnection cnx, int fromVersion, int toVersion) {
            if (fromVersion == toVersion) {
                return;
            } else if (fromVersion > toVersion) {
                throw new Exception("schema_version of database is too new");
            } else if (fromVersion <= 0) {
                throw new Exception("schema_version of database is invalid");
            }

            var asm = Assembly.GetExecutingAssembly();
            (int From, int To, string Name)[] scripts = asm.GetManifestResourceNames()
                .Where(n => n.StartsWith("Elwig.Resources.Sql."))
                .Select(n => {
                    var p = n.Split(".")[^2].Split("-");
                    return (int.Parse(p[0]), int.Parse(p[1]), n);
                })
                .OrderBy(s => s.Item1).ThenBy(s => s.Item2)
                .ToArray();

            List<string> toExecute = [];
            var vers = fromVersion;
            while (vers < toVersion) {
                var (_, to, name) = scripts.Where(s => s.From == vers).Last();
                toExecute.Add(name);
                vers = to;
            }
            if (toExecute.Count == 0)
                return;

            await AppDbContext.ExecuteBatch(cnx, """
                PRAGMA locking_mode = EXCLUSIVE;
                BEGIN EXCLUSIVE;
                """);
            foreach (var script in toExecute) {
                await AppDbContext.ExecuteEmbeddedScript(cnx, asm, script);
            }
            var violations = await AppDbContext.ForeignKeyCheck(cnx);
            if (violations.Length > 0) {
                throw new Exception($"Foreign key violations ({violations.Length}):\n" + string.Join("\n", violations
                    .Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}")));
            }

            await AppDbContext.ExecuteBatch(cnx, $"""
                COMMIT;
                VACUUM;
                PRAGMA schema_version = {toVersion * 100 + VersionOffset};
                """);
        }
    }
}