using System.Text.RegularExpressions;

namespace ZB.MOM.WW.OtOpcUa.Commons.Types;

/// <summary>
/// Helpers for equipment-relative virtual-tag / scripted-alarm script paths. The reserved token
/// <c>{{equip}}/&lt;RefName&gt;</c> inside a <c>ctx.GetTag</c>/<c>ctx.SetVirtualTag</c> path literal is
/// replaced at the compose seams (<c>AddressSpaceComposer</c> + <c>DeploymentArtifact</c>) with the
/// backing raw tag's <c>RawPath</c>, resolved through the owning equipment's
/// <c>UnsTagReference</c> rows by effective name (<c>&lt;RefName&gt;</c>).
/// <para><b>v3 syntax:</b> the token joint is a <b>slash</b> — <c>{{equip}}/&lt;RefName&gt;</c> — replacing
/// the v2 dot-prefix derivation (<c>{{equip}}.X</c>). <c>&lt;RefName&gt;</c> is a reference's effective name
/// (its <c>DisplayNameOverride</c> else the backing raw tag's <c>Name</c>). An unresolved
/// <c>&lt;RefName&gt;</c> is left un-substituted (substitution never throws); the deploy-time validator +
/// the authoring gate + the Monaco diagnostic reject it first, preserving the invariant
/// <b>the editor accepts ⇔ publish accepts</b>.</para>
/// Pure + regex-based (no Roslyn) so the OpcUaServer composer and the Runtime artifact-decode path can
/// both share it. Also the single home for the <c>ctx.GetTag("…")</c> dependency-ref extraction those
/// two seams used to duplicate.
/// </summary>
public static class EquipmentScriptPaths
{
    /// <summary>The reserved equipment token stem.</summary>
    public const string EquipToken = "{{equip}}";

    /// <summary>The reserved equipment reference-relative prefix — the token stem plus the slash joint.</summary>
    public const string EquipTokenPrefix = "{{equip}}/";

    // ctx.GetTag("ref") — reads only; the dependency graph subscribes to exactly these.
    private static readonly Regex GetTagRefRegex =
        new(@"ctx\s*\.\s*GetTag\s*\(\s*""([^""]+)""\s*\)", RegexOptions.Compiled);

    // {TagPath} message-template token — a single-brace {…} run. The negative look-behind/-ahead
    // reject the reserved {{equip}} double-brace form (and any other doubled brace), so "{A.X}"
    // matches but "{{equip}}" does not yield "{equip}".
    private static readonly Regex MessageTemplateTokenRegex =
        new(@"(?<!\{)\{([^{}]+)\}(?!\})", RegexOptions.Compiled);

    // ctx.GetTag("…") OR ctx.SetVirtualTag("…", …) — first string-literal arg captured in
    // three parts (prefix, content, closing quote) so token substitution touches ONLY the
    // literal content (never a comment, Logger string, or other code).
    private static readonly Regex PathLiteralRegex =
        new(@"(ctx\s*\.\s*(?:GetTag|SetVirtualTag)\s*\(\s*"")([^""]*)("")", RegexOptions.Compiled);

    // {{equip}}/<RefName> occurrence in FREE TEXT (message templates). <RefName> runs until the next
    // delimiter that cannot appear in an intra-literal RawPath segment used inline in a template —
    // whitespace, quote, brace, paren, semicolon, or the '/' separator. (Reference effective names may
    // contain internal spaces; that space-bearing form is unambiguous only inside a path literal, so the
    // free-text scan is best-effort — see ExtractEquipReferenceNamesFromText.)
    private static readonly Regex EquipRefTextRegex =
        new(@"\{\{equip\}\}/([^\s""{}();/]+)", RegexOptions.Compiled);

    // A pure pass-through virtual-tag body: exactly `return ctx.GetTag("<ref>").Value;`
    // (whitespace-insensitive). Captures <ref> for relay→alias conversion. Anything with extra
    // statements, arithmetic, a different member than .Value, or multiple GetTag calls is NOT a relay.
    private static readonly Regex RelayBodyRegex = new(
        @"^\s*return\s+ctx\s*\.\s*GetTag\s*\(\s*""([^""]+)""\s*\)\s*\.\s*Value\s*;\s*$",
        RegexOptions.Compiled);

    /// <summary>True when the source uses the <c>{{equip}}</c> token anywhere.</summary>
    /// <param name="source">The script source to scan.</param>
    /// <returns>True if <paramref name="source"/> contains the <c>{{equip}}</c> token; otherwise false.</returns>
    public static bool ContainsEquipToken(string? source) =>
        !string.IsNullOrEmpty(source) && source.Contains(EquipToken, StringComparison.Ordinal);

