using FrameProcessor.Domain;
using Microsoft.Extensions.Options;
using SixLabors.ImageSharp;

namespace FrameProcessor.Configuration;

/// <summary>
/// Bound from <c>frames.json</c> (registered as an additional config source with
/// <c>reloadOnChange: true</c>). Holds the raw, un-typed frame entries; parsing into
/// <see cref="FrameName"/>, <see cref="MacAddress"/>, etc. happens downstream after
/// <see cref="FramesOptionsValidator"/> confirms the values are well-formed.
/// See SPEC.md §6.2.
/// </summary>
public sealed class FramesOptions
{
    public List<FrameOptions> Frames { get; set; } = new();
}

public sealed class FrameOptions
{
    public string? Name { get; set; }

    public string? Mac { get; set; }

    public ResolutionOptions? Resolution { get; set; }

    public string? Orientation { get; set; }

    public string? Dithering { get; set; }

    public List<PaletteEntryOptions> Palette { get; set; } = new();
}

public sealed class ResolutionOptions
{
    public int Width { get; set; }

    public int Height { get; set; }
}

public sealed class PaletteEntryOptions
{
    public string? Name { get; set; }

    public string? Color { get; set; }

    public string? DeviceColor { get; set; }
}

/// <summary>
/// Enforces the field-level rules from SPEC.md §6.2: name is URL-safe, MAC parseable,
/// resolution positive, orientation and dithering keys recognized, palette has at
/// least two entries with parseable hex colors, and names + MACs are unique across
/// frames.
/// </summary>
public sealed class FramesOptionsValidator : IValidateOptions<FramesOptions>
{
    public ValidateOptionsResult Validate(string? name, FramesOptions options)
    {
        var errors = new List<string>();

        for (var i = 0; i < options.Frames.Count; i++)
        {
            ValidateFrame(options.Frames[i], $"Frames[{i}]", errors);
        }

        ValidateUniqueness(options.Frames, errors);

        return errors.Count == 0
            ? ValidateOptionsResult.Success
            : ValidateOptionsResult.Fail(errors);
    }

    private static void ValidateFrame(FrameOptions frame, string prefix, List<string> errors)
    {
        if (string.IsNullOrWhiteSpace(frame.Name))
        {
            errors.Add($"{prefix}.Name is required.");
        }
        else if (!FrameName.TryParse(frame.Name, out _))
        {
            errors.Add($"{prefix}.Name '{frame.Name}' is not a valid frame name (RFC 3986 unreserved characters only).");
        }

        if (string.IsNullOrWhiteSpace(frame.Mac))
        {
            errors.Add($"{prefix}.Mac is required.");
        }
        else if (!MacAddress.TryParse(frame.Mac, out _))
        {
            errors.Add($"{prefix}.Mac '{frame.Mac}' is not a valid MAC address.");
        }

        if (frame.Resolution is null)
        {
            errors.Add($"{prefix}.Resolution is required.");
        }
        else
        {
            if (frame.Resolution.Width <= 0)
            {
                errors.Add($"{prefix}.Resolution.Width must be positive (got {frame.Resolution.Width}).");
            }

            if (frame.Resolution.Height <= 0)
            {
                errors.Add($"{prefix}.Resolution.Height must be positive (got {frame.Resolution.Height}).");
            }
        }

        if (!IsKnownOrientation(frame.Orientation))
        {
            errors.Add($"{prefix}.Orientation '{frame.Orientation}' is not valid. Expected 'landscape' or 'portrait'.");
        }

        if (!IsKnownDithering(frame.Dithering))
        {
            errors.Add($"{prefix}.Dithering '{frame.Dithering}' is not valid. Expected 'floyd-steinberg', 'atkinson', 'stucki', or 'jarvis'.");
        }

        if (frame.Palette.Count < 2)
        {
            errors.Add($"{prefix}.Palette must contain at least 2 entries (found {frame.Palette.Count}).");
        }

        for (var j = 0; j < frame.Palette.Count; j++)
        {
            ValidatePaletteEntry(frame.Palette[j], $"{prefix}.Palette[{j}]", errors);
        }
    }

    private static void ValidatePaletteEntry(PaletteEntryOptions entry, string prefix, List<string> errors)
    {
        if (string.IsNullOrWhiteSpace(entry.Name))
        {
            errors.Add($"{prefix}.Name is required.");
        }

        if (string.IsNullOrWhiteSpace(entry.Color) || !Color.TryParseHex(entry.Color, out _))
        {
            errors.Add($"{prefix}.Color '{entry.Color}' is not a valid hex color.");
        }

        if (string.IsNullOrWhiteSpace(entry.DeviceColor) || !Color.TryParseHex(entry.DeviceColor, out _))
        {
            errors.Add($"{prefix}.DeviceColor '{entry.DeviceColor}' is not a valid hex color.");
        }
    }

    private static void ValidateUniqueness(List<FrameOptions> frames, List<string> errors)
    {
        var seenNames = new HashSet<string>(StringComparer.Ordinal);
        var seenMacs = new HashSet<string>(StringComparer.Ordinal);

        for (var i = 0; i < frames.Count; i++)
        {
            var frame = frames[i];

            if (!string.IsNullOrWhiteSpace(frame.Name) && !seenNames.Add(frame.Name))
            {
                errors.Add($"Frames[{i}].Name '{frame.Name}' is a duplicate.");
            }

            if (!string.IsNullOrWhiteSpace(frame.Mac) && MacAddress.TryParse(frame.Mac, out var mac))
            {
                var canonical = mac.ToString();
                if (!seenMacs.Add(canonical))
                {
                    errors.Add($"Frames[{i}].Mac '{frame.Mac}' (canonical '{canonical}') is a duplicate.");
                }
            }
        }
    }

    private static bool IsKnownOrientation(string? value) => value switch
    {
        "landscape" or "portrait" => true,
        _ => false,
    };

    private static bool IsKnownDithering(string? value) => value switch
    {
        "floyd-steinberg" or "atkinson" or "stucki" or "jarvis" => true,
        _ => false,
    };
}
