namespace Coder.Desktop.Vpn.Proto;

/// <summary>
///     A version of the RPC API. Can be compared other versions to determine compatibility between two peers.
/// </summary>
public class RpcVersion
{
    public static readonly RpcVersion Current = new(1, 1);

    public ulong Major { get; }
    public ulong Minor { get; }

    /// <param name="major">The major version of the peer</param>
    /// <param name="minor">The minor version of the peer</param>
    public RpcVersion(ulong major, ulong minor)
    {
        Major = major;
        Minor = minor;
    }

    /// <summary>
    ///     Parse a string in the format "major.minor" into an ApiVersion.
    /// </summary>
    /// <param name="versionString">Version string to parse</param>
    /// <returns>Parsed ApiVersion</returns>
    /// <exception cref="ArgumentException">The version string is invalid</exception>
    public static RpcVersion Parse(string versionString)
    {
        var parts = versionString.Split('.');
        if (parts.Length != 2) throw new ArgumentException($"Invalid version string '{versionString}'");

        try
        {
            var major = ulong.Parse(parts[0]);
            if (major == 0) throw new ArgumentException($"Invalid major version '{major}'");
            var minor = ulong.Parse(parts[1]);
            return new RpcVersion(major, minor);
        }
        catch (FormatException e)
        {
            throw new ArgumentException($"Invalid version string '{versionString}'", e);
        }
    }

    public override string ToString()
    {
        return $"{Major}.{Minor}";
    }

    /// <summary>
    ///     Returns the lowest version that is compatible with both this version and the other version. If no compatible
    ///     version is found, null is returned.
    /// </summary>
    /// <param name="other">Version to compare against</param>
    /// <returns>The highest compatible version</returns>
    public RpcVersion? IsCompatibleWith(RpcVersion other)
    {
        if (Major != other.Major)
            return null;

        // The lowest minor version from the two versions should be returned.
        return Minor < other.Minor ? this : other;
    }

    #region RpcVersion Equality

    public static bool operator ==(RpcVersion a, RpcVersion b)
    {
        return a.Equals(b);
    }

    public static bool operator !=(RpcVersion a, RpcVersion b)
    {
        return !a.Equals(b);
    }

    private bool Equals(RpcVersion other)
    {
        return Major == other.Major && Minor == other.Minor;
    }

    public override bool Equals(object? obj)
    {
        if (obj is null) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != GetType()) return false;
        return Equals((RpcVersion)obj);
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Major, Minor);
    }

    #endregion
}

public class RpcVersionList : List<RpcVersion>
{
    public static readonly RpcVersionList Current = new(RpcVersion.Current);

    public RpcVersionList(IEnumerable<RpcVersion> versions) : base(versions)
    {
    }

    public RpcVersionList(params RpcVersion[] versions) : base(versions)
    {
    }

    public static RpcVersionList Parse(string versions)
    {
        try
        {
            var l = new RpcVersionList(versions.Split(',').Select(RpcVersion.Parse));
            l.Validate();
            return l;
        }
        catch (Exception e)
        {
            throw new ArgumentException($"Invalid version list '{versions}'", e);
        }
    }

    public override string ToString()
    {
        return string.Join(",", this);
    }

    /// <summary>
    ///     Validates that the version list doesn't contain any invalid versions, duplicate major versions, or is unsorted.
    /// </summary>
    /// <exception cref="ArgumentException">The version list is not valid</exception>
    public void Validate()
    {
        if (Count == 0) throw new ArgumentException("Version list must contain at least one version");
        for (var i = 0; i < Count; i++)
        {
            if (this[i].Major == 0) throw new ArgumentException($"Invalid major version '{this[i].Major}'");
            if (i > 0 && this[i - 1].Major == this[i].Major)
                throw new ArgumentException($"Duplicate major version '{this[i].Major}'");
            if (i > 0 && this[i - 1].Major > this[i].Major) throw new ArgumentException("Versions are not sorted");
        }
    }

    /// <summary>
    ///     Returns the lowest version that is compatible with both version lists. If there is no compatible version,
    ///     null is returned.
    /// </summary>
    /// <param name="other">Version list to compare against</param>
    /// <returns>The highest compatible version</returns>
    public RpcVersion? IsCompatibleWith(RpcVersionList other)
    {
        RpcVersion? bestVersion = null;
        foreach (var v1 in this)
            foreach (var v2 in other)
                if (v1.Major == v2.Major && (bestVersion is null || v1.Major > bestVersion.Major))
                {
                    var v = v1.IsCompatibleWith(v2);
                    if (v is not null) bestVersion = v;
                }

        return bestVersion;
    }
}