using InABox.Configuration;
using InABox.Core;
using Microsoft.CodeAnalysis.Scripting;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace InABox.Database;

public class DatabaseVersion : BaseObject, IGlobalConfigurationSettings
{
    public string Version { get; set; }

    public DatabaseVersion()
    {
        Version = "0.00";
    }
}

public class VersionNumber
{
    public int MajorVersion { get; set; }
    public int MinorVersion { get; set; }

    public string Release { get; set; }

    public bool IsDevelopmentVersion { get; set; }

    public VersionNumber(int majorVersion, int minorVersion, string release = "", bool isDevelopmentVersion = false)
    {
        MajorVersion = majorVersion;
        MinorVersion = minorVersion;
        Release = release;
        IsDevelopmentVersion = isDevelopmentVersion;
    }

    private static Regex _format = new(@"^(\d+)\.(\d+)([a-zA-Z]*)$");
    public static VersionNumber Parse(string versionStr)
    {
        if(versionStr == "???")
        {
            return new(0, 0, "", true);
        }
        var match = _format.Match(versionStr);
        if (!match.Success)
            throw new FormatException($"'{versionStr}' is not a valid version!");
        return new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false);
    }
    public static bool TryParse(string versionStr, [NotNullWhen(true)] out VersionNumber? version)
    {
        if (versionStr == "???")
        {
            version = new(0, 0, "", true);
            return true;
        }
        var match = _format.Match(versionStr);
        if (!match.Success)
        {
            version = null;
            return false;
        }
        version = new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false);
        return true;
    }

    public static bool operator <(VersionNumber a, VersionNumber b)
    {
        if (a.IsDevelopmentVersion)
        {
            return false;
        }
        else if (b.IsDevelopmentVersion)
        {
            return true;
        }
        return a.MajorVersion < b.MajorVersion || 
            (a.MajorVersion == b.MajorVersion &&
                (a.MinorVersion < b.MinorVersion || 
                    (a.MinorVersion == b.MinorVersion && string.Compare(a.Release, b.Release, StringComparison.Ordinal) < 0)));
    }
    public static bool operator >(VersionNumber a, VersionNumber b)
    {
        return b < a;
    }

    public static bool operator <=(VersionNumber a, VersionNumber b)
    {
        return !(b < a);
    }

    public static bool operator >=(VersionNumber a, VersionNumber b)
    {
        return !(a < b);
    }

    public override bool Equals(object? obj)
    {
        if(obj is VersionNumber v)
        {
            return this == v;
        }
        return false;
    }

    public override int GetHashCode()
    {
        if (IsDevelopmentVersion)
            return 0;
        return MajorVersion ^ MinorVersion ^ Release.GetHashCode();
    }

    public static bool operator ==(VersionNumber a, VersionNumber b)
    {
        if (a.IsDevelopmentVersion)
            return b.IsDevelopmentVersion;
        if (b.IsDevelopmentVersion)
            return false;
        return a.MajorVersion == b.MajorVersion && a.MinorVersion == b.MinorVersion && a.Release == b.Release;
    }
    public static bool operator !=(VersionNumber a, VersionNumber b)
    {
        if (a.IsDevelopmentVersion)
            return !b.IsDevelopmentVersion;
        if (b.IsDevelopmentVersion)
            return true;
        return a.MajorVersion != b.MajorVersion || a.MinorVersion != b.MinorVersion || a.Release != b.Release;
    }

    public override string ToString()
    {
        return IsDevelopmentVersion ? "???" : $"{MajorVersion}.{MinorVersion:D2}{Release}";
    }
}

public static class DataUpdater
{
    private static Dictionary<VersionNumber, List<DatabaseUpdateScript>> updateScripts = new();

    /// <summary>
    /// Register a migration script to run when updating to this version.
    /// 
    /// <para>The <paramref name="action"/> should probably be repeatable; 
    /// that is, if you run it a second time, it only updates data that needed updating. This way if it accidentally somehow gets run twice, there is no issue.
    /// </para>
    /// </summary>
    /// <param name="version">The version to update to.</param>
    /// <param name="action">The action to be run.</param>
    public static void RegisterUpdateScript<TUpdater>()
        where TUpdater : DatabaseUpdateScript, new()
    {
        var updater = new TUpdater();
        
        if(!updateScripts.TryGetValue(updater.Version, out var list))
        {
            list = new();
            updateScripts[updater.Version] = list;
        }
        list.Add(updater);
    }

    private static bool MigrateDatabase(VersionNumber fromVersion, VersionNumber toVersion, out VersionNumber newVersion)
    {
        var versionNumbers = updateScripts.Keys.ToList();
        versionNumbers.Sort((x, y) => x == y ? 0 : x < y ? -1 : 1);

        newVersion = fromVersion;

        int? index = null;
        foreach (var (i, number) in versionNumbers.Select((x, i) => new Tuple<int, VersionNumber>(i, x)))
        {
            if (number > fromVersion)
            {
                index = i;
                break;
            }
        }
        if(index != null && fromVersion < toVersion)
        {
            Logger.Send(LogType.Information, "", $"Updating database from {fromVersion} to {toVersion}");
            for (int i = (int)index; i < versionNumbers.Count; i++)
            {
                var version = versionNumbers[i];
                if (toVersion < version)
                {
                    break;
                }
                Logger.Send(LogType.Information, "", $"Executing update to {version}");
                foreach(var updater in updateScripts[version])
                {
                    if (!updater.Update())
                    {
                        Logger.Send(LogType.Error, "", $"Script failed, cancelling migration");
                        return false;
                    }
                }
                newVersion = version;
            }
            Logger.Send(LogType.Information, "", $"Data migration complete!");
        }
        newVersion = toVersion;
        return true;
    }
    
    private static void UpdateVersionNumber(VersionNumber version)
    {
        if (version.IsDevelopmentVersion)
        {
            return;
        }
        var dbVersion = DbFactory.GetVersionSettings();
        dbVersion.Version = version.ToString();

        var result = DbFactory.NewProvider(Logger.Main).Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))
            .Rows.FirstOrDefault()?.ToObject<GlobalSettings>() ?? new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };
        result.OriginalValueList["Contents"] = result.Contents;
        result.Contents = Serialization.Serialize(dbVersion);
        DbFactory.NewProvider(Logger.Main).Save(result);
    }

    /// <summary>
    /// Migrates the database to the current version.
    /// </summary>
    /// <returns><c>false</c> if the migration fails.</returns>
    public static bool MigrateDatabase()
    {
        try
        {
            var from = DbFactory.GetDatabaseVersion();
            var to = VersionNumber.Parse(CoreUtils.GetVersion());

            var success = MigrateDatabase(from, to, out var newVersion);
            if (newVersion != from)
            {
                UpdateVersionNumber(newVersion);
            }
            return success;
        }
        catch(Exception e)
        {
            Logger.Send(LogType.Error, "", $"Error while migrating database: {CoreUtils.FormatException(e)}");
            return false;
        }
    }
    
    public static bool DoSpecificMigration(VersionNumber from, VersionNumber to)
    {
        try
        {
            var _success = MigrateDatabase(from, to, out var _);
            return _success;
        }
        catch(Exception e)
        {
            Logger.Send(LogType.Error, "", $"Error while migrating database: {CoreUtils.FormatException(e)}");
            return false;
        }
    }
}
