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

/// <summary>
///     Wire-layer abstraction over one FOCAS session to a CNC. The driver holds one per
///     configured device; lifetime matches the device.
/// </summary>
/// <remarks>
///     <para>The default implementation is <see cref="Wire.WireFocasClient"/> — a pure-managed
///     FOCAS/2 Ethernet client that speaks the wire protocol directly on TCP:8193. No
///     P/Invoke, no native DLLs, no out-of-process isolation.</para>
///
///     <para><see cref="UnimplementedFocasClientFactory"/> is a scaffolding backend that
///     throws on <see cref="IFocasClientFactory.Create"/> — selected by
///     <c>"Backend": "unimplemented"</c> so a DriverInstance row can be seeded before the CNC
///     endpoint is reachable without silently reading stale data.</para>
/// </remarks>
public interface IFocasClient : IDisposable
{
    /// <summary>Open the FWLIB handle + TCP session. Idempotent.</summary>
    /// <param name="address">The CNC host address and port.</param>
    /// <param name="timeout">The connection timeout duration.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken);

    /// <summary>True when the FWLIB handle is valid + the socket is up.</summary>
    bool IsConnected { get; }

    /// <summary>
    ///     Read the value at <paramref name="address"/> in the requested
    ///     <paramref name="type"/>. Returns a boxed .NET value + the OPC UA status mapped
    ///     through <see cref="FocasStatusMapper"/>.
    /// </summary>
    /// <param name="address">The CNC memory address to read from.</param>
    /// <param name="type">The FOCAS data type to read.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A tuple of the boxed value read and the mapped OPC UA status code.</returns>
    Task<(object? value, uint status)> ReadAsync(
        FocasAddress address,
        FocasDataType type,
        CancellationToken cancellationToken);

    /// <summary>
    ///     Write <paramref name="value"/> to <paramref name="address"/>. Returns the mapped
    ///     OPC UA status (0 = Good).
    /// </summary>
    /// <param name="address">The CNC memory address to write to.</param>
    /// <param name="type">The FOCAS data type to write.</param>
    /// <param name="value">The value to write.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The mapped OPC UA status code (0 = Good).</returns>
    Task<uint> WriteAsync(
        FocasAddress address,
        FocasDataType type,
        object? value,
        CancellationToken cancellationToken);

    /// <summary>
    ///     Cheap health probe — e.g. <c>cnc_rdcncstat</c>. Returns <c>true</c> when the CNC
    ///     responds with any valid status.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns><c>true</c> when the CNC responds with a valid status; otherwise <c>false</c>.</returns>
    Task<bool> ProbeAsync(CancellationToken cancellationToken);

    /// <summary>
    ///     Read active alarm messages from the CNC via <c>cnc_rdalmmsg2</c>. Returns
    ///     zero-or-more active alarms. Null / empty list means "no alarms currently
    ///     active". IAlarmSource projection polls this at a configurable interval +
    ///     emits transitions (raise / clear) on the driver's <c>OnAlarmEvent</c>.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The list of currently active alarms; empty when none are active.</returns>
    Task<IReadOnlyList<FocasActiveAlarm>> ReadAlarmsAsync(CancellationToken cancellationToken);

    // ---- Fixed-tree T1 (identity + axis discovery + fast-poll dynamic bundle) ----

    /// <summary>
    ///     Read CNC identity via <c>cnc_sysinfo</c>. Populates the <c>Identity/*</c>
    ///     subtree of the fixed-node surface. Callable once at session open; the
    ///     values don't change across the session.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The CNC identity snapshot.</returns>
    Task<FocasSysInfo> GetSysInfoAsync(CancellationToken cancellationToken);

    /// <summary>
    ///     Read the CNC's configured axis names via <c>cnc_rdaxisname</c>. The driver
    ///     uses these to build the <c>Axes/{name}/</c> subtree and to index
    ///     <see cref="ReadDynamicAsync"/> calls.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The CNC's configured axis names.</returns>
    Task<IReadOnlyList<FocasAxisName>> GetAxisNamesAsync(CancellationToken cancellationToken);

