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

namespace MBN_STOCK_WEBVIEW.Infrastructure;

/// <summary>
/// Raised when the trusted s5025 manual-data boundary cannot prove that its
/// configuration or input file satisfies the closed production contract.
/// The exception deliberately does not retain filesystem exceptions, which can
/// contain the configured external path.
/// </summary>
public sealed class S5025TrustedManualDataException : Exception
{
    internal S5025TrustedManualDataException()
        : base("The trusted s5025 manual-data source is unavailable or invalid.")
    {
    }
}

/// <summary>
/// Reads the three legacy CP949 s5025 manual-data files from one directory
/// selected only by the application composition root. Callers select a closed
/// audience value and can never supply a path or filename.
/// </summary>
public sealed class S5025TrustedManualFileDataSource : IS5025TrustedManualDataSource
{
    public const string DirectoryEnvironmentVariable =
        "MBN_STOCK_S5025_MANUAL_DATA_DIRECTORY";

    public const int MaximumFileSizeBytes = 32 * 1024;

    internal const int ExpectedRowCount = 5;
    internal const int ExpectedColumnCount = 4;
    internal const int MaximumCellLength = 256;

    internal static readonly Encoding Cp949Encoding = CreateCp949Encoding();

    private readonly string _trustedDirectory;
    private readonly StringComparison _pathComparison = OperatingSystem.IsWindows()
        ? StringComparison.OrdinalIgnoreCase
        : StringComparison.Ordinal;

    /// <summary>
    /// Creates the source from an explicit composition-root setting. The
    /// directory must already exist and be an absolute, non-reparse path.
    /// </summary>
    public S5025TrustedManualFileDataSource(string trustedDirectory)
    {
        _trustedDirectory = NormalizeAndValidateTrustedDirectory(trustedDirectory);
    }

    /// <summary>
    /// Creates the source from the process environment. The injectable reader
    /// is intended for composition-root tests and does not alter the closed
    /// runtime data-source contract.
    /// </summary>
    public static S5025TrustedManualFileDataSource CreateFromEnvironment(
        Func<string, string?>? readEnvironmentVariable = null)
    {
        var read = readEnvironmentVariable ?? Environment.GetEnvironmentVariable;
        return new S5025TrustedManualFileDataSource(
            read(DirectoryEnvironmentVariable) ?? string.Empty);
    }

    public async Task<IReadOnlyList<S5025TrustedManualRow>> ReadAsync(
        S5025ManualAudience audience,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var fileName = GetFileName(audience);

        try
        {
            EnsureDirectoryIsTrusted(_trustedDirectory);
            var path = GetContainedFilePath(fileName);
            EnsureRegularNonReparseFile(path);

            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: false);
                if (stream.Length <= 0 || 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;
                }

                if (await stream.ReadAsync(
                    new byte[1],
                    cancellationToken).ConfigureAwait(false) != 0)
                {
                    throw InvalidData();
                }
            }

