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

/// <summary>
///     The v3 identity authority. A <c>RawPath</c> is the cluster-scoped, slash-separated raw device
///     path — <c>&lt;Folder/…&gt;/&lt;Driver&gt;/&lt;Device&gt;/&lt;TagGroup/…&gt;/&lt;Tag&gt;</c> — and is
///     the single string used at every identity seam: <c>ns=Raw</c> NodeIds, the driver wire-reference
///     (<c>EquipmentTagRefResolver</c> key), the value fan-out / write-routing maps
///     (<c>DriverHostActor</c>), the historian default tagname + mux interest key, the native-alarm
///     <c>ConditionId</c>, and <c>ctx.GetTag</c> script paths.
///     <para>
///     RawPaths are ALWAYS built through this helper — never string-concatenated ad hoc — so the
///     separator and the segment charset are enforced in exactly one place. Comparison is
///     case-sensitive ordinal everywhere (see <see cref="Comparer"/>).
///     </para>
/// </summary>
public static class RawPaths
{
    /// <summary>The single path separator character. Forbidden inside any segment.</summary>
    public const char Separator = '/';

    /// <summary>The path separator as a string (join/split convenience).</summary>
    public const string SeparatorString = "/";

    /// <summary>The one comparer for RawPaths and their segments: case-sensitive ordinal.</summary>
    public static StringComparer Comparer => StringComparer.Ordinal;

    /// <summary>
    ///     Validate a single path segment. A segment must be non-empty, contain no
    ///     <see cref="Separator"/>, and carry no leading/trailing whitespace (the separator and
    ///     surrounding whitespace would corrupt the NodeId / round-trip).
    /// </summary>
    /// <param name="segment">The candidate segment (a folder / driver / device / group / tag name).</param>
    /// <returns>A human-readable error when invalid, or <see langword="null"/> when the segment is valid.</returns>
    public static string? ValidateSegment(string? segment)
    {
        if (string.IsNullOrEmpty(segment))
            return "Name must not be empty.";
        if (segment.IndexOf(Separator) >= 0)
            return $"Name must not contain '{Separator}'.";
        if (char.IsWhiteSpace(segment[0]) || char.IsWhiteSpace(segment[^1]))
            return "Name must not have leading or trailing whitespace.";
        return null;
    }

    /// <summary>True when <paramref name="segment"/> passes <see cref="ValidateSegment"/>.</summary>
    /// <param name="segment">The candidate segment.</param>
    /// <returns><see langword="true"/> when the segment is a legal RawPath segment.</returns>
    public static bool IsValidSegment(string? segment) => ValidateSegment(segment) is null;

    /// <summary>
    ///     Build a RawPath from ordered segments (root-first). Every segment is validated via
    ///     <see cref="ValidateSegment"/>; an invalid or empty segment list throws.
    /// </summary>
    /// <param name="segments">The ordered, non-empty segments (root → leaf).</param>
    /// <returns>The slash-joined RawPath.</returns>
    /// <exception cref="ArgumentException">A segment is invalid, or the sequence is empty.</exception>
    public static string Build(IEnumerable<string> segments)
    {
        ArgumentNullException.ThrowIfNull(segments);
        var list = segments as IReadOnlyList<string> ?? segments.ToList();
        if (list.Count == 0)
            throw new ArgumentException("A RawPath must have at least one segment.", nameof(segments));

        for (var i = 0; i < list.Count; i++)
        {
            var error = ValidateSegment(list[i]);
            if (error is not null)
                throw new ArgumentException($"Invalid RawPath segment at index {i} ('{list[i]}'): {error}", nameof(segments));
        }

        return string.Join(Separator, list);
    }

    /// <summary>Build a RawPath from ordered segments (root-first). See <see cref="Build(IEnumerable{string})"/>.</summary>
    /// <param name="segments">The ordered, non-empty segments (root → leaf).</param>
    /// <returns>The slash-joined RawPath.</returns>
    public static string Build(params string[] segments) => Build((IEnumerable<string>)segments);

    /// <summary>
    ///     Append a child segment to an existing (already-built) parent RawPath. The child is
    ///     validated; a blank parent yields the (validated) child alone.
    /// </summary>
    /// <param name="parentPath">The parent RawPath (assumed already valid), or blank for a root segment.</param>
    /// <param name="childSegment">The child segment to append.</param>
    /// <returns>The combined RawPath.</returns>
    /// <exception cref="ArgumentException">The child segment is invalid.</exception>
    public static string Combine(string? parentPath, string childSegment)
    {
        var error = ValidateSegment(childSegment);
        if (error is not null)
            throw new ArgumentException($"Invalid RawPath segment ('{childSegment}'): {error}", nameof(childSegment));

        return string.IsNullOrEmpty(parentPath) ? childSegment : parentPath + Separator + childSegment;
    }

    /// <summary>Split a RawPath into its ordered segments (root → leaf).</summary>
    /// <param name="rawPath">The RawPath to split.</param>
    /// <returns>The ordered segments; a single-element array for a one-segment path.</returns>
    public static IReadOnlyList<string> Split(string rawPath)
    {
        ArgumentNullException.ThrowIfNull(rawPath);
        return rawPath.Split(Separator);
    }

    /// <summary>The leaf (last) segment of a RawPath.</summary>
    /// <param name="rawPath">The RawPath.</param>
    /// <returns>The final segment.</returns>
    public static string Leaf(string rawPath)
    {
        ArgumentNullException.ThrowIfNull(rawPath);
        var idx = rawPath.LastIndexOf(Separator);
        return idx < 0 ? rawPath : rawPath[(idx + 1)..];
    }

    /// <summary>
    ///     Get the parent RawPath (everything up to, not including, the leaf). Returns
    ///     <see langword="false"/> for a single-segment path (no parent).
    /// </summary>
    /// <param name="rawPath">The RawPath.</param>
    /// <param name="parent">The parent RawPath when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when a parent exists.</returns>
    public static bool TryParent(string rawPath, out string parent)
    {
        ArgumentNullException.ThrowIfNull(rawPath);
        var idx = rawPath.LastIndexOf(Separator);
        if (idx < 0)
        {
            parent = string.Empty;
            return false;
        }

        parent = rawPath[..idx];
        return true;
    }
}