    /// <summary>
    ///     Read the CNC's configured spindle names via <c>cnc_rdspdlname</c>. Drives
    ///     the <c>Spindle/{name}/</c> subtree.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The CNC's configured spindle names.</returns>
    Task<IReadOnlyList<FocasSpindleName>> GetSpindleNamesAsync(CancellationToken cancellationToken);

    /// <summary>
    ///     Read the fast-poll dynamic bundle for one axis via <c>cnc_rddynamic2</c>.
    ///     Returns the current position quadruple (absolute / machine / relative /
    ///     distance-to-go) plus actual feed rate + actual spindle speed + alarm
    ///     flags + program / sequence numbers — one network round-trip per call.
    /// </summary>
    /// <param name="axisIndex">The axis index to read dynamics for.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The fast-poll dynamic bundle for the requested axis.</returns>
    Task<FocasDynamicSnapshot> ReadDynamicAsync(int axisIndex, CancellationToken cancellationToken);

    // ---- Fixed-tree T2 (program + operation mode) ----

    /// <summary>
    ///     Aggregate program + operation-mode snapshot. One wire round-trip per
    ///     underlying FWLIB call — <c>cnc_rdblkcount</c>, <c>cnc_exeprgname2</c>,
    ///     <c>cnc_rdopmode</c>. The driver polls this on a slower cadence than
    ///     <see cref="ReadDynamicAsync"/> since program / mode transitions happen
    ///     on human-operator timescales.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The aggregate program + operation-mode snapshot.</returns>
    Task<FocasProgramInfo> GetProgramInfoAsync(CancellationToken cancellationToken);

    // ---- Fixed-tree T3 (timers) ----

    /// <summary>
    ///     Read one CNC cumulative timer. Kind selects PowerOn / Operating / Cutting /
    ///     Cycle. Values are seconds — the managed side already converted the native
    ///     minute+msec representation so downstream nodes display uniform units.
    /// </summary>
    /// <param name="kind">The timer kind to read.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The requested cumulative timer reading.</returns>
    Task<FocasTimer> GetTimerAsync(FocasTimerKind kind, CancellationToken cancellationToken);

    // ---- Fixed-tree T3.5 (servo meters) ----

    /// <summary>
    ///     Read the servo-load meter percentages across all configured axes.
    ///     Values are percentages (scaled by <c>10^Dec</c>). Empty list on a
    ///     disconnected session or unsupported CNC.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The servo-load meter percentages across all configured axes.</returns>
    Task<IReadOnlyList<FocasServoLoad>> GetServoLoadsAsync(CancellationToken cancellationToken);

    // ---- Fixed-tree T3.6 (spindle meters) ----

    /// <summary>
    ///     Read per-spindle load percentages. Result list index corresponds to
    ///     spindle index from <see cref="GetSpindleNamesAsync"/>. Empty list on a
    ///     disconnected session or when the CNC doesn't support the call (older
    ///     series like 16i may return EW_FUNC).
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>Per-spindle load percentages, indexed as per <see cref="GetSpindleNamesAsync"/>.</returns>
    Task<IReadOnlyList<int>> GetSpindleLoadsAsync(CancellationToken cancellationToken);

    /// <summary>
    ///     Read per-spindle maximum RPM values. Static configuration, fetched once at
    ///     bootstrap. Index alignment as per <see cref="GetSpindleLoadsAsync"/>.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>Per-spindle maximum RPM values, indexed as per <see cref="GetSpindleLoadsAsync"/>.</returns>
    Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken);

    /// <summary>
    ///     Read the per-axis position decimal-place figure via <c>cnc_getfigure</c>. The returned
    ///     list is parallel to <see cref="GetAxisNamesAsync"/> (index = axis). An EMPTY list means the
    ///     CNC/backend does not report figures — the driver then falls back to the configured
    ///     <c>PositionDecimalPlaces</c>. Values are clamped non-negative.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>Per-axis position decimal-place figures; empty when the CNC/backend doesn't report them.</returns>
    Task<IReadOnlyList<int>> GetPositionFiguresAsync(CancellationToken cancellationToken);
}

