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

namespace MBN_STOCK_WEBVIEW.Infrastructure;

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

public sealed record ViTrustedManualSnapshot(
    IReadOnlyList<ViTrustedManualItem> Items,
    string RowVersion);

public sealed record ViTrustedManualWriteResult(
    ViTrustedManualSnapshot Snapshot,
    bool Persisted);

public sealed class ViTrustedManualDataException : Exception
{
    internal ViTrustedManualDataException()
        : base("The trusted VI manual-data store is unavailable or invalid.")
    {
    }
}

/// <summary>
/// Closed store for the original VI발동.dat operator list. The path remains a
/// composition-root decision; WebView callers can read or replace only the
/// validated code/name rows and cannot choose a file.
/// </summary>
public sealed class ViTrustedManualFileStore : ITrustedViStockNameSnapshotSource, IDisposable
{
    public const int MaximumItemCount = 100;
    public const int MaximumFileSizeBytes = 64 * 1024;

    private const string FileName = "VI발동.dat";
    private const int MaximumCodeLength = 64;
    private const int MaximumNameLength = 200;
    private static readonly Encoding Cp949 = CreateCp949Encoding();

    private readonly TrustedDirectoryLease _directoryLease;
    private readonly S5025TrustedManualFileDataSource _pathBoundary;

    public ViTrustedManualFileStore(string trustedDirectory)
    {
        _directoryLease = TrustedManualDirectory.AcquirePrivateWritableLease(trustedDirectory);
        _pathBoundary = new S5025TrustedManualFileDataSource(trustedDirectory);
    }

    public async Task<IReadOnlyList<ViTrustedManualItem>> ReadAsync(
        CancellationToken cancellationToken = default) =>
        (await ReadSnapshotAsync(cancellationToken).ConfigureAwait(false)).Items;

    public async Task<ViTrustedManualSnapshot> ReadSnapshotAsync(
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var directory = _pathBoundary.TrustedDirectory;
        var path = _pathBoundary.GetContainedFilePath(FileName);
        try
        {
            S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(directory);
            if (!File.Exists(path))
            {
                return CreateSnapshot(Array.Empty<ViTrustedManualItem>());
            }

            var attributes = File.GetAttributes(path);
            if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0)
            {
                throw InvalidData();
            }

            byte[] bytes;
            await using (var stream = new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                bufferSize: 4_096,
                FileOptions.Asynchronous | FileOptions.SequentialScan))
            {
                TrustedDirectoryLease.EnsureRegularFileHandle(
                    stream.SafeFileHandle,
                    path,
                    MaximumFileSizeBytes,
                    allowEmpty: true);
                if (stream.Length > MaximumFileSizeBytes)
                {
                    throw InvalidData();
                }

                bytes = new byte[checked((int)stream.Length)];
                var offset = 0;
                while (offset < bytes.Length)
                {
                    var read = await stream.ReadAsync(
                        bytes.AsMemory(offset), cancellationToken).ConfigureAwait(false);
                    if (read == 0)
                    {
                        throw InvalidData();
                    }

                    offset += read;
                }
            }

            S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(directory);
            EnsureViFile(path);
            if (bytes.Length == 0)
            {
                return CreateSnapshot(Array.Empty<ViTrustedManualItem>());
            }