            // Re-check the closed path after reading. FileShare.Read prevents a
            // conforming Windows writer from replacing or modifying the file
            // while the handle is open; this second check also detects a path
            // replacement immediately after close.
            EnsureDirectoryIsTrusted(_trustedDirectory);
            EnsureRegularNonReparseFile(path);
            return Parse(bytes);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (S5025TrustedManualDataException)
        {
            throw;
        }
        catch (Exception exception) when (exception is
            IOException or
            UnauthorizedAccessException or
            SecurityException or
            ArgumentException or
            NotSupportedException or
            DecoderFallbackException)
        {
            throw InvalidData();
        }
    }

    internal string TrustedDirectory => _trustedDirectory;

    internal static string GetFileName(S5025ManualAudience audience) => audience switch
    {
        S5025ManualAudience.Individual => "개인.dat",
        S5025ManualAudience.Foreign => "외국인.dat",
        S5025ManualAudience.Institution => "기관.dat",
        _ => throw InvalidData()
    };

    internal string GetContainedFilePath(string fileName)
    {
        var candidate = Path.GetFullPath(Path.Combine(_trustedDirectory, fileName));
        var prefix = Path.EndsInDirectorySeparator(_trustedDirectory)
            ? _trustedDirectory
            : _trustedDirectory + Path.DirectorySeparatorChar;

        if (!candidate.StartsWith(prefix, _pathComparison))
        {
            throw InvalidData();
        }

        var relative = Path.GetRelativePath(_trustedDirectory, candidate);
        if (Path.IsPathRooted(relative)
            || relative.Equals("..", _pathComparison)
            || relative.StartsWith(".." + Path.DirectorySeparatorChar, _pathComparison)
            || relative.StartsWith(".." + Path.AltDirectorySeparatorChar, _pathComparison)
            || !relative.Equals(fileName, _pathComparison))
        {
            throw InvalidData();
        }

        return candidate;
    }

    private static IReadOnlyList<S5025TrustedManualRow> Parse(byte[] bytes)
    {
        RejectUnicodeBom(bytes);

        string text;
        try
        {
            text = Cp949Encoding.GetString(bytes);
        }
        catch (DecoderFallbackException)
        {
            throw InvalidData();
        }

        var lines = new List<string>(ExpectedRowCount);
        using (var reader = new StringReader(text))
        {
            while (reader.ReadLine() is { } line)
            {
                lines.Add(line.Trim());
            }
        }

        // FSell always wrote five rows plus exactly one empty terminal record.
        // Original s5025 rendered `row - 1`; accepting a five-row file without
        // that record would therefore render one more row than the original.
        if (lines.Count != ExpectedRowCount + 1 || lines[^1].Length != 0)
        {
            throw InvalidData();
        }
        lines.RemoveAt(lines.Count - 1);

        var rows = new S5025TrustedManualRow[ExpectedRowCount];
        for (var rowIndex = 0; rowIndex < lines.Count; rowIndex++)
        {
            var columns = lines[rowIndex].Split('^');
            if (columns.Length != ExpectedColumnCount
                || columns.Any(column =>
                    column.Length > MaximumCellLength || column.Any(char.IsControl)))
            {
                throw InvalidData();
            }

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

        return rows;
    }

    private static string NormalizeAndValidateTrustedDirectory(string trustedDirectory)
    {
        if (string.IsNullOrWhiteSpace(trustedDirectory)
            || trustedDirectory.IndexOf('\0') >= 0
            || !Path.IsPathFullyQualified(trustedDirectory))
        {
            throw InvalidData();
        }

        try
        {
            var fullPath = Path.TrimEndingDirectorySeparator(
                Path.GetFullPath(trustedDirectory));
            EnsureDirectoryIsTrusted(fullPath);
            return fullPath;
        }
        catch (S5025TrustedManualDataException)
        {
            throw;
        }
        catch (Exception exception) when (exception is
            IOException or
            UnauthorizedAccessException or
            SecurityException or
            ArgumentException or
            NotSupportedException)
        {
            throw InvalidData();
        }
    }

    internal static void EnsureDirectoryIsTrusted(string directory)
    {
        var current = new DirectoryInfo(directory);
        while (current is not null)
        {
            var attributes = File.GetAttributes(current.FullName);
            if ((attributes & FileAttributes.Directory) == 0
                || (attributes & FileAttributes.ReparsePoint) != 0)
            {
                throw InvalidData();
            }

            current = current.Parent;
        }
    }

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

        var length = new FileInfo(path).Length;
        if (length <= 0 || length > MaximumFileSizeBytes)
        {
            throw InvalidData();
        }
    }

    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();
        }
    }

    internal static S5025TrustedManualDataException InvalidData() => new();
}

/// <summary>
/// Closed read/write boundary used by the migrated FSell operator screen. The
/// browser selects only one of the three legacy audiences; it can never supply
/// a directory or file name. Writes are validated, CP949 encoded, and replaced
/// atomically inside the composition-root-owned trusted directory.
/// </summary>
public sealed class S5025TrustedManualFileStore : IS5025TrustedManualDataSource, IDisposable
{
    private readonly TrustedDirectoryLease _directoryLease;
    private readonly S5025TrustedManualFileDataSource _source;

    public S5025TrustedManualFileStore(string trustedDirectory)
    {
        _directoryLease = TrustedManualDirectory.AcquirePrivateWritableLease(trustedDirectory);
        _source = new S5025TrustedManualFileDataSource(trustedDirectory);
    }

