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

namespace MBN_STOCK_WEBVIEW.Playout.Configuration;

public static class PlayoutOptionsLoader
{
    internal const int MaximumDevelopmentLiveConfigurationFileBytes = 64 * 1024;

    public const string LiveAuthorizationEnvironmentVariable =
        "MBN_STOCK_PLAYOUT_AUTHORIZE_LIVE_OUTPUT";

    public const string RequiredLiveAuthorizationValue =
        "I_AUTHORIZE_LIVE_PROGRAM_OUTPUT_FOR_THIS_LAUNCH";

    private static readonly string[] DevelopmentLiveSceneAllowlist =
    [
        "5001", "5006", "5011", "5016", "50160", "5023", "5024", "5025",
        "5026", "5029", "5032", "5037", "5068", "5070", "5072", "5074",
        "5076", "5077", "5078", "5079", "5080", "5081", "5082", "5083",
        "5084", "5085", "5086", "50860", "5087", "5088", "6001", "6067",
        "8001", "8002", "8003", "8018", "8032", "8035", "8040", "8046",
        "8051", "8056", "8061", "8067", "N5001"
    ];

    private static readonly IReadOnlySet<string> DevelopmentLiveProperties =
        new HashSet<string>(StringComparer.Ordinal)
        {
            "mode",
            "host",
            "port",
            "tcpMode",
            "clientPort",
            "sceneDirectory",
            "outputChannel",
            "layoutIndex",
            "legacySceneFadeDuration",
            "legacySceneBackgroundKind",
            "legacySceneBackgroundAssetPath",
            "legacySceneBackgroundVideoLoopCount",
            "legacySceneBackgroundVideoLoopInfinite",
            "legacyBackgroundDirectory",
            "testProcessWindowTitlePattern",
            "testSceneAllowlist",
            "trustedLiveOutputEnabled",
            "queueCapacity",
            "connectTimeoutMilliseconds",
            "operationTimeoutMilliseconds",
            "disconnectTimeoutMilliseconds",
            "processPollIntervalMilliseconds",
            "reconnectDelayMilliseconds",
            "maximumReconnectAttempts",
            "reconnectEnabled",
            "maximumAutomaticRefreshesPerTakeIn"
        };

