using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;

/// <summary>
///     Lists the configured tag + virtual-tag path strings a virtual-tag script author can pass to
///     <c>ctx.GetTag("…")</c> / <c>ctx.SetVirtualTag("…")</c>. A later Monaco task uses this to
///     autocomplete those string literals.
/// </summary>
public interface IScriptTagCatalog
{
    /// <summary>Distinct configured tag + virtual-tag paths (the strings a script passes to
    /// ctx.GetTag/SetVirtualTag), optionally filtered by a literal prefix.</summary>
    /// <param name="filter">Case-insensitive StartsWith prefix; <c>null</c>/empty returns all (bounded).</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>The matching configured tag + virtual-tag paths.</returns>
    Task<IReadOnlyList<string>> GetPathsAsync(string? filter, CancellationToken ct);

    /// <summary>Exact (case-sensitive, Ordinal) lookup of a resolvable tag path; null when not a known configured path.</summary>
    /// <param name="path">The exact tag/virtual-tag path to look up.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>The resolved tag info, or <see langword="null"/> when <paramref name="path"/> is not a known configured path.</returns>
    Task<ScriptTagInfo?> GetTagInfoAsync(string path, CancellationToken ct);

    /// <summary>v3: the owning equipment's <c>UnsTagReference</c> effective names (its
    /// <c>DisplayNameOverride</c> else the backing raw tag's <c>Name</c>) — the resolvable set for
    /// <c>{{equip}}/&lt;RefName&gt;</c> completion + diagnostics, matching what the compose seams substitute
    /// and the deploy gate enforces. Optionally prefix-filtered. Empty when the equipment id is blank or has
    /// no references.</summary>
    /// <param name="equipmentId">The equipment whose references to list; blank returns empty.</param>
    /// <param name="filter">Case-insensitive StartsWith prefix over the reference name; null/empty = all (bounded).</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>Distinct reference effective names.</returns>
    Task<IReadOnlyList<string>> GetEquipmentReferenceNamesAsync(string equipmentId, string? filter, CancellationToken ct);
}

/// <summary>Resolved info for one configured tag/virtual-tag path (for hover).</summary>
public sealed record ScriptTagInfo(string Path, string Kind, string DataType, string? DriverInstanceId);

/// <summary>
///     Default <see cref="IScriptTagCatalog"/>. Returns ONLY the resolvable keys a script may pass to
///     <c>ctx.GetTag</c> / <c>ctx.SetVirtualTag</c> — the driver <c>FullName</c> for equipment tags,
///     the MXAccess dot-ref for FolderPath-scoped tags (<c>EquipmentId == null</c>), and the leaf
///     <c>Name</c> (best-effort) for virtual tags.
/// </summary>
/// <remarks>
///     <para>
///         <b>Fidelity over breadth.</b> Verified: the live runtime resolves a <c>ctx.GetTag("X")</c>
///         literal against the driver <c>FullName</c> — the resolution chain is
///         <c>AddressSpaceComposer</c> (via <c>EquipmentScriptPaths.ExtractDependencyRefs</c>) harvesting the <c>ctx.GetTag("…")</c> literals
///         into <c>EquipmentVirtualTagPlan.DependencyRefs</c>
///         (<c>src/Server/…OpcUaServer/AddressSpaceComposer.cs</c>); those become
///         <c>VirtualTagActor._dependencyRefs</c>, registered with the
///         <c>DependencyMuxActor</c>, whose <c>_byRef</c> map is keyed by
///         <c>DriverInstanceActor.AttributeValuePublished.FullReference</c>
///         (<c>src/Server/…Runtime/VirtualTags/DependencyMuxActor.cs:97</c>) — and that
///         <c>FullReference</c> is the <c>FullName</c> field extracted from <c>Tag.TagConfig</c>
///         (see <c>Commons.Types.TagConfigIntent.Parse</c>, the shared byte-parity FullName authority).
///         The UNS-path engine (<c>Core.VirtualTags.VirtualTagEngine</c>, keyed by a slash-joined
///         <c>Enterprise/Site/Area/Line/Equipment/TagName</c>) is dormant — it is NOT wired into the
///         host — so UNS browse paths never resolve at runtime and are intentionally NOT suggested.
///     </para>
///     <para>
///         The per-category resolvable key:
///         <list type="bullet">
///           <item>Equipment driver tag (<c>EquipmentId != null</c>) → the driver <c>FullName</c>
///           extracted from <c>Tag.TagConfig</c> (the verified <c>DependencyMux</c> key).
///           GalaxyMxGateway is a standard Equipment-kind driver, so Galaxy points resolve
///           by this same <c>FullName</c> key.</item>
///           <item>VirtualTag → its leaf <c>Name</c> only, as a BEST-EFFORT key (the live resolution
///           of virtual-tag cascade/write targets is unconfirmed).</item>
///         </list>
///     </para>
///     <para>
///         Follow-up: surface the UNS browse path as a completion <i>detail</i> (a non-inserted hint
///         shown alongside the resolvable key) for discoverability, rather than as an inserted value.
///     </para>
///     <para>
///         Each call creates and disposes its own context via the pooled factory — the same pattern
///         <c>UnsTreeService</c> uses — so the service is safe to register Scoped per Blazor circuit.
///     </para>
/// </remarks>
public sealed class ScriptTagCatalog(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory) : IScriptTagCatalog
{
    /// <summary>Upper bound on returned suggestions — keeps the completion list responsive on large fleets.</summary>
    private const int MaxResults = 200;