            RejectUnicodeBom(bytes);
            var text = Cp949.GetString(bytes);
            var rows = new List<ViTrustedManualItem>();
            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 ViTrustedManualItem(columns[0], columns[1]);
                ValidateItem(item);
                rows.Add(item);
                if (rows.Count > MaximumItemCount)
                {
                    throw InvalidData();
                }
            }

            return CreateSnapshot(rows);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (ViTrustedManualDataException)
        {
            throw;
        }
        catch (Exception exception) when (exception is
            IOException or UnauthorizedAccessException or SecurityException or
            ArgumentException or NotSupportedException or DecoderFallbackException)
        {
            throw InvalidData();
        }
    }

    public async Task<ViTrustedManualWriteResult> WriteAsync(
        IReadOnlyList<ViTrustedManualItem> items,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (items is null || items.Count > MaximumItemCount)
        {
            throw InvalidData();
        }

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

        // Original VILIST did not call WriteListFile when the grid was empty.
        // Preserve the existing file and return its current version explicitly.
        if (items.Count == 0)
        {
            return new ViTrustedManualWriteResult(
                await ReadSnapshotAsync(cancellationToken).ConfigureAwait(false),
                Persisted: false);
        }

        byte[] bytes;
        try
        {
            bytes = SerializeCanonical(items);
        }
        catch (EncoderFallbackException)
        {
            throw InvalidData();
        }

        if (bytes.Length > MaximumFileSizeBytes)
        {
            throw InvalidData();
        }

        var directory = _pathBoundary.TrustedDirectory;
        var destination = _pathBoundary.GetContainedFilePath(FileName);
        var temporary = _pathBoundary.GetContainedFilePath(
            $".{FileName}.{Guid.NewGuid():N}.tmp");
        try
        {
            S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(directory);
            if (File.Exists(destination))
            {
                EnsureViFile(destination);
            }

            await using (var stream = new FileStream(
                temporary,
                FileMode.CreateNew,
                FileAccess.Write,
                FileShare.None,
                bufferSize: 4_096,
                FileOptions.Asynchronous | FileOptions.WriteThrough))
            {
                TrustedDirectoryLease.EnsureRegularFileHandle(
                    stream.SafeFileHandle,
                    temporary,
                    MaximumFileSizeBytes,
                    allowEmpty: true);
                await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
                await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
                stream.Flush(flushToDisk: true);
            }

            S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(directory);
            var temporaryAttributes = File.GetAttributes(temporary);
            if ((temporaryAttributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0 ||
                new FileInfo(temporary).Length > MaximumFileSizeBytes)
            {
                throw InvalidData();
            }

            if (File.Exists(destination))
            {
                EnsureViFile(destination);
            }

            File.Move(temporary, destination, overwrite: true);
            S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(directory);
            var persisted = await ReadSnapshotAsync(cancellationToken).ConfigureAwait(false);
            var expectedVersion = ComputeRowVersion(bytes);
            if (!persisted.Items.SequenceEqual(items) ||
                !string.Equals(persisted.RowVersion, expectedVersion, StringComparison.Ordinal))
            {
                throw InvalidData();
            }

            return new ViTrustedManualWriteResult(persisted, Persisted: true);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (ViTrustedManualDataException)
        {
            throw;
        }
        catch (Exception exception) when (exception is
            IOException or UnauthorizedAccessException or SecurityException or
            ArgumentException or NotSupportedException)
        {
            throw InvalidData();
        }
        finally
        {
            try
            {
                if (File.Exists(temporary))
                {
                    var attributes = File.GetAttributes(temporary);
                    if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) == 0)
                    {
                        File.Delete(temporary);
                    }
                }
            }
            catch (Exception exception) when (exception is
                IOException or UnauthorizedAccessException or SecurityException)
            {
                // A leftover hidden temporary file is never consumed by the store.
            }
        }
    }

    public async Task<TrustedViStockNameSnapshot> ReadStockNameSnapshotAsync(
        CancellationToken cancellationToken = default)
    {
        var snapshot = await ReadSnapshotAsync(cancellationToken).ConfigureAwait(false);
        return new TrustedViStockNameSnapshot(
            Array.AsReadOnly(snapshot.Items.Select(item => item.Name).ToArray()),
            snapshot.RowVersion);
    }

    public void Dispose() => _directoryLease.Dispose();

    private static void ValidateItem(ViTrustedManualItem? item)
    {
        if (item is null ||
            !IsSafe(item.Code, MaximumCodeLength) ||
            !IsSafe(item.Name, MaximumNameLength) ||
            item.Name.Contains(','))
        {
            throw InvalidData();
        }
    }

    private static bool IsSafe(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 void EnsureViFile(string path)
    {
        var attributes = File.GetAttributes(path);
        if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0 ||
            new FileInfo(path).Length > MaximumFileSizeBytes)
        {
            throw InvalidData();
        }
    }

    private static ViTrustedManualSnapshot CreateSnapshot(
        IReadOnlyList<ViTrustedManualItem> items)
    {
        var immutable = Array.AsReadOnly(items.ToArray());
        byte[] canonical;
        try
        {
            canonical = SerializeCanonical(immutable);
        }
        catch (EncoderFallbackException)
        {
            throw InvalidData();
        }

        return new ViTrustedManualSnapshot(immutable, ComputeRowVersion(canonical));
    }

    private static byte[] SerializeCanonical(IEnumerable<ViTrustedManualItem> items)
    {
        var text = string.Concat(items.Select(item =>
            $"{item.Code}^{item.Name}^^^^^^^\r\n"));
        return Cp949.GetBytes(text);
    }

    private static string ComputeRowVersion(ReadOnlySpan<byte> canonicalBytes) =>
        Convert.ToHexString(SHA256.HashData(canonicalBytes)).ToLowerInvariant();

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

    private static void RejectUnicodeBom(ReadOnlySpan<byte> bytes)
    {
        if (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 }))
        {
            throw InvalidData();
        }
    }

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