using System.Security;
using System.Text;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Infrastructure;

/// <summary>
/// Reads the legacy executable-relative Res\MmoneyCoder.ini contract without
/// exposing any of its values through diagnostics or exception messages.
/// </summary>
public sealed class LegacyIniDatabaseOptionsLoader
{
    public const int MaximumFileBytes = 64 * 1024;

    private static readonly UTF8Encoding StrictUtf8 =
        new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
    private static readonly Encoding StrictCp949 = CreateCp949Encoding();

    private readonly DatabaseOptionsLoader _optionsLoader;

    public LegacyIniDatabaseOptionsLoader(
        Func<string, string?>? readEnvironmentVariable = null)
    {
        _optionsLoader = new DatabaseOptionsLoader(readEnvironmentVariable);
    }

    public DatabaseOptions Load(string path)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(path);

        byte[] bytes;
        try
        {
            var fullPath = Path.GetFullPath(path);
            var attributes = File.GetAttributes(fullPath);
            if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0)
            {
                throw new InvalidDataException();
            }

            using var stream = new FileStream(
                fullPath,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                bufferSize: 4096,
                FileOptions.SequentialScan);
            if (stream.Length is <= 0 or > MaximumFileBytes)
            {
                throw new InvalidDataException();
            }

            bytes = new byte[checked((int)stream.Length)];
            stream.ReadExactly(bytes);
        }
        catch (Exception exception) when (IsExpectedFileFailure(exception))
        {
            throw InvalidConfiguration();
        }

        var text = Decode(bytes);
        var values = Parse(text);
        var options = CreateOptions(values);
        options = _optionsLoader.ApplyEnvironmentOverridesAndValidate(options);
        if (!options.IsConfigured(DataSourceKind.Oracle) ||
            !options.IsConfigured(DataSourceKind.MariaDb))
        {
            throw InvalidConfiguration();
        }

        return options;
    }

    private static string Decode(byte[] bytes)
    {
        string text;
        try
        {
            text = StrictUtf8.GetString(bytes);
        }
        catch (DecoderFallbackException)
        {
            try
            {
                text = StrictCp949.GetString(bytes);
            }
            catch (DecoderFallbackException)
            {
                throw InvalidConfiguration();
            }
        }

        return text.Length > 0 && text[0] == '\uFEFF'
            ? text[1..]
            : text;
    }

    private static IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> Parse(
        string text)
    {
        var sections = new Dictionary<string, Dictionary<string, string>>(
            StringComparer.Ordinal);
        Dictionary<string, string>? current = null;

        using var reader = new StringReader(text);
        while (reader.ReadLine() is { } sourceLine)
        {
            var line = sourceLine.Trim();
            if (line.Length == 0)
            {
                continue;
            }

            if (line.StartsWith("[", StringComparison.Ordinal))
            {
                if (line.Length < 3 || line[^1] != ']')
                {
                    throw InvalidConfiguration();
                }

                var sectionName = line[1..^1];
                if (!sections.TryGetValue(sectionName, out current))
                {
                    current = new Dictionary<string, string>(StringComparer.Ordinal);
                    sections.Add(sectionName, current);
                }

                continue;
            }

            if (line.StartsWith("//", StringComparison.Ordinal))
            {
                continue;
            }

            if (current is null)
            {
                throw InvalidConfiguration();
            }

            var separatorIndex = line.IndexOf('=');
            if (separatorIndex <= 0)
            {
                continue;
            }

            var key = line[..separatorIndex].Trim();
            if (key is not ("ConnectionName" or "ID" or "Pass"))
            {
                continue;
            }

            var value = line[(separatorIndex + 1)..].Trim();
            if (value.Length == 0 || value.Any(char.IsControl))
            {
                throw InvalidConfiguration();
            }

            // The original reader keeps the last occurrence in file order.
            current[key] = value;
        }

        return sections.ToDictionary(
            static pair => pair.Key,
            static pair => (IReadOnlyDictionary<string, string>)pair.Value,
            StringComparer.Ordinal);
    }

    private static DatabaseOptions CreateOptions(
        IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> sections)
    {
        if (!sections.TryGetValue("Oracle", out var oracle) ||
            !sections.TryGetValue("Maria", out var maria) ||
            !TryGetRequired(oracle, "ConnectionName", out var oracleConnection) ||
            !TryGetRequired(oracle, "ID", out var oracleId) ||
            !TryGetRequired(oracle, "Pass", out var oraclePassword) ||
            !TryGetRequired(maria, "ConnectionName", out var mariaConnection) ||
            !TryGetRequired(maria, "ID", out var mariaId) ||
            !TryGetRequired(maria, "Pass", out var mariaPassword) ||
            !TryParseConnectionName(
                oracleConnection,
                out var oracleHost,
                out var oraclePort,
                out var oracleSid) ||
            !TryParseConnectionName(
                mariaConnection,
                out var mariaHost,
                out var mariaPort,
                out var mariaDatabase))
        {
            throw InvalidConfiguration();
        }

        return new DatabaseOptions
        {
            Oracle = new OracleDatabaseOptions
            {
                Host = oracleHost,
                Port = oraclePort,
                Sid = oracleSid,
                ServiceName = null,
                UserName = oracleId,
                Password = oraclePassword
            },
            MariaDb = new MariaDbDatabaseOptions
            {
                Host = mariaHost,
                Port = mariaPort,
                Database = mariaDatabase,
                UserName = mariaId,
                Password = mariaPassword,
                // The legacy MySQL connection string did not request TLS.
                TlsMode = MariaDbTlsMode.Disabled
            }
        };
    }

    private static bool TryGetRequired(
        IReadOnlyDictionary<string, string> section,
        string key,
        out string value)
    {
        if (section.TryGetValue(key, out var candidate) &&
            !string.IsNullOrWhiteSpace(candidate))
        {
            value = candidate;
            return true;
        }

        value = string.Empty;
        return false;
    }

    private static bool TryParseConnectionName(
        string value,
        out string host,
        out int port,
        out string database)
    {
        host = string.Empty;
        port = 0;
        database = string.Empty;

        var colonIndex = value.IndexOf(':');
        var slashIndex = value.IndexOf('/', colonIndex + 1);
        if (colonIndex <= 0 || slashIndex <= colonIndex + 1 ||
            slashIndex == value.Length - 1 ||
            value.IndexOf(':', colonIndex + 1) >= 0 ||
            value.IndexOf('/', slashIndex + 1) >= 0)
        {
            return false;
        }

        var parsedHost = value[..colonIndex].Trim();
        var portText = value[(colonIndex + 1)..slashIndex].Trim();
        var parsedDatabase = value[(slashIndex + 1)..].Trim();
        if (parsedHost.Length == 0 || parsedDatabase.Length == 0 ||
            parsedHost.Any(character => char.IsWhiteSpace(character) || char.IsControl(character)) ||
            parsedDatabase.Any(character => char.IsWhiteSpace(character) || char.IsControl(character)) ||
            !int.TryParse(portText, out var parsedPort) ||
            parsedPort is < 1 or > 65_535)
        {
            return false;
        }

        host = parsedHost;
        port = parsedPort;
        database = parsedDatabase;
        return true;
    }

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

    private static bool IsExpectedFileFailure(Exception exception) =>
        exception is IOException or UnauthorizedAccessException or SecurityException or
            ArgumentException or NotSupportedException;

    private static DatabaseConfigurationException InvalidConfiguration() =>
        new("실행 폴더의 레거시 데이터베이스 설정을 읽을 수 없습니다.");
}