/// <summary>One servo-meter entry — one axis's current load percentage.</summary>
public sealed record FocasServoLoad(string AxisName, double LoadPercent);

/// <summary>Which cumulative counter <see cref="IFocasClient.GetTimerAsync"/> reads.</summary>
public enum FocasTimerKind
{
    /// <summary>Machine power-on hours — resets never.</summary>
    PowerOn = 0,
    /// <summary>Cycle operating time — resets when the operator clears the counter.</summary>
    Operating = 1,
    /// <summary>Cutting time — only counts while in cutting feed.</summary>
    Cutting = 2,
    /// <summary>Cycle time since the last program start.</summary>
    Cycle = 3,
}

/// <summary>One cumulative timer reading. <see cref="TotalSeconds"/> is the canonical unit.</summary>
public sealed record FocasTimer(FocasTimerKind Kind, int Minutes, int Milliseconds)
{
    /// <summary>Cumulative time in seconds — <c>Minutes * 60 + Milliseconds / 1000</c>.</summary>
    public double TotalSeconds => Minutes * 60.0 + Milliseconds / 1000.0;
}

/// <summary>
///     CNC identity snapshot from <c>cnc_sysinfo</c>. Strings are trimmed ASCII.
/// </summary>
public sealed record FocasSysInfo(
    int AddInfo,
    int MaxAxis,
    string CncType,   // "M" (mill) / "T" (lathe)
    string MtType,
    string Series,    // e.g. "30i"
    string Version,   // e.g. "A1.0"
    int AxesCount);

/// <summary>One configured axis name (e.g. "X", "X1").</summary>
public sealed record FocasAxisName(string Name, string Suffix)
{
    /// <summary>
    ///     Display name — name + suffix concatenated, trimmed. Empty suffix yields
    ///     just the name (the common case on single-channel CNCs).
    /// </summary>
    public string Display => string.IsNullOrEmpty(Suffix) ? Name : $"{Name}{Suffix}";
}

/// <summary>One configured spindle name (e.g. "S1").</summary>
public sealed record FocasSpindleName(string Name, string Suffix1, string Suffix2, string Suffix3)
{
    /// <summary>Gets the display name — name + concatenated suffixes, trimmed.</summary>
    public string Display
    {
        get
        {
            var s = Name + Suffix1 + Suffix2 + Suffix3;
            return s.TrimEnd('\0', ' ');
        }
    }
}

/// <summary>
///     Fast-poll bundle for one axis. Position values are scaled integers; the caller
///     divides by <c>10^DecimalPlaces</c> to get the decimal value. DecimalPlaces is
///     supplied by the caller — either via device config (<c>PositionDecimalPlaces</c>) or
///     the per-axis <see cref="IFocasClient.GetPositionFiguresAsync"/> (<c>cnc_getfigure</c>) path.
/// </summary>
/// <summary>
///     Program + operation-mode snapshot. Name is the currently-executing
///     program filename (e.g. "O0001.NC"); ONumber is its Fanuc O-number (1-9999).
///     Mode is the numeric code from <c>cnc_rdopmode</c> — see <see cref="FocasOpMode"/>.
/// </summary>
public sealed record FocasProgramInfo(
    string Name,
    int ONumber,
    int BlockCount,
    int Mode);

/// <summary>Human-readable text for the <see cref="FocasProgramInfo.Mode"/> integer.</summary>
public static class FocasOpMode
{
    /// <summary>Converts a numeric operation mode to its text representation.</summary>
    /// <param name="mode">The operation mode integer code.</param>
    /// <returns>The text representation of the mode, or a fallback format if unknown.</returns>
    public static string ToText(int mode) => mode switch
    {
        0 => "MDI",
        1 => "AUTO",
        2 => "TJOG",
        3 => "EDIT",
        4 => "HANDLE",
        5 => "JOG",
        6 => "TEACH_IN_HANDLE",
        7 => "REFERENCE",
        8 => "REMOTE",
        9 => "TEST",
        _ => $"Mode{mode}",
    };
}

