using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace MBN_STOCK_WEBVIEW.Infrastructure;

public sealed class DatabaseOptionsLoader
{
    private static readonly JsonSerializerOptions SerializerOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        ReadCommentHandling = JsonCommentHandling.Skip,
        AllowTrailingCommas = true,
        Converters = { new JsonStringEnumConverter() }
    };

    private readonly Func<string, string?> _readEnvironmentVariable;

    public DatabaseOptionsLoader(Func<string, string?>? readEnvironmentVariable = null)
    {
        _readEnvironmentVariable = readEnvironmentVariable ?? Environment.GetEnvironmentVariable;
    }

    public static string DefaultConfigurationPath
    {
        get
        {
            var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            if (string.IsNullOrWhiteSpace(localAppData))
            {
                throw new DatabaseConfigurationException(
                    "사용자 로컬 애플리케이션 데이터 폴더를 찾을 수 없습니다.");
            }

            return Path.Combine(
                localAppData,
                "MBN_STOCK_WEBVIEW",
                "Config",
                "database.local.json");
        }
    }

    public DatabaseOptions Load(string? jsonPath = null)
    {
        var path = jsonPath ?? DefaultConfigurationPath;
        var options = LoadJson(path);
        return ApplyEnvironmentOverridesAndValidate(options);
    }

    public async Task<DatabaseOptions> LoadAsync(
        string? jsonPath = null,
        CancellationToken cancellationToken = default)
    {
        var path = jsonPath ?? DefaultConfigurationPath;
        var options = await LoadJsonAsync(path, cancellationToken).ConfigureAwait(false);
        return ApplyEnvironmentOverridesAndValidate(options);
    }

    internal DatabaseOptions ApplyEnvironmentOverridesAndValidate(DatabaseOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);
        ApplyEnvironmentOverrides(options);
        options.Normalize();
        options.ValidateRuntimeSettings();
        return options;
    }

    private static DatabaseOptions LoadJson(string path)
    {
        if (!File.Exists(path))
        {
            return new DatabaseOptions();
        }

        try
        {
            var json = File.ReadAllText(path);
            return JsonSerializer.Deserialize<DatabaseOptions>(json, SerializerOptions)
                ?? new DatabaseOptions();
        }
        catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException)
        {
            throw new DatabaseConfigurationException(
                "로컬 데이터베이스 설정 파일을 읽을 수 없습니다.");
        }
    }

    private static async Task<DatabaseOptions> LoadJsonAsync(string path, CancellationToken cancellationToken)
    {
        if (!File.Exists(path))
        {
            return new DatabaseOptions();
        }

        try
        {
            await using var stream = new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                bufferSize: 4_096,
                FileOptions.Asynchronous | FileOptions.SequentialScan);
            return await JsonSerializer
                .DeserializeAsync<DatabaseOptions>(stream, SerializerOptions, cancellationToken)
                .ConfigureAwait(false)
                ?? new DatabaseOptions();
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException)
        {
            throw new DatabaseConfigurationException(
                "로컬 데이터베이스 설정 파일을 읽을 수 없습니다.");
        }
    }

    private void ApplyEnvironmentOverrides(DatabaseOptions options)
    {
        options.Normalize();

        OverrideString("MBN_STOCK_ORACLE_HOST", value => options.Oracle.Host = value);
        OverrideInt32("MBN_STOCK_ORACLE_PORT", value => options.Oracle.Port = value);
        OverrideString("MBN_STOCK_ORACLE_SID", value => options.Oracle.Sid = value);
        OverrideString("MBN_STOCK_ORACLE_SERVICE_NAME", value => options.Oracle.ServiceName = value);
        OverrideString("MBN_STOCK_ORACLE_USERNAME", value => options.Oracle.UserName = value);
        OverrideString("MBN_STOCK_ORACLE_PASSWORD", value => options.Oracle.Password = value);
        OverrideInt32(
            "MBN_STOCK_ORACLE_CONNECT_TIMEOUT_SECONDS",
            value => options.Oracle.ConnectTimeoutSeconds = value);
        OverrideInt32(
            "MBN_STOCK_ORACLE_COMMAND_TIMEOUT_SECONDS",
            value => options.Oracle.CommandTimeoutSeconds = value);
        OverrideBoolean("MBN_STOCK_ORACLE_POOLING", value => options.Oracle.Pooling = value);

        OverrideString("MBN_STOCK_MARIADB_HOST", value => options.MariaDb.Host = value);
        OverrideInt32("MBN_STOCK_MARIADB_PORT", value => options.MariaDb.Port = value);
        OverrideString("MBN_STOCK_MARIADB_DATABASE", value => options.MariaDb.Database = value);
        OverrideString("MBN_STOCK_MARIADB_USERNAME", value => options.MariaDb.UserName = value);
        OverrideString("MBN_STOCK_MARIADB_PASSWORD", value => options.MariaDb.Password = value);
        OverrideEnum<MariaDbTlsMode>(
            "MBN_STOCK_MARIADB_TLS_MODE",
            value => options.MariaDb.TlsMode = value);
        OverrideInt32(
            "MBN_STOCK_MARIADB_CONNECT_TIMEOUT_SECONDS",
            value => options.MariaDb.ConnectTimeoutSeconds = value);
        OverrideInt32(
            "MBN_STOCK_MARIADB_COMMAND_TIMEOUT_SECONDS",
            value => options.MariaDb.CommandTimeoutSeconds = value);
        OverrideBoolean("MBN_STOCK_MARIADB_POOLING", value => options.MariaDb.Pooling = value);

        OverrideInt32(
            "MBN_STOCK_DB_OPERATION_TIMEOUT_SECONDS",
            value => options.Resilience.OperationTimeoutSeconds = value);
        OverrideInt32(
            "MBN_STOCK_DB_MAX_RETRY_COUNT",
            value => options.Resilience.MaximumRetryCount = value);
        OverrideInt32(
            "MBN_STOCK_DB_INITIAL_RETRY_DELAY_MS",
            value => options.Resilience.InitialRetryDelayMilliseconds = value);
    }

    private void OverrideString(string name, Action<string> setter)
    {
        var value = _readEnvironmentVariable(name);
        if (value is not null)
        {
            setter(value.Trim());
        }
    }

    private void OverrideInt32(string name, Action<int> setter)
    {
        var value = _readEnvironmentVariable(name);
        if (value is null)
        {
            return;
        }

        if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
        {
            throw InvalidEnvironmentSetting(name);
        }

        setter(parsed);
    }

    private void OverrideBoolean(string name, Action<bool> setter)
    {
        var value = _readEnvironmentVariable(name);
        if (value is null)
        {
            return;
        }

        if (!bool.TryParse(value, out var parsed))
        {
            throw InvalidEnvironmentSetting(name);
        }

        setter(parsed);
    }

    private void OverrideEnum<T>(string name, Action<T> setter)
        where T : struct, Enum
    {
        var value = _readEnvironmentVariable(name);
        if (value is null)
        {
            return;
        }

        if (!Enum.TryParse<T>(value, ignoreCase: true, out var parsed)
            || !Enum.IsDefined(parsed))
        {
            throw InvalidEnvironmentSetting(name);
        }

        setter(parsed);
    }

    private static DatabaseConfigurationException InvalidEnvironmentSetting(string name) =>
        new($"환경 설정 {name}의 값이 올바르지 않습니다.");
}
