using System.Text.Json.Serialization;

namespace Flyshot.Core.Domain;

/// <summary>
/// Represents an uploaded flyshot program built from teach points and shot metadata.
/// </summary>
public sealed class FlyshotProgram
{
    /// <summary>
    /// Initializes a validated flyshot program.
    /// </summary>
    public FlyshotProgram(
        string name,
        IEnumerable<JointWaypoint> waypoints,
        IEnumerable<bool> shotFlags,
        IEnumerable<int> offsetValues,
        IEnumerable<IoAddressGroup> addressGroups)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new ArgumentException("Program name is required.", nameof(name));
        }

        ArgumentNullException.ThrowIfNull(waypoints);
        ArgumentNullException.ThrowIfNull(shotFlags);
        ArgumentNullException.ThrowIfNull(offsetValues);
        ArgumentNullException.ThrowIfNull(addressGroups);

        // Freeze the uploaded data so later orchestration cannot silently drift from the request payload.
        var copiedWaypoints = waypoints.ToArray();
        var copiedShotFlags = shotFlags.ToArray();
        var copiedOffsetValues = offsetValues.ToArray();
        var copiedAddressGroups = addressGroups.ToArray();

        if (copiedWaypoints.Length == 0)
        {
            throw new ArgumentException("At least one waypoint is required.", nameof(waypoints));
        }

        if (copiedShotFlags.Length != copiedWaypoints.Length)
        {
            throw new ArgumentException("Shot flag count must match waypoint count.", nameof(shotFlags));
        }

        if (copiedOffsetValues.Length != copiedWaypoints.Length)
        {
            throw new ArgumentException("Offset value count must match waypoint count.", nameof(offsetValues));
        }

        if (copiedAddressGroups.Length != copiedWaypoints.Length)
        {
            throw new ArgumentException("Address group count must match waypoint count.", nameof(addressGroups));
        }

        var jointDimension = copiedWaypoints[0].Positions.Count;
        if (copiedWaypoints.Any(waypoint => waypoint.Positions.Count != jointDimension))
        {
            throw new ArgumentException("All waypoints must share the same joint dimension.", nameof(waypoints));
        }

        Name = name;
        Waypoints = copiedWaypoints;
        ShotFlags = copiedShotFlags;
        OffsetValues = copiedOffsetValues;
        AddressGroups = copiedAddressGroups;
        JointDimension = jointDimension;
        ShotWaypointCount = copiedShotFlags.Count(flag => flag);
    }

    /// <summary>
    /// Gets the program name used by cache and gateway lookups.
    /// </summary>
    [JsonPropertyName("name")]
    public string Name { get; }

    /// <summary>
    /// Gets the immutable teach waypoint list.
    /// </summary>
    [JsonPropertyName("waypoints")]
    public IReadOnlyList<JointWaypoint> Waypoints { get; }

    /// <summary>
    /// Gets the per-waypoint shot trigger flags.
    /// </summary>
    [JsonPropertyName("shotFlags")]
    public IReadOnlyList<bool> ShotFlags { get; }

    /// <summary>
    /// Gets the per-waypoint trigger offset values in servo cycles.
    /// </summary>
    [JsonPropertyName("offsetValues")]
    public IReadOnlyList<int> OffsetValues { get; }

    /// <summary>
    /// Gets the per-waypoint IO address groups.
    /// </summary>
    [JsonPropertyName("addressGroups")]
    public IReadOnlyList<IoAddressGroup> AddressGroups { get; }

    /// <summary>
    /// Gets the joint dimension shared by all waypoints.
    /// </summary>
    [JsonPropertyName("jointDimension")]
    public int JointDimension { get; }

    /// <summary>
    /// Gets the number of waypoints that request a shot trigger.
    /// </summary>
    [JsonPropertyName("shotWaypointCount")]
    public int ShotWaypointCount { get; }
}

/// <summary>
/// Represents a single teach waypoint in joint space.
/// </summary>
public sealed class JointWaypoint
{
    /// <summary>
    /// Initializes a validated joint waypoint.
    /// </summary>
    public JointWaypoint(IEnumerable<double> positions)
    {
        ArgumentNullException.ThrowIfNull(positions);

        // Copy the input once so the waypoint remains immutable after upload.
        var copiedPositions = positions.ToArray();
        if (copiedPositions.Length == 0)
        {
            throw new ArgumentException("Joint waypoint must contain at least one joint value.", nameof(positions));
        }

        Positions = copiedPositions;
    }

    /// <summary>
    /// Gets the immutable joint value vector.
    /// </summary>
    [JsonPropertyName("positions")]
    public IReadOnlyList<double> Positions { get; }
}

/// <summary>
/// Represents the list of IO addresses that should fire at one logical trigger point.
/// </summary>
public sealed class IoAddressGroup
{
    /// <summary>
    /// Initializes a validated IO address group.
    /// </summary>
    public IoAddressGroup(IEnumerable<int> addresses)
    {
        ArgumentNullException.ThrowIfNull(addresses);

        // Preserve address order because some upper-layer tooling expects exported order stability.
        var copiedAddresses = addresses.ToArray();
        if (copiedAddresses.Length > 8)
        {
            throw new ArgumentException("A single trigger point can contain at most eight IO addresses.", nameof(addresses));
        }

        if (copiedAddresses.Any(address => address < 0))
        {
            throw new ArgumentException("IO addresses must be zero or positive.", nameof(addresses));
        }

        Addresses = copiedAddresses;
    }

    /// <summary>
    /// Gets the immutable ordered IO address list.
    /// </summary>
    [JsonPropertyName("addresses")]
    public IReadOnlyList<int> Addresses { get; }
}
