#nullable enable

using System.Text;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MMoneyCoderSharp.Data;

public sealed class LegacyManualOperatorDataValidationException : Exception
{
    public LegacyManualOperatorDataValidationException()
        : base("The legacy manual operator data is invalid.")
    {
    }
}

public sealed record LegacyManualViItem(string Code, string Name);

public sealed record LegacyManualOperatorSourceSnapshot(
    IReadOnlyList<S5025TrustedManualRow> IndividualRows,
    IReadOnlyList<S5025TrustedManualRow> ForeignRows,
    IReadOnlyList<S5025TrustedManualRow> InstitutionRows,
    IReadOnlyList<LegacyManualViItem> ViItems,
    string SourceSha256);

/// <summary>
/// Closed source contract for the four original FSell/VI files. The caller can
/// request the one supported snapshot, but cannot provide a path or filename.
/// </summary>
public interface ILegacyManualOperatorDataSource
{
    Task<LegacyManualOperatorSourceSnapshot> ReadAsync(
        CancellationToken cancellationToken = default);
}

/// <summary>
/// Parses and emits only the original CP949 manual-data shapes. Keeping this
/// contract in Core makes the destination transaction independent from raw
/// legacy bytes and gives every imported row an explicit type.
/// </summary>
public static class LegacyManualOperatorDataCodec
{
    public const int NetSellRowCount = 5;
    public const int MaximumNetSellCellLength = 256;
    public const int MaximumViItemCount = 100;
    public const int MaximumViCodeLength = 64;
    public const int MaximumViNameLength = 200;
    public const int MaximumViSerializedBytes = 4_000;

    private static readonly Encoding Cp949 = CreateCp949Encoding();

    public static IReadOnlyList<S5025TrustedManualRow> ParseNetSell(
        ReadOnlySpan<byte> bytes)
    {
        var text = DecodeStrict(bytes, allowEmpty: false);
        var lines = new List<string>(NetSellRowCount + 1);
        using (var reader = new StringReader(text))
        {
            while (reader.ReadLine() is { } line)
            {
                lines.Add(line.Trim());
            }
        }

        // FSell wrote five rows followed by one empty terminal record. The
        // migrated reader deliberately preserves that exact record count.
        if (lines.Count != NetSellRowCount + 1 || lines[^1].Length != 0)
        {
            throw InvalidData();
        }

        var rows = new S5025TrustedManualRow[NetSellRowCount];
        for (var index = 0; index < NetSellRowCount; index++)
        {
            var columns = lines[index].Split('^');
            if (columns.Length != 4 || columns.Any(value =>
                    value.Length > MaximumNetSellCellLength ||
                    value.Any(char.IsControl)))
            {
                throw InvalidData();
            }

            rows[index] = new S5025TrustedManualRow(
                columns[0],
                columns[1],
                columns[2],
                columns[3]);
        }

        return Array.AsReadOnly(rows);
    }

    public static IReadOnlyList<LegacyManualViItem> ParseVi(ReadOnlySpan<byte> bytes)
    {
        if (bytes.Length > MaximumViSerializedBytes)
        {
            throw InvalidData();
        }

        var text = DecodeStrict(bytes, allowEmpty: true);
        var items = new List<LegacyManualViItem>();
        using var reader = new StringReader(text);
        while (reader.ReadLine() is { } rawLine)
        {
            var line = rawLine.Trim();
            if (line.Length == 0)
            {
                continue;
            }

            var columns = line.Split('^');
            if (columns.Length != 9 || columns.Skip(2).Any(value => value.Length != 0))
            {
                throw InvalidData();
            }

            var item = new LegacyManualViItem(columns[0], columns[1]);
            ValidateViItem(item);
            items.Add(item);
            if (items.Count > MaximumViItemCount)
            {
                throw InvalidData();
            }
        }

        // The canonical representation is the value consumed by the migrated
        // store, so the 4,000-byte legacy payload bound is checked again after
        // normalization. Duplicates and row order are intentionally preserved.
        _ = SerializeVi(items);
        return Array.AsReadOnly(items.ToArray());
    }