public sealed record FocasDynamicSnapshot(
    int AxisIndex,
    int AlarmFlags,
    int ProgramNumber,
    int MainProgramNumber,
    int SequenceNumber,
    int ActualFeedRate,
    int ActualSpindleSpeed,
    int AbsolutePosition,
    int MachinePosition,
    int RelativePosition,
    int DistanceToGo);

/// <summary>
///     One active alarm surfaced by <see cref="IFocasClient.ReadAlarmsAsync"/>. Shape
///     mirrors <c>ODBALMMSG2</c> but normalises the message bytes to a .NET string.
/// </summary>
public sealed record FocasActiveAlarm(
    int AlarmNumber,
    short Type,
    short Axis,
    string Message);

/// <summary>Factory for <see cref="IFocasClient"/>s. One client per configured device.</summary>
public interface IFocasClientFactory
{
    /// <summary>Creates a new FOCAS client instance.</summary>
    /// <returns>A new IFocasClient instance.</returns>
    IFocasClient Create();

    /// <summary>
    ///     Config-time usability probe called by <see cref="FocasDriver.InitializeAsync"/> before
    ///     any background loops start. Must NOT create a live wire client. Implementations that
    ///     are permanently non-functional (e.g. <see cref="UnimplementedFocasClientFactory"/>)
    ///     throw <see cref="NotSupportedException"/> here so the driver faults immediately at
    ///     init rather than lazily on the first read.
    /// </summary>
    /// <exception cref="NotSupportedException">
    ///     Thrown by stub/unimplemented backends to force a fail-fast init fault.
    /// </exception>
    void EnsureUsable();
}

/// <summary>
///     Scaffolding factory — throws on <see cref="Create"/> so a DriverInstance row can be
///     seeded ahead of the CNC endpoint being reachable without silently reading stale data.
///     Select via <c>"Backend": "unimplemented"</c> in driver config. Flip to
///     <c>"Backend": "wire"</c> once the CNC is provisioned.
/// </summary>
public sealed class UnimplementedFocasClientFactory : IFocasClientFactory
{
    private const string Message =
        "FOCAS driver backend is 'unimplemented'. Switch to 'Backend: \"wire\"' in driver config " +
        "once the CNC is provisioned — see docs/drivers/FOCAS.md.";

    /// <inheritdoc />
    public void EnsureUsable() => throw new NotSupportedException(Message);

    /// <inheritdoc />
    public IFocasClient Create() => throw new NotSupportedException(Message);
}

/// <summary>
///     Well-known FOCAS alarm types from <c>fwlib32.h</c> <c>ALM_TYPE_*</c>. Narrow subset —
///     the full list is ~15 types per model; these cover the universally-present categories.
/// </summary>
/// <remarks>
///     Constants are typed <see cref="short"/> so they match the wire field width on
///     <c>cnc_rdalmmsg2</c> (and so <see cref="FocasAlarmProjection"/>'s <c>switch (short)</c>
///     statements compile against a matching type rather than relying on implicit int→short
///     narrowing on the constants).
/// </remarks>
public static class FocasAlarmType
{
    /// <summary>Pass to <see cref="IFocasClient.ReadAlarmsAsync"/>-equivalent to mean "any type".</summary>
    public const short All = -1;
    public const short Parameter = 0;   // ALM_P
    public const short PulseCode = 1;   // ALM_Y (servo)
    public const short Overtravel = 2;  // ALM_O
    public const short Overheat = 3;    // ALM_H
    public const short Servo = 4;       // ALM_S
    public const short DataIo = 5;      // ALM_T
    public const short MemoryCheck = 6; // ALM_M
    public const short MacroAlarm = 13; // ALM_MC
}
