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>
    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>
    Task<ScriptTagInfo?> GetTagInfoAsync(string path, CancellationToken ct);

    /// <summary>Distinct attribute leaf names — the substring after the first dot of each
    /// configured tag FullName — optionally prefix-filtered. Powers {{equip}}. completion,
    /// which needs the per-equipment object prefix stripped.</summary>
    /// <param name="filter">Case-insensitive StartsWith prefix over the leaf; null/empty = all (bounded).</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>Distinct leaf names.</returns>
    Task<IReadOnlyList<string>> GetEquipmentRelativeLeavesAsync(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 SystemPlatform tags, 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>Phase7Composer</c> (via <c>EquipmentScriptPaths.ExtractDependencyRefs</c>) harvesting the <c>ctx.GetTag("…")</c> literals
///         into <c>EquipmentVirtualTagPlan.DependencyRefs</c>
///         (<c>src/Server/…OpcUaServer/Phase7Composer.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>Phase7Composer.ExtractTagFullName</c> + <c>EquipmentNodeWalker.ExtractFullName</c>).
///         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).</item>
///           <item>SystemPlatform tag (<c>EquipmentId == null</c>) → the MXAccess dot-ref
///           (<c>FolderPath.Name</c> when a folder is set, else <c>Name</c>) — see
///           <c>Phase7Composer</c>'s <c>GalaxyTagPlan.MxAccessRef</c>.</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>> GetEquipmentRelativeLeavesAsync(string? filter, CancellationToken ct)
    {
        var entries = await BuildEntriesAsync(ct);
        var leaves = new HashSet<string>(StringComparer.Ordinal);
        foreach (var e in entries)
        {
            var dot = e.Path.IndexOf('.');
            if (dot < 0 || dot + 1 >= e.Path.Length) continue;
            leaves.Add(e.Path.Substring(dot + 1));
        }
        IEnumerable<string> q = leaves;
        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);

        // Only the resolvable keys are projected, so no UNS join is needed: the equipment-tag key is
        // the FullName from TagConfig, the SystemPlatform key is FolderPath/Name, and the virtual-tag
        // key is its own Name. Pull just those columns, untracked.
        var tagRows = await db.Tags
            .AsNoTracking()
            .Select(t => new { t.EquipmentId, t.Name, t.FolderPath, t.DataType, t.DriverInstanceId, 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)
        {
            var path = t.EquipmentId is null
                // SystemPlatform tag — the Galaxy driver subscribes by the MXAccess dot-ref, which is
                // "FolderPath.Name" when a folder is set, else just "Name".
                ? (string.IsNullOrWhiteSpace(t.FolderPath) ? t.Name : $"{t.FolderPath}.{t.Name}")
                // Equipment driver tag — the runtime GetTag key is the driver FullName from TagConfig.
                : ExtractFullNameFromTagConfig(t.TagConfig);
            entries.Add(new ScriptTagInfo(path, "Tag", t.DataType, t.DriverInstanceId));
        }

        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>EquipmentNodeWalker.ExtractFullName</c> / <c>Phase7Composer.ExtractTagFullName</c>
    ///     (AdminUI does not reference those assemblies). 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)
    {
        if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig;
        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() ?? tagConfig;
            }
        }
        catch (System.Text.Json.JsonException) { /* fall through to raw blob */ }
        return tagConfig;
    }
}
