namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;

/// <summary>
///     Parsed Logix-symbolic tag path. Handles controller-scope (<c>Motor1_Speed</c>),
///     program-scope (<c>Program:MainProgram.StepIndex</c>), structured member access
///     (<c>Motor1.Speed.Setpoint</c>), array subscripts (<c>Array[0]</c>, <c>Matrix[1,2]</c>),
///     and bit-within-DINT access (<c>Flags.3</c>). Reassembles the canonical Logix syntax via
///     <see cref="ToLibplctagName"/>, which is the exact string libplctag's <c>name=...</c>
///     attribute consumes.
/// </summary>
/// <remarks>
///     Scope + members + subscripts are captured structurally so UDT support can walk the
///     path against a cached template (see <see cref="AbCipTemplateCache"/>) without
///     re-parsing. <see cref="BitIndex"/> is
///     non-null only when the trailing segment is a decimal integer between 0 and 31 that
///     parses as a bit-selector — this is the <c>.N</c> syntax documented in the Logix 5000
///     General Instructions Reference §Tags, and it applies only to DINT-typed parents. The
///     parser does not validate the parent type (requires live template data) — it accepts the
///     shape and defers type-correctness to the runtime.
/// </remarks>
public sealed record AbCipTagPath(
    string? ProgramScope,
    IReadOnlyList<AbCipTagPathSegment> Segments,
    int? BitIndex)
{
    /// <summary>Rebuild the canonical Logix tag string.</summary>
    /// <returns>The canonical Logix tag string.</returns>
    public string ToLibplctagName()
    {
        var buf = new System.Text.StringBuilder();
        if (ProgramScope is not null)
            buf.Append("Program:").Append(ProgramScope).Append('.');

        for (var i = 0; i < Segments.Count; i++)
        {
            if (i > 0) buf.Append('.');
            var seg = Segments[i];
            buf.Append(seg.Name);
            if (seg.Subscripts.Count > 0)
                buf.Append('[').Append(string.Join(",", seg.Subscripts)).Append(']');
        }
        if (BitIndex is not null) buf.Append('.').Append(BitIndex.Value);
        return buf.ToString();
    }

    /// <summary>
    ///     Parse a Logix-symbolic tag reference. Returns <c>null</c> on a shape the parser
    ///     doesn't support — the driver surfaces that as a config-validation error rather than
    ///     attempting a best-effort translation.
    /// </summary>
    /// <param name="value">The tag path string to parse, or null.</param>
    /// <returns>The parsed AbCipTagPath, or null if the syntax is invalid.</returns>
    public static AbCipTagPath? TryParse(string? value)
    {
        if (string.IsNullOrWhiteSpace(value)) return null;
        var src = value.Trim();

        string? programScope = null;
        const string programPrefix = "Program:";
        if (src.StartsWith(programPrefix, StringComparison.OrdinalIgnoreCase))
        {
            var afterPrefix = src[programPrefix.Length..];
            var dotIdx = afterPrefix.IndexOf('.');
            if (dotIdx <= 0) return null;
            programScope = afterPrefix[..dotIdx];
            src = afterPrefix[(dotIdx + 1)..];
            if (string.IsNullOrEmpty(src)) return null;
        }

        // Split on dots, but preserve any [i,j] subscript runs that contain only digits + commas.
        var parts = new List<string>();
        var depth = 0;
        var start = 0;
        for (var i = 0; i < src.Length; i++)
        {
            var c = src[i];
            if (c == '[') depth++;
            else if (c == ']') depth--;
            else if (c == '.' && depth == 0)
            {
                parts.Add(src[start..i]);
                start = i + 1;
            }
        }
        parts.Add(src[start..]);
        if (depth != 0 || parts.Any(string.IsNullOrEmpty)) return null;

        int? bitIndex = null;
        if (parts.Count >= 2 && int.TryParse(parts[^1], out var maybeBit)
            && maybeBit is >= 0 and <= 31
            && !parts[^1].Contains('['))
        {
            bitIndex = maybeBit;
            parts.RemoveAt(parts.Count - 1);
        }

        var segments = new List<AbCipTagPathSegment>(parts.Count);
        foreach (var part in parts)
        {
            var bracketIdx = part.IndexOf('[');
            if (bracketIdx < 0)
            {
                if (!IsValidIdent(part)) return null;
                segments.Add(new AbCipTagPathSegment(part, []));
                continue;
            }
            if (!part.EndsWith(']')) return null;
            var name = part[..bracketIdx];
            if (!IsValidIdent(name)) return null;
            var inner = part[(bracketIdx + 1)..^1];
            var subs = new List<int>();
            foreach (var tok in inner.Split(','))
            {
                if (!int.TryParse(tok, out var n) || n < 0) return null;
                subs.Add(n);
            }
            if (subs.Count == 0) return null;
            segments.Add(new AbCipTagPathSegment(name, subs));
        }
        if (segments.Count == 0) return null;

        return new AbCipTagPath(programScope, segments, bitIndex);
    }

    private static bool IsValidIdent(string s)
    {
        if (string.IsNullOrEmpty(s)) return false;
        if (!char.IsLetter(s[0]) && s[0] != '_') return false;
        for (var i = 1; i < s.Length; i++)
            if (!char.IsLetterOrDigit(s[i]) && s[i] != '_') return false;
        return true;
    }
}

/// <summary>One path segment: a member name plus any numeric subscripts.</summary>
public sealed record AbCipTagPathSegment(string Name, IReadOnlyList<int> Subscripts);
