using System.Collections;
using System.Diagnostics.CodeAnalysis;

namespace DodoSSH.Client.Domain;

/// <summary>
/// An ordered route to a host: the intermediate hosts to tunnel through, nearest hop first.
/// </summary>
/// <remarks>
/// <para>
/// A dedicated type rather than a list of ids, for two reasons. It compares by contents, which the
/// merge depends on — a plain <see cref="IReadOnlyList{T}"/> on a record gets reference equality from
/// the compiler-generated <c>Equals</c>, so every host would read as changed on every sync pass and
/// two identical edits would register as a conflict. And it names the thing: the order here is the
/// route, so this is not a set and must never be merged as one.
/// </para>
/// <para>
/// Duplicate and empty hops are not rejected at construction. They arrive from a decrypted payload
/// written by another client, and a constructor that threw would turn one bad item into a failed sync
/// pass for every other item behind it. <see cref="HostSecret.TryValidate"/> is where that is caught.
/// </para>
/// </remarks>
public sealed class JumpChain : IReadOnlyList<Guid>, IEquatable<JumpChain>
{
    private readonly Guid[] hops;
    private readonly int hash;

    private JumpChain(Guid[] hops)
    {
        this.hops = hops;
        hash = ComputeHash(hops);
    }

    /// <summary>A direct connection: no intermediate hosts.</summary>
    public static JumpChain Empty { get; } = new([]);

    /// <inheritdoc />
    public int Count => hops.Length;

    /// <inheritdoc />
    public Guid this[int index] => hops[index];

    /// <summary>Copies a sequence of hops, preserving order.</summary>
    public static JumpChain Create(IEnumerable<Guid> hops)
    {
        ArgumentNullException.ThrowIfNull(hops);

        var copy = hops.ToArray();
        return copy.Length == 0 ? Empty : new JumpChain(copy);
    }

    /// <summary>Copies a span of hops, preserving order.</summary>
    public static JumpChain Create(ReadOnlySpan<Guid> hops) =>
        hops.IsEmpty ? Empty : new JumpChain(hops.ToArray());

    /// <inheritdoc />
    public bool Equals(JumpChain? other)
    {
        if (ReferenceEquals(this, other))
        {
            return true;
        }

        return other is not null
            && other.hash == hash
            && hops.AsSpan().SequenceEqual(other.hops);
    }

    /// <inheritdoc />
    public override bool Equals(object? obj) => Equals(obj as JumpChain);

    /// <inheritdoc />
    public override int GetHashCode() => hash;

    /// <inheritdoc />
    public IEnumerator<Guid> GetEnumerator() => ((IEnumerable<Guid>)hops).GetEnumerator();

    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator() => hops.GetEnumerator();

    /// <summary>The hops, without copying.</summary>
    public ReadOnlySpan<Guid> AsSpan() => hops;

    /// <summary>Contents equality, tolerating nulls on either side.</summary>
    [SuppressMessage(
        "Usage",
        "CA2225:Operator overloads have named alternates",
        Justification = "Equals(JumpChain) is the named alternate.")]
    public static bool operator ==(JumpChain? left, JumpChain? right) =>
        left is null ? right is null : left.Equals(right);

    /// <summary>Contents inequality.</summary>
    public static bool operator !=(JumpChain? left, JumpChain? right) => !(left == right);

    private static int ComputeHash(Guid[] hops)
    {
        // Order-sensitive, because reordering a route changes which machine is reached through which.
        var accumulator = new HashCode();
        accumulator.Add(hops.Length);

        foreach (var hop in hops)
        {
            accumulator.Add(hop);
        }

        return accumulator.ToHashCode();
    }
}