    public static byte[] SerializeNetSell(IReadOnlyList<S5025TrustedManualRow> rows)
    {
        if (rows is null || rows.Count != NetSellRowCount || rows.Any(row => row is null))
        {
            throw InvalidData();
        }

        foreach (var row in rows)
        {
            ValidateNetSellCell(row.LeftName);
            ValidateNetSellCell(row.LeftAmount);
            ValidateNetSellCell(row.RightName);
            ValidateNetSellCell(row.RightAmount);
        }

        var text = string.Join("\r\n", rows.Select(row => string.Join(
            '^',
            row.LeftName,
            row.LeftAmount,
            row.RightName,
            row.RightAmount))) + "\r\n\r\n";
        return EncodeStrict(text);
    }

    public static byte[] SerializeVi(IReadOnlyList<LegacyManualViItem> items)
    {
        if (items is null || items.Count > MaximumViItemCount || items.Any(item => item is null))
        {
            throw InvalidData();
        }

        foreach (var item in items)
        {
            ValidateViItem(item);
        }

        var bytes = EncodeStrict(string.Concat(items.Select(item =>
            $"{item.Code}^{item.Name}^^^^^^^\r\n")));
        if (bytes.Length > MaximumViSerializedBytes)
        {
            throw InvalidData();
        }

        return bytes;
    }

    private static void ValidateNetSellCell(string? value)
    {
        if (value is null ||
            value.Length > MaximumNetSellCellLength ||
            value.Contains('^') ||
            value.Any(char.IsControl))
        {
            throw InvalidData();
        }
    }

    private static void ValidateViItem(LegacyManualViItem? item)
    {
        if (item is null ||
            !IsSafeViValue(item.Code, MaximumViCodeLength) ||
            !IsSafeViValue(item.Name, MaximumViNameLength) ||
            item.Name.Contains(','))
        {
            throw InvalidData();
        }
    }

    private static bool IsSafeViValue(string? value, int maximumLength) =>
        value is not null &&
        value.Length is > 0 &&
        value.Length <= maximumLength &&
        string.Equals(value, value.Trim(), StringComparison.Ordinal) &&
        !value.Contains('^') &&
        !value.Any(char.IsControl);

    private static string DecodeStrict(ReadOnlySpan<byte> bytes, bool allowEmpty)
    {
        if ((!allowEmpty && bytes.Length == 0) || HasUnicodeBom(bytes))
        {
            throw InvalidData();
        }

        try
        {
            var copy = bytes.ToArray();
            var text = Cp949.GetString(copy);
            if (!Cp949.GetBytes(text).AsSpan().SequenceEqual(copy))
            {
                throw InvalidData();
            }

            return text;
        }
        catch (Exception exception) when (exception is DecoderFallbackException or EncoderFallbackException)
        {
            throw InvalidData();
        }
    }

    private static byte[] EncodeStrict(string text)
    {
        try
        {
            return Cp949.GetBytes(text);
        }
        catch (EncoderFallbackException)
        {
            throw InvalidData();
        }
    }

    private static bool HasUnicodeBom(ReadOnlySpan<byte> bytes) =>
        bytes.StartsWith(new byte[] { 0xEF, 0xBB, 0xBF }) ||
        bytes.StartsWith(new byte[] { 0xFF, 0xFE }) ||
        bytes.StartsWith(new byte[] { 0xFE, 0xFF }) ||
        bytes.StartsWith(new byte[] { 0x00, 0x00, 0xFE, 0xFF });

    private static Encoding CreateCp949Encoding()
    {
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        return Encoding.GetEncoding(
            949,
            EncoderFallback.ExceptionFallback,
            DecoderFallback.ExceptionFallback);
    }

    private static LegacyManualOperatorDataValidationException InvalidData() => new();
}