    public static S5025TrustedManualFileStore CreateFromEnvironment(
        Func<string, string?>? readEnvironmentVariable = null)
    {
        var read = readEnvironmentVariable ?? Environment.GetEnvironmentVariable;
        return new S5025TrustedManualFileStore(
            read(S5025TrustedManualFileDataSource.DirectoryEnvironmentVariable) ?? string.Empty);
    }

    public Task<IReadOnlyList<S5025TrustedManualRow>> ReadAsync(
        S5025ManualAudience audience,
        CancellationToken cancellationToken = default) =>
        _source.ReadAsync(audience, cancellationToken);

    public async Task WriteAsync(
        S5025ManualAudience audience,
        IReadOnlyList<S5025TrustedManualRow> rows,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var fileName = S5025TrustedManualFileDataSource.GetFileName(audience);
        ValidateRows(rows);

        string text;
        byte[] bytes;
        try
        {
            // FSell wrote five records and one empty terminal record. Preserve
            // that exact shape because the scene reader deliberately validates it.
            text = string.Join("\r\n", rows.Select(row => string.Join(
                '^',
                row.LeftName,
                row.LeftAmount,
                row.RightName,
                row.RightAmount))) + "\r\n\r\n";
            bytes = S5025TrustedManualFileDataSource.Cp949Encoding.GetBytes(text);
        }
        catch (EncoderFallbackException)
        {
            throw S5025TrustedManualFileDataSource.InvalidData();
        }

        if (bytes.Length <= 0 ||
            bytes.Length > S5025TrustedManualFileDataSource.MaximumFileSizeBytes)
        {
            throw S5025TrustedManualFileDataSource.InvalidData();
        }

        var directory = _source.TrustedDirectory;
        var destination = _source.GetContainedFilePath(fileName);
        var temporaryName = $".{fileName}.{Guid.NewGuid():N}.tmp";
        var temporary = _source.GetContainedFilePath(temporaryName);

        try
        {
            S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(directory);
            if (File.Exists(destination))
            {
                S5025TrustedManualFileDataSource.EnsureRegularNonReparseFile(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,
                    S5025TrustedManualFileDataSource.MaximumFileSizeBytes,
                    allowEmpty: true);
                await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
                await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
                stream.Flush(flushToDisk: true);
            }

            S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(directory);
            S5025TrustedManualFileDataSource.EnsureRegularNonReparseFile(temporary);
            if (File.Exists(destination))
            {
                S5025TrustedManualFileDataSource.EnsureRegularNonReparseFile(destination);
            }

            File.Move(temporary, destination, overwrite: true);
            S5025TrustedManualFileDataSource.EnsureDirectoryIsTrusted(directory);
            S5025TrustedManualFileDataSource.EnsureRegularNonReparseFile(destination);

            // Read through the production parser before reporting success. This
            // makes a malformed or partially persisted file fail closed.
            _ = await _source.ReadAsync(audience, cancellationToken).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (S5025TrustedManualDataException)
        {
            throw;
        }
        catch (Exception exception) when (exception is
            IOException or
            UnauthorizedAccessException or
            SecurityException or
            ArgumentException or
            NotSupportedException)
        {
            throw S5025TrustedManualFileDataSource.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 failed cleanup does not alter the success/failure result. A
                // hidden temporary file is never considered by the scene source.
            }
        }
    }

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

    private static void ValidateRows(IReadOnlyList<S5025TrustedManualRow>? rows)
    {
        if (rows is null || rows.Count != S5025TrustedManualFileDataSource.ExpectedRowCount)
        {
            throw S5025TrustedManualFileDataSource.InvalidData();
        }

        foreach (var row in rows)
        {
            if (row is null)
            {
                throw S5025TrustedManualFileDataSource.InvalidData();
            }

            var values = new[]
            {
                row.LeftName,
                row.LeftAmount,
                row.RightName,
                row.RightAmount
            };
            if (values.Any(value =>
                value is null ||
                value.Length > S5025TrustedManualFileDataSource.MaximumCellLength ||
                value.Contains('^') ||
                value.Any(char.IsControl)))
            {
                throw S5025TrustedManualFileDataSource.InvalidData();
            }
        }
    }
}