    public static string DefaultPath => Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "MBN_STOCK_WEBVIEW",
        "Config",
        "playout.local.json");

    public static PlayoutOptions Load(
        string? path = null,
        string? operatorSceneDirectory = null,
        string? operatorBackgroundDirectory = null,
        bool forceSafeDryRun = false)
    {
        // Source-only builds are for code/UI review on machines without the
        // approved runtime tree. Do not consume a previous machine-local playout
        // profile or process override in that build flavor.
        if (forceSafeDryRun)
        {
            return new PlayoutOptions();
        }

        var options = LoadJson(path ?? DefaultPath);
        ApplyOperatorAssetOverrides(
            options,
            operatorSceneDirectory,
            operatorBackgroundDirectory);
        ApplyEnvironment(options);
        ApplyExecutableAssetDefaults(options, AppContext.BaseDirectory);
        return options;
    }

    /// <summary>
    /// Loads the protected LocalAppData Live profile for an already-applied
    /// Development Live bootstrap. This path deliberately ignores process
    /// environment and ordinary playout-profile scene overrides. The scene root
    /// is pinned to the native folder selected during first-run setup.
    /// </summary>
    internal static PlayoutOptions LoadDevelopmentLive(
        string? path = null,
        string? baseDirectory = null,
        string? operatorSceneDirectory = null)
    {
        var options = LoadDevelopmentLiveJson(
            path ?? DefaultPath,
            requireProtectedDefaultPath: path is null);
        EnsureDevelopmentLiveBaseContract(options);

        options.Mode = PlayoutMode.Live;
        options.SceneDirectory = null;
        ApplyOperatorAssetOverrides(
            options,
            operatorSceneDirectory,
            backgroundDirectory: null);
        ApplyExecutableAssetDefaults(
            options,
            baseDirectory ?? AppContext.BaseDirectory);
        options.DevelopmentLiveExecutableSceneDirectory = options.SceneDirectory;
        options.DevelopmentLiveBootstrapApplied = true;
        return options;
    }

    /// <summary>
    /// Applies only the two non-command asset roots selected by the native settings
    /// screen. Process environment values are applied afterwards and therefore retain
    /// precedence for controlled diagnostic and development-live launches.
    /// </summary>
    internal static void ApplyOperatorAssetOverrides(
        PlayoutOptions options,
        string? sceneDirectory,
        string? backgroundDirectory)
    {
        ArgumentNullException.ThrowIfNull(options);
        if (!string.IsNullOrWhiteSpace(sceneDirectory))
        {
            options.SceneDirectory = sceneDirectory.Trim();
        }

        if (!string.IsNullOrWhiteSpace(backgroundDirectory))
        {
            options.LegacyBackgroundDirectory = backgroundDirectory.Trim();
        }
    }

    /// <summary>
    /// Loads only the specified JSON file and deliberately ignores all environment
    /// overrides. Intended for explicit, reproducible diagnostic commands.
    /// </summary>
    public static PlayoutOptions LoadFileOnly(string path)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(path);
        return LoadJson(path);
    }

    internal static bool IsLiveAuthorizedForThisLaunch() =>
        string.Equals(
            Environment.GetEnvironmentVariable(LiveAuthorizationEnvironmentVariable),
            RequiredLiveAuthorizationValue,
            StringComparison.Ordinal);

    internal static void ApplyExecutableAssetDefaults(
        PlayoutOptions options,
        string? baseDirectory)
    {
        ArgumentNullException.ThrowIfNull(options);
        if (!string.IsNullOrWhiteSpace(options.SceneDirectory) ||
            string.IsNullOrWhiteSpace(baseDirectory))
        {
            return;
        }

        try
        {
            var cutsDirectory = Path.Combine(
                Path.GetFullPath(baseDirectory),
                "Cuts");
            if (Directory.Exists(cutsDirectory))
            {
                options.SceneDirectory = cutsDirectory;
            }
        }
        catch (Exception exception) when (
            exception is ArgumentException or IOException or NotSupportedException or
                System.Security.SecurityException)
        {
            // A missing or invalid executable asset root leaves the existing safe
            // configuration unchanged; validation reports it if a command needs it.
        }
    }

    private static void EnsureDevelopmentLiveBaseContract(PlayoutOptions options)
    {
        if (options.Mode != PlayoutMode.Live ||
            !IsLiteralLoopback(options.Host) ||
            options.Port is < 1 or > 65_535 ||
            options.TcpMode != 1 ||
            options.ClientPort != 0 ||
            options.OutputChannel is < 0 ||
            options.LayoutIndex != 10 ||
            !string.IsNullOrWhiteSpace(options.SceneDirectory) ||
            options.LegacySceneFadeDuration != 6 ||
            options.LegacySceneBackgroundKind != LegacySceneBackgroundKind.None ||
            !string.IsNullOrWhiteSpace(options.LegacySceneBackgroundAssetPath) ||
            options.LegacySceneBackgroundVideoLoopCount != 2004 ||
            !options.LegacySceneBackgroundVideoLoopInfinite ||
            !string.IsNullOrWhiteSpace(options.LegacyBackgroundDirectory) ||
            !string.IsNullOrWhiteSpace(options.TestProcessWindowTitlePattern) ||
            options.TestSceneAllowlist is null ||
            !options.TestSceneAllowlist.SequenceEqual(
                DevelopmentLiveSceneAllowlist,
                StringComparer.Ordinal) ||
            !options.TrustedLiveOutputEnabled ||
            options.QueueCapacity != 64 ||
            options.ConnectTimeoutMilliseconds != 5_000 ||
            options.OperationTimeoutMilliseconds != 5_000 ||
            options.DisconnectTimeoutMilliseconds != 3_000 ||
            options.ProcessPollIntervalMilliseconds != 1_000 ||
            options.ReconnectDelayMilliseconds != 1_000 ||
            options.ReconnectEnabled ||
            options.MaximumReconnectAttempts != 0 ||
            options.MaximumAutomaticRefreshesPerTakeIn != 0)
        {
            throw new PlayoutConfigurationException(
                "Development Live requires the exact protected local base configuration.");
        }
    }

    private static PlayoutOptions LoadDevelopmentLiveJson(
        string path,
        bool requireProtectedDefaultPath)
    {
        try
        {
            var fullPath = Path.GetFullPath(path);
            if (requireProtectedDefaultPath)
            {
                EnsureProtectedDevelopmentLivePath(fullPath);
            }

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

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

                json = new byte[checked((int)stream.Length)];
                stream.ReadExactly(json);
            }

            using var document = JsonDocument.Parse(
                json,
                new JsonDocumentOptions
                {
                    AllowTrailingCommas = false,
                    CommentHandling = JsonCommentHandling.Disallow,
                    MaxDepth = 4
                });
            var root = document.RootElement;
            if (root.ValueKind != JsonValueKind.Object)
            {
                throw new InvalidDataException();
            }

            var seen = new HashSet<string>(StringComparer.Ordinal);
            foreach (var property in root.EnumerateObject())
            {
                if (!DevelopmentLiveProperties.Contains(property.Name) ||
                    !seen.Add(property.Name))
                {
                    throw new InvalidDataException();
                }
            }

            if (seen.Count != DevelopmentLiveProperties.Count ||
                !IsExactString(root, "mode", "Live") ||
                !IsLoopbackString(root, "host") ||
                !IsIntegerInRange(root, "port", 1, 65_535) ||
                !IsExactInteger(root, "tcpMode", 1) ||
                !IsExactInteger(root, "clientPort", 0) ||
                !IsNull(root, "sceneDirectory") ||
                !IsNullableNonNegativeInteger(root, "outputChannel") ||
                !IsExactInteger(root, "layoutIndex", 10) ||
                !IsExactInteger(root, "legacySceneFadeDuration", 6) ||
                !IsExactString(root, "legacySceneBackgroundKind", "None") ||
                !IsNull(root, "legacySceneBackgroundAssetPath") ||
                !IsExactInteger(root, "legacySceneBackgroundVideoLoopCount", 2004) ||
                !IsExactBoolean(root, "legacySceneBackgroundVideoLoopInfinite", true) ||
                !IsNull(root, "legacyBackgroundDirectory") ||
                !IsNull(root, "testProcessWindowTitlePattern") ||
                !IsExactStringArray(
                    root,
                    "testSceneAllowlist",
                    DevelopmentLiveSceneAllowlist) ||
                !IsExactBoolean(root, "trustedLiveOutputEnabled", true) ||
                !IsExactInteger(root, "queueCapacity", 64) ||
                !IsExactInteger(root, "connectTimeoutMilliseconds", 5_000) ||
                !IsExactInteger(root, "operationTimeoutMilliseconds", 5_000) ||
                !IsExactInteger(root, "disconnectTimeoutMilliseconds", 3_000) ||
                !IsExactInteger(root, "processPollIntervalMilliseconds", 1_000) ||
                !IsExactInteger(root, "reconnectDelayMilliseconds", 1_000) ||
                !IsExactInteger(root, "maximumReconnectAttempts", 0) ||
                !IsExactBoolean(root, "reconnectEnabled", false) ||
                !IsExactInteger(root, "maximumAutomaticRefreshesPerTakeIn", 0))
            {
                throw new InvalidDataException();
            }

            return JsonSerializer.Deserialize<PlayoutOptions>(
                    json,
                    JsonOptions())
                ?? throw new InvalidDataException();
        }
        catch (Exception exception) when (
            exception is IOException or UnauthorizedAccessException or SecurityException or
                InvalidDataException or ArgumentException or NotSupportedException or
                JsonException)
        {
            throw new PlayoutConfigurationException(
                "Development Live local configuration is invalid.", exception);
        }
    }

    private static void EnsureProtectedDevelopmentLivePath(string fullPath)
    {
        var localApplicationData = Environment.GetFolderPath(
            Environment.SpecialFolder.LocalApplicationData);
        if (string.IsNullOrWhiteSpace(localApplicationData))
        {
            throw new InvalidDataException();
        }

        var localRoot = Path.TrimEndingDirectorySeparator(
            Path.GetFullPath(localApplicationData));
        var expectedPath = Path.Combine(
            localRoot,
            "MBN_STOCK_WEBVIEW",
            "Config",
            "playout.local.json");
        if (!string.Equals(fullPath, expectedPath, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidDataException();
        }

        var directory = Path.GetDirectoryName(fullPath);
        while (!string.IsNullOrWhiteSpace(directory) &&
               !string.Equals(directory, localRoot, StringComparison.OrdinalIgnoreCase))
        {
            if ((File.GetAttributes(directory) & FileAttributes.ReparsePoint) != 0)
            {
                throw new InvalidDataException();
            }

            directory = Path.GetDirectoryName(directory);
        }

        if (!string.Equals(directory, localRoot, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidDataException();
        }
    }

    private static bool IsExactString(
        JsonElement root,
        string propertyName,
        string expected)
    {
        var value = root.GetProperty(propertyName);
        return value.ValueKind == JsonValueKind.String &&
            string.Equals(value.GetString(), expected, StringComparison.Ordinal);
    }

    private static bool IsLoopbackString(JsonElement root, string propertyName)
    {
        var value = root.GetProperty(propertyName);
        return value.ValueKind == JsonValueKind.String &&
            IsLiteralLoopback(value.GetString());
    }

    private static bool IsLiteralLoopback(string? host) =>
        string.Equals(host, "127.0.0.1", StringComparison.Ordinal) ||
        string.Equals(host, "::1", StringComparison.Ordinal);

    private static bool IsIntegerInRange(
        JsonElement root,
        string propertyName,
        int minimum,
        int maximum)
    {
        var value = root.GetProperty(propertyName);
        return value.ValueKind == JsonValueKind.Number &&
            value.TryGetInt32(out var parsed) &&
            parsed >= minimum &&
            parsed <= maximum;
    }

    private static bool IsExactInteger(
        JsonElement root,
        string propertyName,
        int expected) =>
        IsIntegerInRange(root, propertyName, expected, expected);

    private static bool IsNullableNonNegativeInteger(
        JsonElement root,
        string propertyName)
    {
        var value = root.GetProperty(propertyName);
        return value.ValueKind == JsonValueKind.Null ||
            value.ValueKind == JsonValueKind.Number &&
            value.TryGetInt32(out var parsed) &&
            parsed >= 0;
    }

    private static bool IsExactBoolean(
        JsonElement root,
        string propertyName,
        bool expected)
    {
        var value = root.GetProperty(propertyName);
        return expected
            ? value.ValueKind == JsonValueKind.True
            : value.ValueKind == JsonValueKind.False;
    }

    private static bool IsNull(JsonElement root, string propertyName) =>
        root.GetProperty(propertyName).ValueKind == JsonValueKind.Null;

    private static bool IsExactStringArray(
        JsonElement root,
        string propertyName,
        IReadOnlyList<string> expected)
    {
        var value = root.GetProperty(propertyName);
        if (value.ValueKind != JsonValueKind.Array ||
            value.GetArrayLength() != expected.Count)
        {
            return false;
        }

        var index = 0;
        foreach (var item in value.EnumerateArray())
        {
            if (item.ValueKind != JsonValueKind.String ||
                !string.Equals(
                    item.GetString(),
                    expected[index],
                    StringComparison.Ordinal))
            {
                return false;
            }

            index++;
        }

        return true;
    }

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

        try
        {
            var json = File.ReadAllText(path);
            return JsonSerializer.Deserialize<PlayoutOptions>(json, JsonOptions())
                ?? new PlayoutOptions();
        }
        catch (JsonException exception)
        {
            throw new PlayoutConfigurationException(
                "송출 설정 파일 형식이 올바르지 않습니다.", exception);
        }
        catch (IOException exception)
        {
            throw new PlayoutConfigurationException(
                "송출 설정 파일을 읽을 수 없습니다.", exception);
        }
        catch (UnauthorizedAccessException exception)
        {
            throw new PlayoutConfigurationException(
                "송출 설정 파일에 접근할 수 없습니다.", exception);
        }
    }

    private static void ApplyEnvironment(PlayoutOptions options)
    {
        ApplyEnum(
            "MBN_STOCK_PLAYOUT_MODE",
            (PlayoutMode value) => options.Mode = value);
        ApplyString("MBN_STOCK_PLAYOUT_HOST", value => options.Host = value);
        ApplyInt("MBN_STOCK_PLAYOUT_PORT", value => options.Port = value);
        ApplyInt("MBN_STOCK_PLAYOUT_TCP_MODE", value => options.TcpMode = value);
        ApplyInt("MBN_STOCK_PLAYOUT_CLIENT_PORT", value => options.ClientPort = value);
        ApplyNullableInt("MBN_STOCK_PLAYOUT_OUTPUT_CHANNEL", value => options.OutputChannel = value);
        ApplyInt("MBN_STOCK_PLAYOUT_LAYOUT_INDEX", value => options.LayoutIndex = value);
        ApplyInt(
            "MBN_STOCK_PLAYOUT_LEGACY_FADE_DURATION",
            value => options.LegacySceneFadeDuration = value);
        ApplyEnum(
            "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_KIND",
            (LegacySceneBackgroundKind value) => options.LegacySceneBackgroundKind = value);
        ApplyString(
            "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_ASSET",
            value => options.LegacySceneBackgroundAssetPath = value);
        ApplyInt(
            "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_COUNT",
            value => options.LegacySceneBackgroundVideoLoopCount = value);
        ApplyBool(
            "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_INFINITE",
            value => options.LegacySceneBackgroundVideoLoopInfinite = value);
        ApplyString(
            "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY",
            value => options.LegacyBackgroundDirectory = value);
        ApplyString("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", value => options.SceneDirectory = value);
        ApplyString(
            "MBN_STOCK_PLAYOUT_TEST_WINDOW_TITLE_PATTERN",
            value => options.TestProcessWindowTitlePattern = value);
        ApplyInt("MBN_STOCK_PLAYOUT_QUEUE_CAPACITY", value => options.QueueCapacity = value);
        ApplyInt(
            "MBN_STOCK_PLAYOUT_CONNECT_TIMEOUT_MS",
            value => options.ConnectTimeoutMilliseconds = value);
        ApplyInt(
            "MBN_STOCK_PLAYOUT_OPERATION_TIMEOUT_MS",
            value => options.OperationTimeoutMilliseconds = value);
        ApplyInt(
            "MBN_STOCK_PLAYOUT_DISCONNECT_TIMEOUT_MS",
            value => options.DisconnectTimeoutMilliseconds = value);
        ApplyInt(
            "MBN_STOCK_PLAYOUT_PROCESS_POLL_INTERVAL_MS",
            value => options.ProcessPollIntervalMilliseconds = value);
        ApplyInt(
            "MBN_STOCK_PLAYOUT_RECONNECT_DELAY_MS",
            value => options.ReconnectDelayMilliseconds = value);
        ApplyInt(
            "MBN_STOCK_PLAYOUT_MAXIMUM_RECONNECT_ATTEMPTS",
            value => options.MaximumReconnectAttempts = value);
        ApplyBool(
            "MBN_STOCK_PLAYOUT_RECONNECT_ENABLED",
            value => options.ReconnectEnabled = value);
    }

    private static JsonSerializerOptions JsonOptions()
    {
        var result = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
            ReadCommentHandling = JsonCommentHandling.Skip,
            AllowTrailingCommas = true
        };
        result.Converters.Add(new JsonStringEnumConverter());
        return result;
    }

    private static void ApplyString(string name, Action<string> apply)
    {
        var value = Environment.GetEnvironmentVariable(name);
        if (!string.IsNullOrWhiteSpace(value))
        {
            apply(value.Trim());
        }
    }

    private static void ApplyInt(string name, Action<int> apply)
    {
        var text = Environment.GetEnvironmentVariable(name);
        if (string.IsNullOrWhiteSpace(text))
        {
            return;
        }

        if (!int.TryParse(text, out var value))
        {
            throw InvalidEnvironment(name);
        }

        apply(value);
    }

    private static void ApplyNullableInt(string name, Action<int?> apply)
    {
        var text = Environment.GetEnvironmentVariable(name);
        if (string.IsNullOrWhiteSpace(text))
        {
            return;
        }

        if (!int.TryParse(text, out var value))
        {
            throw InvalidEnvironment(name);
        }

        apply(value);
    }

    private static void ApplyBool(string name, Action<bool> apply)
    {
        var text = Environment.GetEnvironmentVariable(name);
        if (string.IsNullOrWhiteSpace(text))
        {
            return;
        }

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

        apply(value);
    }

    private static void ApplyEnum<TEnum>(string name, Action<TEnum> apply)
        where TEnum : struct, Enum
    {
        var text = Environment.GetEnvironmentVariable(name);
        if (string.IsNullOrWhiteSpace(text))
        {
            return;
        }

        if (!Enum.TryParse<TEnum>(text, true, out var value) || !Enum.IsDefined(value))
        {
            throw InvalidEnvironment(name);
        }

        apply(value);
    }

    private static PlayoutConfigurationException InvalidEnvironment(string name) =>
        new($"환경 변수 {name} 값이 올바르지 않습니다.");
}

public sealed class PlayoutConfigurationException : Exception
{
    public PlayoutConfigurationException(string message)
        : base(message)
    {
    }

    public PlayoutConfigurationException(string message, Exception innerException)
        : base(message, innerException)
    {
    }
}