    /// <inheritdoc />
    public async Task<IReadOnlyList<string>> GetPathsAsync(string? filter, CancellationToken ct)
    {
        var entries = await BuildEntriesAsync(ct);

        var paths = new HashSet<string>(entries.Select(e => e.Path), StringComparer.OrdinalIgnoreCase);

        IEnumerable<string> query = paths;
        if (!string.IsNullOrWhiteSpace(filter))
        {
            query = query.Where(p => p.StartsWith(filter, StringComparison.OrdinalIgnoreCase));
        }

        return query
            .OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
            .Take(MaxResults)
            .ToList();
    }

    /// <inheritdoc />
    public async Task<ScriptTagInfo?> GetTagInfoAsync(string path, CancellationToken ct)
    {
        var entries = await BuildEntriesAsync(ct);

        // Exact, case-SENSITIVE (Ordinal) match: the live runtime resolves a ctx.GetTag("X") literal
        // via DependencyMuxActor keyed with StringComparer.Ordinal, so a case-mismatched path would
        // not resolve at runtime — hover must report it as "not a known configured tag path".
        return entries.FirstOrDefault(e => string.Equals(e.Path, path, StringComparison.Ordinal));
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<string>> GetEquipmentReferenceNamesAsync(string equipmentId, string? filter, CancellationToken ct)
    {
        if (string.IsNullOrEmpty(equipmentId)) return Array.Empty<string>();
        await using var db = await dbFactory.CreateDbContextAsync(ct);

        var refs = await db.UnsTagReferences.AsNoTracking()
            .Where(r => r.EquipmentId == equipmentId)
            .Select(r => new { r.TagId, r.DisplayNameOverride })
            .ToListAsync(ct);
        if (refs.Count == 0) return Array.Empty<string>();

        var tagIds = refs.Select(r => r.TagId).Distinct().ToList();
        var tagNameById = await db.Tags.AsNoTracking()
            .Where(t => tagIds.Contains(t.TagId))
            .Select(t => new { t.TagId, t.Name })
            .ToDictionaryAsync(t => t.TagId, t => t.Name, ct);

        // Effective name = DisplayNameOverride else the backing raw tag's Name (ordinal set).
        var names = new HashSet<string>(StringComparer.Ordinal);
        foreach (var r in refs)
        {
            var eff = r.DisplayNameOverride ?? (tagNameById.TryGetValue(r.TagId, out var n) ? n : null);
            if (!string.IsNullOrEmpty(eff)) names.Add(eff!);
        }

        IEnumerable<string> q = names;
        if (!string.IsNullOrWhiteSpace(filter))
            q = q.Where(n => n.StartsWith(filter, StringComparison.OrdinalIgnoreCase));
        return q.OrderBy(n => n, StringComparer.OrdinalIgnoreCase).Take(MaxResults).ToList();
    }

    /// <summary>
    ///     Loads Tags + VirtualTags (untracked) and projects one <see cref="ScriptTagInfo"/> per row —
    ///     the SINGLE source of the per-category resolvable-key projection shared by
    ///     <see cref="GetPathsAsync"/> (distinct paths, prefix-filtered) and
    ///     <see cref="GetTagInfoAsync"/> (exact Ordinal lookup).
    /// </summary>
    private async Task<IReadOnlyList<ScriptTagInfo>> BuildEntriesAsync(CancellationToken ct)
    {
        await using var db = await dbFactory.CreateDbContextAsync(ct);

        // v3: Tag is Raw-only — EquipmentId / FolderPath / DriverInstanceId were retired (the
        // equipment↔Tag + driver bindings are gone). The resolvable key is the driver FullName carried
        // in TagConfig; project only the surviving columns. (The Raw/UNS tag catalog is rebuilt in
        // Batch 2/3.)
        var tagRows = await db.Tags
            .AsNoTracking()
            .Select(t => new { t.Name, t.DataType, t.TagConfig })
            .ToListAsync(ct);

        var vtagRows = await db.VirtualTags
            .AsNoTracking()
            .Select(v => new { v.Name, v.DataType })
            .ToListAsync(ct);

        var entries = new List<ScriptTagInfo>(tagRows.Count + vtagRows.Count);

        foreach (var t in tagRows)
        {
            // The runtime GetTag key is the driver FullName from TagConfig; fall back to Name if absent.
            var full = ExtractFullNameFromTagConfig(t.TagConfig);
            var path = string.IsNullOrWhiteSpace(full) ? t.Name : full;
            entries.Add(new ScriptTagInfo(path, "Tag", t.DataType, null));
        }

        foreach (var v in vtagRows)
        {
            // Virtual tag — best-effort: the live resolution of virtual-tag cascade/write targets is
            // unconfirmed, so emit the leaf Name only (no UNS browse path, which never resolves).
            entries.Add(new ScriptTagInfo(v.Name, "Virtual tag", v.DataType, null));
        }

        return entries;
    }

    /// <summary>
    ///     Extracts the driver-side full reference from a <c>Tag.TagConfig</c> JSON blob — the
    ///     top-level <c>FullName</c> string every shipped driver stores. Mirrors
    ///     <c>Commons.Types.TagConfigIntent.Parse(...).FullName</c> (the shared byte-parity authority;
    ///     AdminUI does not reference that assembly here). Falls back to the raw blob when it is not
    ///     a JSON object carrying a string <c>FullName</c>.
    /// </summary>
    private static string ExtractFullNameFromTagConfig(string tagConfig)
    {
        // Best-effort: pull the driver FullName out of the TagConfig JSON blob if present.
        // Returns "" when absent/unparseable so the caller falls back to the tag Name — never
        // the raw blob (which would surface as a garbage completion path).
        if (string.IsNullOrWhiteSpace(tagConfig)) return "";
        try
        {
            using var doc = System.Text.Json.JsonDocument.Parse(tagConfig);
            if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object
                && doc.RootElement.TryGetProperty("FullName", out var fullName)
                && fullName.ValueKind == System.Text.Json.JsonValueKind.String)
            {
                return fullName.GetString() ?? "";
            }
        }
        catch (System.Text.Json.JsonException) { /* fall through to tag Name */ }
        return "";
    }
}