    /// <summary>
    /// Replace each <c>{{equip}}/&lt;RefName&gt;</c> reference-relative path with the backing raw tag's
    /// <c>RawPath</c> from <paramref name="referenceMap"/> (effective name → RawPath), inside
    /// <c>ctx.GetTag</c>/<c>ctx.SetVirtualTag</c> path literals only. A path literal whose content is
    /// <c>{{equip}}/&lt;RefName&gt;</c> is treated as a single reference: <c>&lt;RefName&gt;</c> is the
    /// entire remainder after the prefix (so reference effective names may contain internal spaces).
    /// Identity when <paramref name="source"/> is null/empty, <paramref name="referenceMap"/> is empty,
    /// or the token is absent (so every existing script — none of which use the token — is byte-unchanged).
    /// An <c>&lt;RefName&gt;</c> absent from the map is left un-substituted (never throws) — the validator
    /// rejects it first at deploy.
    /// </summary>
    /// <param name="source">The script source.</param>
    /// <param name="referenceMap">The equipment's reference map: effective name → backing-tag RawPath.</param>
    /// <returns>The source with each resolvable <c>{{equip}}/&lt;RefName&gt;</c> substituted inside path literals.</returns>
    public static string SubstituteEquipmentToken(string source, IReadOnlyDictionary<string, string> referenceMap)
    {
        if (string.IsNullOrEmpty(source) || referenceMap is null || referenceMap.Count == 0) return source;
        if (!source.Contains(EquipTokenPrefix, StringComparison.Ordinal)) return source;
        return PathLiteralRegex.Replace(source, m =>
            m.Groups[1].Value
            + SubstituteContent(m.Groups[2].Value, referenceMap)
            + m.Groups[3].Value);
    }

    // Substitute the reference-relative token inside a single path-literal's content. The content is one
    // whole tag path; when it starts with the {{equip}}/ prefix the remainder is the reference name.
    private static string SubstituteContent(string content, IReadOnlyDictionary<string, string> referenceMap)
    {
        if (!content.StartsWith(EquipTokenPrefix, StringComparison.Ordinal)) return content;
        var refName = content.Substring(EquipTokenPrefix.Length);
        if (refName.Length == 0) return content;
        return referenceMap.TryGetValue(refName, out var rawPath) ? rawPath : content;
    }

    /// <summary>
    /// Distinct <c>&lt;RefName&gt;</c> values used via <c>{{equip}}/&lt;RefName&gt;</c> inside
    /// <c>ctx.GetTag</c>/<c>ctx.SetVirtualTag</c> path literals, in first-seen order. Scoped to the SAME
    /// path literals <see cref="SubstituteEquipmentToken"/> operates on (so a token in a comment / logger
    /// string is not reported), and the entire post-prefix remainder is the reference name (matching
    /// substitution) — this keeps the validator / authoring gate / Monaco diagnostic in lockstep with what
    /// resolves. Feeds the deploy-gate + authoring rejection + editor completion.
    /// </summary>
    /// <param name="scriptSource">The virtual-tag / scripted-alarm predicate script source.</param>
    /// <returns>Distinct reference names, first-seen order; empty when none / no token.</returns>
    public static IReadOnlyList<string> ExtractEquipReferenceNames(string? scriptSource)
    {
        if (string.IsNullOrEmpty(scriptSource)
            || !scriptSource.Contains(EquipTokenPrefix, StringComparison.Ordinal))
            return Array.Empty<string>();
        var seen = new HashSet<string>(StringComparer.Ordinal);
        var result = new List<string>();
        foreach (Match m in PathLiteralRegex.Matches(scriptSource))
        {
            var content = m.Groups[2].Value;
            if (!content.StartsWith(EquipTokenPrefix, StringComparison.Ordinal)) continue;
            var refName = content.Substring(EquipTokenPrefix.Length);
            if (refName.Length > 0 && seen.Add(refName)) result.Add(refName);
        }
        return result;
    }

    /// <summary>
    /// Distinct <c>&lt;RefName&gt;</c> values used via <c>{{equip}}/&lt;RefName&gt;</c> in FREE TEXT — the
    /// scripted-alarm <c>MessageTemplate</c>, which is not a path literal — in first-seen order. Best-effort:
    /// a reference name is captured up to the next whitespace / quote / brace / paren / semicolon / slash, so
    /// a space-bearing effective name used inline in a template resolves only up to its first space (a
    /// documented, benign limitation — template rendering is dark until Batch 4). Feeds the deploy-gate rule
    /// for alarm message tokens.
    /// </summary>
    /// <param name="text">The free text (e.g. a scripted-alarm message template).</param>
    /// <returns>Distinct reference names, first-seen order; empty when none / no token.</returns>
    public static IReadOnlyList<string> ExtractEquipReferenceNamesFromText(string? text)
    {
        if (string.IsNullOrEmpty(text)
            || !text.Contains(EquipTokenPrefix, StringComparison.Ordinal))
            return Array.Empty<string>();
        var seen = new HashSet<string>(StringComparer.Ordinal);
        var result = new List<string>();
        foreach (Match m in EquipRefTextRegex.Matches(text))
        {
            var refName = m.Groups[1].Value;
            if (refName.Length > 0 && seen.Add(refName)) result.Add(refName);
        }
        return result;
    }

