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

namespace FrameProcessor.Configuration;

/// <summary>
/// Holds the current set of valid frames parsed from <c>frames.json</c> and exposes
/// lookups by <see cref="FrameName"/> and <see cref="MacAddress"/>. Enforces the
/// startup-vs-reload asymmetry from SPEC.md §6.2 and CLAUDE.md "frames.json reload
/// asymmetry": an invalid frame at startup fails fast; an invalid frame at hot-reload
/// is logged and skipped while the remaining valid frames keep serving.
/// </summary>
public sealed class FramesRegistry : IDisposable
{
    private readonly FramesOptionsValidator _validator;
    private readonly ILogger<FramesRegistry> _logger;
    private readonly IDisposable? _changeSubscription;
    private volatile FrameSet _frames;

    public FramesRegistry(
        IOptionsMonitor<FramesOptions> monitor,
        FramesOptionsValidator validator,
        ILogger<FramesRegistry> logger)
    {
        _validator = validator;
        _logger = logger;

        _frames = BuildStrict(monitor.CurrentValue);
        _changeSubscription = monitor.OnChange(OnFramesChanged);
    }

    public IReadOnlyCollection<Frame> All => (IReadOnlyCollection<Frame>)_frames.ByName.Values;

    public bool TryGetByName(FrameName name, out Frame frame)
        => _frames.ByName.TryGetValue(name, out frame!);

    public bool TryGetByMac(MacAddress mac, out Frame frame)
        => _frames.ByMac.TryGetValue(mac, out frame!);

    public void Dispose() => _changeSubscription?.Dispose();

    private FrameSet BuildStrict(FramesOptions options)
    {
        var result = _validator.Validate(null, options);
        if (result.Failed)
        {
            throw new OptionsValidationException(
                nameof(FramesOptions),
                typeof(FramesOptions),
                result.Failures ?? Array.Empty<string>());
        }

        var byName = new Dictionary<FrameName, Frame>();
        var byMac = new Dictionary<MacAddress, Frame>();
        foreach (var frameOptions in options.Frames)
        {
            var frame = ToFrame(frameOptions);
            byName[frame.Name] = frame;
            byMac[frame.Mac] = frame;
        }

        return new FrameSet(byName, byMac);
    }

    private void OnFramesChanged(FramesOptions options)
    {
        var byName = new Dictionary<FrameName, Frame>();
        var byMac = new Dictionary<MacAddress, Frame>();

        for (var i = 0; i < options.Frames.Count; i++)
        {
            var frameOptions = options.Frames[i];
            var single = new FramesOptions { Frames = { frameOptions } };
            var result = _validator.Validate(null, single);
            if (result.Failed)
            {
                _logger.LogWarning(
                    "Skipping invalid frame at Frames[{Index}] on reload: {Errors}",
                    i,
                    string.Join("; ", result.Failures ?? Array.Empty<string>()));
                continue;
            }

            var frame = ToFrame(frameOptions);

            if (byName.ContainsKey(frame.Name))
            {
                _logger.LogWarning(
                    "Skipping frame at Frames[{Index}] on reload: duplicate name '{Name}'.",
                    i,
                    frame.Name);
                continue;
            }

            if (byMac.ContainsKey(frame.Mac))
            {
                _logger.LogWarning(
                    "Skipping frame at Frames[{Index}] on reload: duplicate MAC '{Mac}'.",
                    i,
                    frame.Mac);
                continue;
            }

            byName[frame.Name] = frame;
            byMac[frame.Mac] = frame;
        }

        _frames = new FrameSet(byName, byMac);
        _logger.LogInformation("Reloaded frames; {Count} active.", byName.Count);
    }

    private static Frame ToFrame(FrameOptions opts)
    {
        var name = FrameName.Parse(opts.Name!);
        var mac = MacAddress.Parse(opts.Mac!);
        var resolution = new Resolution(opts.Resolution!.Width, opts.Resolution.Height);
        var orientation = opts.Orientation switch
        {
            "landscape" => Orientation.Landscape,
            "portrait" => Orientation.Portrait,
            _ => throw new InvalidOperationException(
                $"Validator allowed unexpected orientation '{opts.Orientation}'."),
        };
        var dithering = opts.Dithering switch
        {
            "floyd-steinberg" => DitherAlgorithm.FloydSteinberg,
            "atkinson" => DitherAlgorithm.Atkinson,
            "stucki" => DitherAlgorithm.Stucki,
            "jarvis" => DitherAlgorithm.Jarvis,
            _ => throw new InvalidOperationException(
                $"Validator allowed unexpected dithering '{opts.Dithering}'."),
        };
        var palette = opts.Palette
            .Select(p => new PaletteEntry(
                p.Name!,
                Color.ParseHex(p.Color!),
                Color.ParseHex(p.DeviceColor!)))
            .ToList();

        return new Frame(name, mac, resolution, orientation, dithering, palette);
    }

    private sealed record FrameSet(
        IReadOnlyDictionary<FrameName, Frame> ByName,
        IReadOnlyDictionary<MacAddress, Frame> ByMac);
}
