using System.Text.RegularExpressions;

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

/// <summary>
/// Helpers for equipment-relative virtual-tag script paths. The reserved token
/// <c>{{equip}}</c> inside a <c>ctx.GetTag</c>/<c>ctx.SetVirtualTag</c> path literal is
/// replaced at the compose seams with the owning equipment's tag base prefix (derived
/// from its child-tag <c>FullName</c>s). 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-base token.</summary>
    public const string EquipToken = "{{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);

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

    /// <summary>
    /// Equipment tag base = the single shared substring-before-first-dot across the
    /// equipment's child-tag <c>FullName</c>s. Returns <c>null</c> when there are no usable
    /// FullNames or they don't agree on one prefix (equipment spanning multiple objects).
    /// </summary>
    /// <param name="childFullNames">The equipment's child-tag driver FullNames.</param>
    /// <returns>The shared base prefix, or null when none/ambiguous.</returns>
    public static string? DeriveEquipmentBase(IEnumerable<string?> childFullNames)
    {
        string? found = null;
        foreach (var fn in childFullNames)
        {
            if (string.IsNullOrWhiteSpace(fn)) continue;
            var dot = fn.IndexOf('.');
            var prefix = dot < 0 ? fn : fn.Substring(0, dot);
            if (prefix.Length == 0) continue;
            if (found is null) found = prefix;
            else if (!string.Equals(found, prefix, StringComparison.Ordinal)) return null;
        }
        return found;
    }

    /// <summary>
    /// Replace <c>{{equip}}</c> with <paramref name="equipBase"/> inside
    /// <c>ctx.GetTag</c>/<c>ctx.SetVirtualTag</c> path literals only. Identity when
    /// <paramref name="equipBase"/> is null/empty or the token is absent (so every existing
    /// script — none of which use the token — is byte-unchanged).
    /// </summary>
    /// <param name="source">The script source.</param>
    /// <param name="equipBase">The equipment base prefix, or null/empty for no substitution.</param>
    /// <returns>The source with the token substituted inside path literals.</returns>
    public static string SubstituteEquipmentToken(string source, string? equipBase)
    {
        if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(equipBase)) return source;
        if (!source.Contains(EquipToken, StringComparison.Ordinal)) return source;
        return PathLiteralRegex.Replace(source, m =>
            m.Groups[1].Value
            + m.Groups[2].Value.Replace(EquipToken, equipBase, StringComparison.Ordinal)
            + m.Groups[3].Value);
    }

    /// <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>Phase7Composer</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>Phase7Composer</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. Scripted alarms do NOT use <c>{{equip}}</c> substitution (only virtual
    /// tags do) — pass the predicate source as-is.
    /// </summary>
    /// <param name="predicateSource">The resolved 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;
    }
}