    /// <summary>
    /// Distinct <c>ctx.GetTag("ref")</c> string literals in first-seen order — the
    /// dependency refs the <c>VirtualTagActor</c> subscribes to. The single shared copy
    /// formerly duplicated in <c>AddressSpaceComposer</c> + <c>DeploymentArtifact</c>. GetTag
    /// only (writes are not dependencies).
    /// </summary>
    /// <param name="scriptSource">The (already substituted) script source.</param>
    /// <returns>Distinct read refs, first-seen order.</returns>
    public static IReadOnlyList<string> ExtractDependencyRefs(string? scriptSource)
    {
        if (string.IsNullOrWhiteSpace(scriptSource)) return Array.Empty<string>();
        var seen = new HashSet<string>(StringComparer.Ordinal);
        var result = new List<string>();
        foreach (Match m in GetTagRefRegex.Matches(scriptSource))
        {
            var r = m.Groups[1].Value;
            if (seen.Add(r)) result.Add(r);
        }
        return result;
    }

    /// <summary>
    /// Merge a scripted alarm's dependency graph: the predicate script's distinct
    /// <c>ctx.GetTag("…")</c> read refs (via <see cref="ExtractDependencyRefs"/>, first-seen order)
    /// UNION the distinct <c>{TagPath}</c> token paths referenced in the message template (first-seen
    /// order, appended after the predicate reads, trimmed + non-empty). The reserved
    /// <c>{{equip}}</c> double-brace form is excluded by the token regex. Deterministic so the live
    /// composer (<c>AddressSpaceComposer</c>) and the artifact-decode mirror (<c>DeploymentArtifact</c>)
    /// produce the exact same ordered list — the byte-parity contract <c>EquipmentScriptedAlarmPlan</c>
    /// equality depends on. The predicate source passed here is the ALREADY-substituted source (the two
    /// compose seams substitute <c>{{equip}}/&lt;RefName&gt;</c> before extraction, identically), so the
    /// merged refs are resolved RawPaths.
    /// </summary>
    /// <param name="predicateSource">The resolved (substituted) predicate script source.</param>
    /// <param name="messageTemplate">The alarm message template carrying <c>{TagPath}</c> tokens.</param>
    /// <returns>The merged, distinct, deterministically-ordered dependency refs.</returns>
    public static IReadOnlyList<string> ExtractAlarmDependencyRefs(string? predicateSource, string? messageTemplate)
    {
        var seen = new HashSet<string>(StringComparer.Ordinal);
        var result = new List<string>();

        // Predicate reads first — same regex extraction the VirtualTag projection uses.
        foreach (var r in ExtractDependencyRefs(predicateSource))
        {
            if (seen.Add(r)) result.Add(r);
        }

        // Then message-template {TagPath} tokens (first-seen), trimmed, non-empty.
        if (!string.IsNullOrEmpty(messageTemplate))
        {
            foreach (Match m in MessageTemplateTokenRegex.Matches(messageTemplate))
            {
                var token = m.Groups[1].Value.Trim();
                if (token.Length > 0 && seen.Add(token)) result.Add(token);
            }
        }

        return result;
    }

    /// <summary>
    /// Recognise a pure relay virtual-tag body — one whose entire body is exactly
    /// <c>return ctx.GetTag("<paramref name="tagReference"/>").Value;</c> (whitespace-insensitive) —
    /// and capture its single <c>GetTag</c> reference. Bodies with extra statements, arithmetic,
    /// a member other than <c>.Value</c>, or multiple <c>GetTag</c> calls are <em>not</em> relays.
    /// </summary>
    /// <param name="source">The virtual-tag script source to inspect.</param>
    /// <param name="tagReference">
    /// When the method returns <see langword="true"/>, the captured <c>GetTag</c> path literal
    /// (e.g. <c>TestMachine_020.TestChangingInt</c> or <c>{{equip}}/Speed</c>);
    /// otherwise <see langword="null"/>.
    /// </param>
    /// <returns>
    /// <see langword="true"/> when <paramref name="source"/> is a pure pass-through relay;
    /// <see langword="false"/> for any other body or <see langword="null"/>/<see langword="whitespace"/> input.
    /// </returns>
    public static bool TryParseRelayBody(string? source, out string? tagReference)
    {
        tagReference = null;
        if (string.IsNullOrWhiteSpace(source)) return false;
        var m = RelayBodyRegex.Match(source);
        if (!m.Success) return false;
        tagReference = m.Groups[1].Value;
        return true;
    }
}
