#nullable enable

namespace DatabaseUpdater;

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using Newtonsoft.Json;
#if NET8_0_OR_GREATER
using Microsoft.Data.SqlClient;
#else
using System.Data.SqlClient;
#endif

internal class UpdateClassicData
{
    public static void MigrateAwayFromBinaryFormatter(
        string connectionString,
        bool runAlways = false
    )
    {
        using var sqlConnection = new SqlConnection(connectionString);
        sqlConnection.Open();
        using var sqlCommand = sqlConnection.CreateCommand();

        const string scriptName = "DatabaseUpdater_MigrateAwayFromBinaryFormatter";
        sqlCommand.CommandText = "SELECT 1 FROM DatabaseScript WHERE ScriptName = @scriptName";
        sqlCommand.Parameters.AddWithValue("scriptName", scriptName);

        var result = sqlCommand.ExecuteScalar();

        if (result is not null && !runAlways)
        {
            Console.WriteLine("Already ran DatabaseUpdater_MigrateAwayFromBinaryFormatter");
            return;
        }

        sqlCommand.CommandText =
            "SELECT Id, ObjectValue FROM ContentItemField WHERE ObjectValue IS NOT NULL";
        sqlCommand.Parameters.Clear();

        var sqlReader = sqlCommand.ExecuteReader();
        var itemsToCheck = new Dictionary<Guid, byte[]>();
        while (sqlReader.Read())
        {
            itemsToCheck.Add((Guid)sqlReader[0], (byte[])sqlReader[1]);
        }
        sqlReader.Close();

        Console.WriteLine($"Found {itemsToCheck.Count} rows to check");

        sqlCommand.CommandText = "UPDATE ContentItemField SET ObjectValue = @value WHERE Id = @id";

        foreach (var item in itemsToCheck)
        {
            var id = item.Key;
            var objectValue = item.Value;

            var newObject = ToObjectWithBinaryFormatter(objectValue);
            if (newObject is null)
            {
                continue;
            }

            Console.WriteLine("Updating binary data in ContentItemField for " + id);
            sqlCommand.Parameters.Clear();
            sqlCommand.Parameters.AddWithValue("id", id);
            sqlCommand.Parameters.AddWithValue("value", newObject.ToByteArrayUsingJson());
            sqlCommand.ExecuteNonQuery();
        }

        sqlCommand.CommandText = "INSERT INTO DatabaseScript (ScriptName) VALUES (@scriptName)";
        sqlCommand.Parameters.Clear();
        sqlCommand.Parameters.AddWithValue("scriptName", scriptName);
        sqlCommand.ExecuteNonQuery();
    }

    public static object? ToObjectWithBinaryFormatter(byte[] byteArray)
    {
        try
        {
            using var memoryStream = new MemoryStream();
#pragma warning disable SYSLIB0011
            var binaryFormatter = new BinaryFormatter();
#pragma warning restore SYSLIB0011

            memoryStream.Write(byteArray, 0, byteArray.Length);
            memoryStream.Seek(0, SeekOrigin.Begin);

            return binaryFormatter.Deserialize(memoryStream);
        }
        catch (SerializationException)
        {
            // at this point the byteArray may already be json
            // or the binary deserialization failed for something like a missing dll
            try
            {
                var value = Encoding.UTF8.GetString(byteArray);
                ToObjectUsingTypedJson(value);
                return null;
            }
            catch (Exception)
            {
                // we couldn't load it as json so we know it was a problem deserializing the binary format.
                // Throw the original caught serialization exception below
            }

            throw;
        }
    }

    public static void MigrateAwayFromTypedJson(string connectionString)
    {
        using var sqlConnection = new SqlConnection(connectionString);
        sqlConnection.Open();
        using var sqlCommand = sqlConnection.CreateCommand();

        const string scriptName = "DatabaseUpdater_MigrateAwayFromTypedJson";
        sqlCommand.CommandText = "SELECT 1 FROM DatabaseScript WHERE ScriptName = @scriptName";
        sqlCommand.Parameters.AddWithValue("scriptName", scriptName);

        var result = sqlCommand.ExecuteScalar();

        if (result is not null)
        {
            Console.WriteLine("Already ran DatabaseUpdater_MigrateAwayFromTypedJson");
            return;
        }

        sqlCommand.CommandText =
            "SELECT Id, ObjectValue FROM ContentItemField WHERE ObjectValue IS NOT NULL";
        sqlCommand.Parameters.Clear();

        var sqlReader = sqlCommand.ExecuteReader();
        var itemsToCheck = new Dictionary<Guid, byte[]>();
        while (sqlReader.Read())
        {
            itemsToCheck.Add((Guid)sqlReader[0], (byte[])sqlReader[1]);
        }
        sqlReader.Close();

        Console.WriteLine($"Found {itemsToCheck.Count} rows to check");

        sqlCommand.CommandText = "UPDATE ContentItemField SET ObjectValue = @value WHERE Id = @id";

        foreach (var item in itemsToCheck)
        {
            var id = item.Key;
            var objectValue = item.Value;

            var value = Encoding.UTF8.GetString(objectValue);
            try
            {
                var newObject = ToObjectUsingTypedJson(value);
                if (newObject is null)
                {
                    continue;
                }

                Console.WriteLine("Removing type from json in ContentItemField for " + id);
                sqlCommand.Parameters.Clear();
                sqlCommand.Parameters.AddWithValue("id", id);
                sqlCommand.Parameters.AddWithValue("value", newObject.ToByteArrayUsingJson());
                sqlCommand.ExecuteNonQuery();
            }
            catch (Exception ex)
            {
                throw new Exception(
                    $"""
                    Failed to parse json on row with id {item.Key}
                    {value} 
                    """,
                    ex
                );
            }
        }

        sqlCommand.CommandText = "INSERT INTO DatabaseScript (ScriptName) VALUES (@scriptName)";
        sqlCommand.Parameters.Clear();
        sqlCommand.Parameters.AddWithValue("scriptName", scriptName);
        sqlCommand.ExecuteNonQuery();
    }

    public static object? ToObjectUsingTypedJson(string objectValue)
    {
#pragma warning disable ISC1007
        return JsonConvert.DeserializeObject(
            objectValue,
            new JsonSerializerSettings
            {
                TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Full,
                TypeNameHandling = TypeNameHandling.All,
            }
        );
#pragma warning restore ISC1007
    }
}
