using System.Text.RegularExpressions;
using System.Net;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MBN_STOCK_WEBVIEW.Playout.Interop;
using MBN_STOCK_WEBVIEW.Playout.Runtime;

namespace MBN_STOCK_WEBVIEW.Playout.Configuration;

internal sealed record ValidatedPlayoutOptions(
    PlayoutMode Mode,
    string Host,
    int Port,
    int TcpMode,
    int ClientPort,
    int? OutputChannel,
    int LayoutIndex,
    string? SceneDirectory,
    string? OperatorBackgroundDirectory,
    Regex? TestProcessWindowTitleRegex,
    IReadOnlySet<string> TestSceneAllowlist,
    bool TrustedLiveOutputEnabled,
    int QueueCapacity,
    TimeSpan ConnectTimeout,
    TimeSpan OperationTimeout,
    TimeSpan DisconnectTimeout,
    TimeSpan ProcessPollInterval,
    TimeSpan ReconnectDelay,
    int MaximumReconnectAttempts,
    bool ReconnectEnabled)
{
    public static ValidatedPlayoutOptions Create(PlayoutOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);
        if (!Enum.IsDefined(options.Mode))
        {
            throw Invalid(nameof(options.Mode));
        }

        var host = RequiredText(options.Host, nameof(options.Host));
        RequireRange(options.Port, 1, 65_535, nameof(options.Port));
        RequireRange(options.TcpMode, 0, 1, nameof(options.TcpMode));
        if (options.Mode is PlayoutMode.Test or PlayoutMode.Live && options.TcpMode != 1)
        {
            throw Invalid(nameof(options.TcpMode));
        }
        RequireRange(options.ClientPort, 0, 65_535, nameof(options.ClientPort));
        if (options.OutputChannel is { } outputChannel)
        {
            RequireRange(outputChannel, 0, int.MaxValue, nameof(options.OutputChannel));
        }

        if (options.LayoutIndex != K3dComConstants.LegacyLayoutIndex)
        {
            throw Invalid(nameof(options.LayoutIndex));
        }

        string? sceneDirectory = null;
        if (options.Mode is PlayoutMode.Test or PlayoutMode.Live)
        {
            sceneDirectory = ValidateSceneDirectory(options.SceneDirectory);
        }

        var operatorBackgroundDirectory =
            PlayoutSceneCompositionFactory.TryGetConfiguredBackgroundDirectory(
                options,
                out var configuredBackgroundDirectory)
                ? configuredBackgroundDirectory
                : null;

        RequireRange(options.QueueCapacity, 1, 4_096, nameof(options.QueueCapacity));
        RequireRange(
            options.ConnectTimeoutMilliseconds,
            100,
            300_000,
            nameof(options.ConnectTimeoutMilliseconds));
        RequireRange(
            options.OperationTimeoutMilliseconds,
            100,
            300_000,
            nameof(options.OperationTimeoutMilliseconds));
        RequireRange(
            options.DisconnectTimeoutMilliseconds,
            100,
            300_000,
            nameof(options.DisconnectTimeoutMilliseconds));
        RequireRange(
            options.ProcessPollIntervalMilliseconds,
            100,
            60_000,
            nameof(options.ProcessPollIntervalMilliseconds));
        RequireRange(
            options.ReconnectDelayMilliseconds,
            0,
            300_000,
            nameof(options.ReconnectDelayMilliseconds));
        RequireRange(
            options.MaximumReconnectAttempts,
            0,
            1_000,
            nameof(options.MaximumReconnectAttempts));
        if (options.MaximumAutomaticRefreshesPerTakeIn is { } maximumAutomaticRefreshes)
        {
            RequireRange(
                maximumAutomaticRefreshes,
                0,
                1_000_000,
                nameof(options.MaximumAutomaticRefreshesPerTakeIn));
        }

        Regex? titleRegex = null;
        var allowlist = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var item in options.TestSceneAllowlist ?? [])
        {
            var value = item?.Trim();
            if (!IsSafeSceneCode(value))
            {
                throw Invalid(nameof(options.TestSceneAllowlist));
            }

            allowlist.Add(value!);
        }

        if (options.Mode is PlayoutMode.Test or PlayoutMode.Live && allowlist.Count == 0)
        {
            throw Invalid(nameof(options.TestSceneAllowlist));
        }

        if (options.DevelopmentLiveBootstrapApplied)
        {
            if (options.Mode != PlayoutMode.Live)
            {
                throw Invalid(nameof(options.Mode));
            }

            if (!IsLiteralLoopback(host))
            {
                throw Invalid(nameof(options.Host));
            }

            string? expectedSceneDirectory;
            try
            {
                expectedSceneDirectory = string.IsNullOrWhiteSpace(
                    options.DevelopmentLiveExecutableSceneDirectory)
                    ? null
                    : Path.TrimEndingDirectorySeparator(Path.GetFullPath(
                        options.DevelopmentLiveExecutableSceneDirectory));
            }
            catch (Exception exception) when (
                exception is ArgumentException or IOException or NotSupportedException or
                    System.Security.SecurityException)
            {
                throw Invalid(nameof(options.SceneDirectory));
            }

            if (expectedSceneDirectory is null ||
                !string.Equals(
                    sceneDirectory,
                    expectedSceneDirectory,
                    StringComparison.OrdinalIgnoreCase) ||
                options.LegacySceneFadeDuration != 6 ||
                options.LegacySceneBackgroundKind != LegacySceneBackgroundKind.None ||
                !string.IsNullOrWhiteSpace(options.LegacySceneBackgroundAssetPath) ||
                !string.IsNullOrWhiteSpace(options.LegacyBackgroundDirectory) ||
                options.ReconnectEnabled ||
                options.MaximumReconnectAttempts != 0 ||
                options.MaximumAutomaticRefreshesPerTakeIn != 0)
            {
                throw new PlayoutConfigurationException(
                    "Development Live configuration no longer matches its protected startup contract.");
            }
        }

        if (options.Mode == PlayoutMode.Test)
        {
            if (!IsLiteralLoopback(host))
            {
                throw Invalid(nameof(options.Host));
            }

            if (options.OutputChannel is null)
            {
                throw Invalid(nameof(options.OutputChannel));
            }

            var pattern = RequiredText(
                options.TestProcessWindowTitlePattern,
                nameof(options.TestProcessWindowTitlePattern));
            try
            {
                titleRegex = new Regex(
                    pattern,
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant |
                    RegexOptions.NonBacktracking,
                    TimeSpan.FromMilliseconds(250));
            }
            catch (ArgumentException exception)
            {
                throw new PlayoutConfigurationException(
                    "테스트 Tornado 창 제목 설정이 올바르지 않습니다.", exception);
            }

            if (!pattern.Contains("TEST", StringComparison.OrdinalIgnoreCase) ||
                CanMatchProgramTitle(titleRegex))
            {
                throw new PlayoutConfigurationException(
                    "테스트 창 제목 규칙은 명시적인 TEST 표식이 필요하며 PROGRAM 또는 비식별 출력을 가리킬 수 없습니다.");
            }
        }

        return new ValidatedPlayoutOptions(
            options.Mode,
            host,
            options.Port,
            options.TcpMode,
            options.ClientPort,
            options.OutputChannel,
            options.LayoutIndex,
            sceneDirectory,
            operatorBackgroundDirectory,
            titleRegex,
            allowlist,
            options.TrustedLiveOutputEnabled,
            options.QueueCapacity,
            TimeSpan.FromMilliseconds(options.ConnectTimeoutMilliseconds),
            TimeSpan.FromMilliseconds(options.OperationTimeoutMilliseconds),
            TimeSpan.FromMilliseconds(options.DisconnectTimeoutMilliseconds),
            TimeSpan.FromMilliseconds(options.ProcessPollIntervalMilliseconds),
            TimeSpan.FromMilliseconds(options.ReconnectDelayMilliseconds),
            options.MaximumReconnectAttempts,
            options.ReconnectEnabled);
    }

    public bool IsTestSceneAllowed(PlayoutCue cue)
        => IsOutputSceneAllowed(cue);

    /// <summary>
    /// Closed scene allowlist applied to every real Test or Live mutation command.
    /// The legacy property name is retained for local-config compatibility.
    /// </summary>
    public bool IsOutputSceneAllowed(PlayoutCue cue)
    {
        var sceneName = cue.SceneName?.Trim();
        return !string.IsNullOrEmpty(sceneName) && TestSceneAllowlist.Contains(sceneName);
    }

    public PlayoutCue ResolveCue(PlayoutCue cue)
    {
        ArgumentNullException.ThrowIfNull(cue);
        var sceneName = cue.SceneName?.Trim();
        if (!IsSafeSceneCode(sceneName))
        {
            throw new PlayoutRequestException("장면 이름이 올바르지 않습니다.");
        }

        if (cue.FadeDuration is < 0 or > 19)
        {
            throw new PlayoutRequestException("장면 전환 시간이 올바르지 않습니다.");
        }

        foreach (var field in cue.Fields ?? [])
        {
            if (string.IsNullOrWhiteSpace(field.ObjectName) ||
                field.ObjectName.Length > 128 ||
                field.ObjectName.Any(char.IsControl) ||
                (field.Value?.Length ?? 0) > 16_384)
            {
                throw new PlayoutRequestException("장면 필드 값이 올바르지 않습니다.");
            }
        }

        var mutations = ResolveMutations(cue.Mutations);

        var relativePath = cue.SceneFile?.Trim();
        if (string.IsNullOrEmpty(relativePath) || Path.IsPathRooted(relativePath))
        {
            throw new PlayoutRequestException("장면 파일 이름이 올바르지 않습니다.");
        }

        if (!string.Equals(relativePath, Path.GetFileName(relativePath), StringComparison.Ordinal) ||
            relativePath.Contains("..", StringComparison.Ordinal))
        {
            throw new PlayoutRequestException("장면 파일 이름이 올바르지 않습니다.");
        }

        if (!string.Equals(Path.GetExtension(relativePath), ".t2s", StringComparison.OrdinalIgnoreCase))
        {
            throw new PlayoutRequestException("장면 파일 형식이 올바르지 않습니다.");
        }

        var fileSceneName = Path.GetFileNameWithoutExtension(relativePath);
        if (!string.Equals(fileSceneName, sceneName, StringComparison.OrdinalIgnoreCase))
        {
            throw new PlayoutRequestException("장면 이름과 장면 파일이 일치하지 않습니다.");
        }

        if (SceneDirectory is null)
        {
            throw new PlayoutRequestException("장면 폴더가 설정되지 않았습니다.");
        }

        var candidate = Path.GetFullPath(Path.Combine(SceneDirectory, relativePath));
        var rootPrefix = SceneDirectory.EndsWith(Path.DirectorySeparatorChar)
            ? SceneDirectory
            : SceneDirectory + Path.DirectorySeparatorChar;
        if (!candidate.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase) ||
            !File.Exists(candidate) ||
            HasReparsePointBetween(SceneDirectory, candidate))
        {
            throw new PlayoutRequestException("허용된 장면 파일을 찾을 수 없습니다.");
        }

        return cue with
        {
            SceneFile = candidate,
            SceneName = sceneName!,
            Mutations = mutations
        };
    }

    private IReadOnlyList<PlayoutMutation>? ResolveMutations(
        IReadOnlyList<PlayoutMutation>? mutations)
    {
        if (mutations is null)
        {
            return null;
        }

        if (mutations.Count > 100_000)
        {
            throw new PlayoutRequestException("장면 변경 항목이 너무 많습니다.");
        }

        var resolved = new PlayoutMutation[mutations.Count];
        for (var index = 0; index < mutations.Count; index++)
        {
            resolved[index] = ResolveMutation(mutations[index]);
        }

        return resolved;
    }

    private PlayoutMutation ResolveMutation(PlayoutMutation? mutation)
    {
        if (mutation is null)
        {
            throw new PlayoutRequestException("장면 변경 항목이 올바르지 않습니다.");
        }

        switch (mutation)
        {
            case PlayoutSetValue value:
                ValidateObjectName(value.ObjectName);
                ValidateText(value.Value);
                return value;
            case PlayoutSetAssetValue asset:
                ValidateObjectName(asset.ObjectName);
                return asset with { AssetPath = ResolveAssetPath(asset.AssetPath) };
            case PlayoutSetVisible visible:
                ValidateObjectName(visible.ObjectName);
                return visible;
            case PlayoutSetFaceColor color:
                ValidateObjectName(color.ObjectName);
                if (!IsByte(color.Red) || !IsByte(color.Green) ||
                    !IsByte(color.Blue) || !IsByte(color.Alpha))
                {
                    throw new PlayoutRequestException("장면 색상 값이 올바르지 않습니다.");
                }

                return color;
            case PlayoutSetPosition position:
                ValidateObjectName(position.ObjectName);
                ValidatePoint(position.X, position.Y, position.Z);
                ValidateVectorComponents(position.Components);
                return position;
            case PlayoutSetPositionKey positionKey:
                ValidateObjectName(positionKey.ObjectName);
                ValidateKeyIndex(positionKey.KeyIndex);
                ValidatePoint(positionKey.X, positionKey.Y, positionKey.Z);
                ValidateVectorComponents(positionKey.Components);
                return positionKey;
            case PlayoutSetScale scale:
                ValidateObjectName(scale.ObjectName);
                ValidatePoint(scale.X, scale.Y, scale.Z);
                ValidateVectorComponents(scale.Components);
                return scale;
            case PlayoutSetCropKey crop:
                ValidateObjectName(crop.ObjectName);
                ValidateKeyIndex(crop.KeyIndex);
                ValidatePoint(crop.Left, crop.Top, crop.Right);
                if (!float.IsFinite(crop.Bottom) ||
                    !Enum.IsDefined(crop.Edges) || crop.Edges == 0)
                {
                    throw new PlayoutRequestException("장면 Crop 값이 올바르지 않습니다.");
                }

                return crop;
            case PlayoutSetCircleAngleKey angle:
                ValidateObjectName(angle.ObjectName);
                ValidateKeyIndex(angle.KeyIndex);
                if (!float.IsFinite(angle.Start) || !float.IsFinite(angle.End) ||
                    !Enum.IsDefined(angle.Components) || angle.Components == 0)
                {
                    throw new PlayoutRequestException("장면 각도 값이 올바르지 않습니다.");
                }

                return angle;
            case PlayoutSetPathPoints path:
                ValidateObjectName(path.ObjectName);
                ValidatePoints(path.Points);
                return path;
            case PlayoutSetPathShapePoints shape:
                ValidateObjectName(shape.ObjectName);
                ValidatePoints(shape.Points);
                return shape;
            case PlayoutUseBackground background:
                ValidateMutationTiming(background.Timing);
                return background;
            case PlayoutSetBackgroundTexture texture:
                ValidateMutationTiming(texture.Timing);
                ValidateAssetRoot(texture.AssetRoot);
                return texture with
                {
                    AssetPath = ResolveAssetPath(texture.AssetPath, texture.AssetRoot)
                };
            case PlayoutSetBackgroundVideo video:
                ValidateMutationTiming(video.Timing);
                ValidateAssetRoot(video.AssetRoot);
                // The legacy MainForm intentionally uses 2004 for the common studio
                // background. Keep the input bounded while retaining that vendor value.
                if (video.LoopCount is < 0 or > 10_000)
                {
                    throw new PlayoutRequestException("장면 배경 영상 반복 값이 올바르지 않습니다.");
                }

                // Installed K3D Interop declares LoopInfinite as Int32. The source
                // application uses only 0, 1 (s5006), and 10 (common background).
                if (video.LoopInfinite is not (0 or 1 or 10))
                {
                    throw new PlayoutRequestException(
                        "장면 배경 영상 무한 반복 값이 허용된 원본 값이 아닙니다.");
                }

                return video with
                {
                    AssetPath = ResolveAssetPath(video.AssetPath, video.AssetRoot)
                };
            default:
                throw new PlayoutRequestException("지원하지 않는 장면 변경 항목입니다.");
        }
    }

    private string ResolveAssetPath(string? relativePath)
        => ResolveAssetPath(relativePath, PlayoutAssetRoot.SceneDirectory);

    private string ResolveAssetPath(string? relativePath, PlayoutAssetRoot assetRoot)
    {
        relativePath = relativePath?.Trim();
        var root = assetRoot switch
        {
            PlayoutAssetRoot.SceneDirectory => SceneDirectory,
            PlayoutAssetRoot.OperatorBackgroundDirectory => OperatorBackgroundDirectory,
            _ => null
        };
        if (root is null || string.IsNullOrEmpty(relativePath) ||
            Path.IsPathRooted(relativePath) || relativePath.Contains("..", StringComparison.Ordinal) ||
            relativePath.Any(char.IsControl))
        {
            throw new PlayoutRequestException("장면 자산 경로가 올바르지 않습니다.");
        }

        var extension = Path.GetExtension(relativePath);
        string[] allowedExtensions =
        [
            ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds",
            ".vrv", ".avi", ".mp4", ".mov"
        ];
        if (!allowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
        {
            throw new PlayoutRequestException("지원하지 않는 장면 자산 형식입니다.");
        }

        if (assetRoot == PlayoutAssetRoot.OperatorBackgroundDirectory)
        {
            try
            {
                return TrustedPlayoutAssetPath.ResolveRelativeFile(
                    root,
                    relativePath,
                    new HashSet<string>(allowedExtensions, StringComparer.OrdinalIgnoreCase));
            }
            catch (TrustedPlayoutAssetException)
            {
                throw new PlayoutRequestException(
                    "신뢰 배경 파일을 확인할 수 없습니다.");
            }
        }

        var candidate = Path.GetFullPath(Path.Combine(root, relativePath));
        var rootPrefix = root.EndsWith(Path.DirectorySeparatorChar)
            ? root
            : root + Path.DirectorySeparatorChar;
        if (!candidate.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase) ||
            !File.Exists(candidate) || HasReparsePointBetween(root, candidate))
        {
            throw new PlayoutRequestException("허용된 장면 자산을 찾을 수 없습니다.");
        }

        return candidate;
    }

    private static void ValidateObjectName(string? objectName)
    {
        if (string.IsNullOrWhiteSpace(objectName) || objectName.Length > 128 ||
            objectName.Any(char.IsControl))
        {
            throw new PlayoutRequestException("장면 오브젝트 이름이 올바르지 않습니다.");
        }
    }

    private static void ValidateText(string? value)
    {
        if (value is null || value.Length > 16_384 || value.Any(character =>
                character is '\0' or '\u0001' or '\u0002' or '\u0003'))
        {
            throw new PlayoutRequestException("장면 텍스트 값이 올바르지 않습니다.");
        }
    }

    private static void ValidatePoint(float x, float y, float z)
    {
        if (!float.IsFinite(x) || !float.IsFinite(y) || !float.IsFinite(z))
        {
            throw new PlayoutRequestException("장면 좌표 값이 올바르지 않습니다.");
        }
    }

    private static void ValidatePoints(IReadOnlyList<PlayoutPoint>? points)
    {
        if (points is null || points.Count > 10_000)
        {
            throw new PlayoutRequestException("장면 그래프 점 목록이 올바르지 않습니다.");
        }

        foreach (var point in points)
        {
            ValidatePoint(point.X, point.Y, point.Z);
        }
    }

    private static void ValidateVectorComponents(PlayoutVectorComponents components)
    {
        if (!Enum.IsDefined(components) || components == 0)
        {
            throw new PlayoutRequestException("장면 벡터 축 값이 올바르지 않습니다.");
        }
    }

    private static void ValidateKeyIndex(int keyIndex)
    {
        if (keyIndex is < 0 or > 10_000)
        {
            throw new PlayoutRequestException("장면 키 인덱스가 올바르지 않습니다.");
        }
    }

    private static void ValidateMutationTiming(PlayoutMutationTiming timing)
    {
        if (!Enum.IsDefined(timing))
        {
            throw new PlayoutRequestException("장면 변경 시점이 올바르지 않습니다.");
        }
    }

    private static void ValidateAssetRoot(PlayoutAssetRoot assetRoot)
    {
        if (!Enum.IsDefined(assetRoot))
        {
            throw new PlayoutRequestException("배경 자산 루트가 올바르지 않습니다.");
        }
    }

    private static bool IsByte(int value) => value is >= byte.MinValue and <= byte.MaxValue;

    private static bool CanMatchProgramTitle(Regex regex)
    {
        string[] protectedTitles =
        [
            string.Empty,
            " ",
            "Tornado2",
            "PGM",
            "PROGRAM",
            "Tornado2 PGM",
            "Tornado2 PROGRAM",
            "PGM OUTPUT",
            "PROGRAM OUTPUT"
        ];
        return protectedTitles.Any(regex.IsMatch);
    }

    private static bool ContainsPathSyntax(string value) =>
        value.IndexOfAny([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, ':']) >= 0;

    private static bool IsSafeSceneCode(string? value)
    {
        if (string.IsNullOrEmpty(value) || value.Length > 64 || ContainsPathSyntax(value))
        {
            return false;
        }

        foreach (var character in value)
        {
            if (!((character >= 'A' && character <= 'Z') ||
                  (character >= 'a' && character <= 'z') ||
                  (character >= '0' && character <= '9') ||
                  character is '_' or '-'))
            {
                return false;
            }
        }

        return true;
    }

    private static bool IsLiteralLoopback(string host) =>
        string.Equals(host, IPAddress.Loopback.ToString(), StringComparison.Ordinal) ||
        string.Equals(host, IPAddress.IPv6Loopback.ToString(), StringComparison.Ordinal);

    private static string ValidateSceneDirectory(string? value)
    {
        value = value?.Trim();
        if (string.IsNullOrEmpty(value) || !Path.IsPathFullyQualified(value))
        {
            throw Invalid(nameof(PlayoutOptions.SceneDirectory));
        }

        var fullPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(value));
        if (!Directory.Exists(fullPath) || IsReparsePoint(fullPath))
        {
            throw Invalid(nameof(PlayoutOptions.SceneDirectory));
        }

        return fullPath;
    }

    private static bool HasReparsePointBetween(string root, string file)
    {
        if (IsReparsePoint(file))
        {
            return true;
        }

        var directory = Path.GetDirectoryName(file);
        while (!string.IsNullOrEmpty(directory) &&
               !string.Equals(directory, root, StringComparison.OrdinalIgnoreCase))
        {
            if (IsReparsePoint(directory))
            {
                return true;
            }

            directory = Path.GetDirectoryName(directory);
        }

        return !string.Equals(directory, root, StringComparison.OrdinalIgnoreCase);
    }

    private static bool IsReparsePoint(string path)
    {
        try
        {
            return (File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0;
        }
        catch (IOException)
        {
            return true;
        }
        catch (UnauthorizedAccessException)
        {
            return true;
        }
    }

    private static string RequiredText(string? value, string name)
    {
        value = value?.Trim();
        if (string.IsNullOrEmpty(value) || value.Length > 512)
        {
            throw Invalid(name);
        }

        return value;
    }

    private static void RequireRange(int value, int minimum, int maximum, string name)
    {
        if (value < minimum || value > maximum)
        {
            throw Invalid(name);
        }
    }

    private static PlayoutConfigurationException Invalid(string name) =>
        new($"송출 설정 {name} 값이 올바르지 않습니다.");
}

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