using System.Collections.Frozen;
using Mbproxy.Options;

namespace Mbproxy.Bcd;

/// <summary>
/// Builds an immutable <see cref="BcdTagMap"/> from global options and optional per-PLC overrides.
///
/// Resolution algorithm (per docs/Features/BcdRewriting.md):
///   1. Start with the global tag list.
///   2. Remove any address present in perPlc.Remove.
///   3. Merge in perPlc.Add entries — if an address exists in the working set the Add entry wins
///      (this is how a per-PLC width override is expressed).
///
/// Validation:
///   - Duplicate address in the resolved list → BcdError(DuplicateAddress).
///   - 32-bit high register (Address+1) collides with any other entry → BcdError(OverlappingHighRegister).
///   - Width not 16 or 32 → BcdError(InvalidWidth).
///   - Remove address not found in global → BcdWarning (not an error).
/// </summary>
public static class BcdTagMapBuilder
{
    /// <summary>
    /// Resolves the effective BCD tag list for one PLC and validates it.
    /// </summary>
    /// <param name="global">The global BCD tag list from <c>appsettings.json</c>.</param>
    /// <param name="perPlc">Optional per-PLC overrides (Add + Remove). May be null.</param>
    /// <returns>
    /// A <see cref="ValidationResult"/> whose <see cref="ValidationResult.Map"/> contains
    /// only the entries that passed validation. Callers should treat non-empty
    /// <see cref="ValidationResult.Errors"/> as a fatal configuration problem.
    /// </returns>
    public static ValidationResult Build(BcdTagListOptions global, PlcBcdOverrides? perPlc)
        => Build(global, perPlc, perPlcDefaultCacheTtlMs: 0);

    /// <summary>
    /// Overload that resolves the effective BCD tag list for one PLC and validates it,
    /// additionally folding the per-PLC <paramref name="perPlcDefaultCacheTtlMs"/> into
    /// any tag whose explicit <see cref="BcdTagOptions.CacheTtlMs"/> is null.
    ///
    /// <para>Resolution order per tag:</para>
    /// <list type="number">
    ///   <item><description>Explicit per-tag <c>CacheTtlMs</c> if set (including explicit 0).</description></item>
    ///   <item><description>Otherwise the per-PLC default.</description></item>
    ///   <item><description>Otherwise 0 (uncached).</description></item>
    /// </list>
    /// </summary>
    public static ValidationResult Build(
        BcdTagListOptions global,
        PlcBcdOverrides? perPlc,
        int perPlcDefaultCacheTtlMs)
    {
        var errors = new List<BcdError>();
        var warnings = new List<BcdWarning>();

        // Duplicate-address detection happens BEFORE the working dictionary collapses
        // keys. Iterating each list independently catches duplicates that would otherwise
        // be silently last-write-wins'd by the dictionary indexer. Cross-list collisions
        // (same address in BOTH Global and Add) are the documented width-override pattern
        // and must NOT be flagged — only intra-list duplicates fail.
        static void DetectIntraListDuplicates(
            IEnumerable<BcdTagOptions> source, string sourceName, List<BcdError> errors)
        {
            var seen = new HashSet<ushort>();
            foreach (var tag in source)
            {
                if (!seen.Add(tag.Address))
                {
                    errors.Add(new BcdError(BcdValidationError.DuplicateAddress,
                        $"Address {tag.Address} appears more than once in {sourceName}.",
                        tag.Address));
                }
            }
        }

        DetectIntraListDuplicates(global.Global, "Global", errors);
        if (perPlc?.Add is { } addListForDup)
            DetectIntraListDuplicates(addListForDup, "PerPlc.Add", errors);

        // ── Step 1: collect the working set keyed by address ─────────────────
        // Dictionary preserves last-write-wins semantics for the Add override.
        var working = new Dictionary<ushort, BcdTagOptions>(global.Global.Count);

        foreach (var tag in global.Global)
            working[tag.Address] = tag;

        // ── Step 2: apply Remove ─────────────────────────────────────────────
        if (perPlc?.Remove is { } removeList)
        {
            foreach (var addr in removeList)
            {
                if (!working.Remove(addr))
                    warnings.Add(new BcdWarning(
                        $"Remove entry for address {addr} does not match any global tag; " +
                        "the entry is probably stale.", addr));
            }
        }

        // ── Step 3: apply Add (override wins) ────────────────────────────────
        if (perPlc?.Add is { } addList)
        {
            foreach (var tag in addList)
                working[tag.Address] = tag;
        }

        // ── Step 4: validate the resolved list ───────────────────────────────
        // We build a validated-entries list; only clean entries go into the map.
        var validated = new Dictionary<ushort, BcdTag>(working.Count);

        foreach (var (addr, opt) in working)
        {
            // Width check first (defensive — IValidateOptions should have caught this already).
            if (opt.Width != 16 && opt.Width != 32)
            {
                errors.Add(new BcdError(BcdValidationError.InvalidWidth,
                    $"Address {addr}: Width {opt.Width} is not 16 or 32.", addr));
                continue;
            }

            // Resolve the effective per-tag cache TTL:
            //   explicit per-tag (including 0) wins; otherwise fall back to per-PLC default.
            int resolvedTtl = opt.CacheTtlMs ?? perPlcDefaultCacheTtlMs;
            if (resolvedTtl < 0) resolvedTtl = 0;

            validated[addr] = BcdTag.Create(addr, opt.Width, resolvedTtl);
        }

        // High-register collision check (only meaningful for 32-bit entries).
        // Dedupe symmetric reports. Two 32-bit tags whose pairs collide (e.g. (100,W=32)
        // and (101,W=32)) would otherwise produce two BcdErrors — one from each
        // direction. Track reported (low,high) pairs so each collision logs once.
        var reportedCollisions = new HashSet<(ushort, ushort)>();
        foreach (var tag in validated.Values)
        {
            if (!tag.IsThirtyTwoBit)
                continue;

            ushort highReg = tag.HighRegister;
            if (validated.TryGetValue(highReg, out var collision))
            {
                // Canonicalise the pair so (a,b) and (b,a) collapse.
                var pair = tag.Address < collision.Address
                    ? (tag.Address, collision.Address)
                    : (collision.Address, tag.Address);
                if (!reportedCollisions.Add(pair))
                    continue;

                errors.Add(new BcdError(BcdValidationError.OverlappingHighRegister,
                    $"32-bit BCD tag at address {tag.Address} has its high register " +
                    $"({highReg}) colliding with the entry at address {collision.Address}.",
                    tag.Address));
            }
        }

        // ── Step 5: build the frozen map from entries that have no errors ─────
        // Entries implicated in an OverlappingHighRegister error are still included
        // in the map so that the caller can see all context; the error list tells them
        // the config is invalid and must be corrected before the service is safe to run.
        // (If callers want to exclude bad entries they should check Errors.Count > 0
        //  and refuse to start the listener for that PLC.)
        var frozen = validated.ToFrozenDictionary();
        var map = frozen.Count > 0 ? new BcdTagMap(frozen) : BcdTagMap.Empty;

        return new ValidationResult(map, errors, warnings);
    }
}
