using Microsoft.Extensions.Logging;

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

/// <summary>
///     <see cref="IFocasClient"/> implementation backed by the in-tree managed
///     <see cref="FocasWireClient"/>. No P/Invoke, no <c>Fwlib64.dll</c>, no out-of-process
///     Host — the wire client dials the CNC on TCP:8193 directly and speaks the FOCAS/2
///     Ethernet binary protocol.
/// </summary>
/// <remarks>
///     OtOpcUa is read-only against FOCAS. <see cref="WriteAsync"/> returns
///     <see cref="FocasStatusMapper.BadNotWritable"/> for every address — the managed wire
///     client intentionally does not expose <c>cnc_wrparam</c> / <c>pmc_wrpmcrng</c> /
///     <c>cnc_wrmacro</c>.
/// </remarks>
public sealed class WireFocasClient : IFocasClient
{
    private readonly FocasWireClient _wire;
    private FocasHostAddress? _address;

    /// <summary>
    ///     Default constructor — wire client without logger. Selected by the legacy
    ///     no-arg <see cref="WireFocasClientFactory.Create"/> path.
    /// </summary>
    public WireFocasClient() : this(logger: null) { }

    /// <summary>
    ///     Construct with an optional logger. Threaded through to
    ///     <see cref="FocasWireClient"/> so the per-response Debug entries actually reach
    ///     the host's logging pipeline.
    /// </summary>
    /// <param name="logger">Optional logger for debug output from wire client responses.</param>
    public WireFocasClient(ILogger<FocasWireClient>? logger)
    {
        _wire = new FocasWireClient(logger);
    }

    /// <inheritdoc />
    public bool IsConnected => _wire.IsConnected;

    /// <inheritdoc />
    public async Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken)
    {
        if (_wire.IsConnected) return;
        _address = address;
        // FocasWireClient.ConnectAsync interprets TimeSpan.Zero as "no timeout" — clamp the
        // driver's default TimeSpan to at least 1s so a caller passing TimeSpan.Zero gets a
        // sane fail-fast instead of hanging indefinitely.
        var effective = timeout <= TimeSpan.Zero ? TimeSpan.FromSeconds(1) : timeout;
        await _wire.ConnectAsync(address.Host, address.Port, effective, cancellationToken).ConfigureAwait(false);
    }

    /// <inheritdoc />
    public async Task<(object? value, uint status)> ReadAsync(
        FocasAddress address, FocasDataType type, CancellationToken cancellationToken)
    {
        if (!_wire.IsConnected) return (null, FocasStatusMapper.BadCommunicationError);
        cancellationToken.ThrowIfCancellationRequested();

        return address.Kind switch
        {
            FocasAreaKind.Pmc => await ReadPmcAsync(address, type, cancellationToken).ConfigureAwait(false),
            FocasAreaKind.Parameter => await ReadParameterAsync(address, type, cancellationToken).ConfigureAwait(false),
            FocasAreaKind.Macro => await ReadMacroAsync(address, cancellationToken).ConfigureAwait(false),
            _ => (null, FocasStatusMapper.BadNotSupported),
        };
    }

    /// <inheritdoc />
    public Task<uint> WriteAsync(
        FocasAddress address, FocasDataType type, object? value, CancellationToken cancellationToken)
        => Task.FromResult(FocasStatusMapper.BadNotWritable);

    /// <inheritdoc />
    public async Task<bool> ProbeAsync(CancellationToken cancellationToken)
    {
        if (!_wire.IsConnected) return false;
        try
        {
            var result = await _wire.ReadStatusAsync(cancellationToken).ConfigureAwait(false);
            return result.IsOk;
        }
        catch (FocasWireException)
        {
            return false;
        }
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<FocasActiveAlarm>> ReadAlarmsAsync(CancellationToken cancellationToken)
    {
        if (!_wire.IsConnected) return [];
        try
        {
            var result = await _wire.ReadAlarmsAsync(FocasAlarmType.All, 32, cancellationToken).ConfigureAwait(false);
            if (!result.IsOk || result.Value is null) return [];
            return result.Value.Select(Map).ToList();
        }
        catch (FocasWireException)
        {
            return [];
        }

        static FocasActiveAlarm Map(WireAlarm a) => new(
            AlarmNumber: a.AlarmNumber,
            Type: a.Type,
            Axis: a.Axis,
            Message: a.Message ?? string.Empty);
    }

    /// <inheritdoc />
    public async Task<FocasSysInfo> GetSysInfoAsync(CancellationToken cancellationToken)
    {
        RequireConnected();
        var result = await _wire.ReadSysInfoAsync(cancellationToken).ConfigureAwait(false);
        ThrowIfRcNonZero(result.Rc, "cnc_sysinfo", result.IsOk);
        var info = result.Value!;
        // Fanuc right-pads the ASCII axis count with spaces; fall back to MaxAxis if the
        // text field isn't interpretable as an integer.
        var axesCount = int.TryParse(info.Axes?.Trim(), out var parsed) ? parsed : info.MaxAxis;
        return new FocasSysInfo(
            AddInfo: info.AddInfo,
            MaxAxis: info.MaxAxis,
            CncType: info.CncType ?? string.Empty,
            MtType: info.MachineType ?? string.Empty,
            Series: info.Series ?? string.Empty,
            Version: info.Version ?? string.Empty,
            AxesCount: axesCount);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<FocasAxisName>> GetAxisNamesAsync(CancellationToken cancellationToken)
    {
        if (!_wire.IsConnected) return [];
        var result = await _wire.ReadAxisNamesAsync(32, cancellationToken).ConfigureAwait(false);
        if (!result.IsOk || result.Value is null) return [];
        return result.Value.Select(SplitAxis).Where(n => n.Name.Length > 0).ToList();

        // FocasWireClient returns axis records as a single Name string (e.g. "X" or "X1").
        // IFocasClient wants Name + Suffix split — the first char is the axis letter, the
        // rest is the multi-channel suffix.
        static FocasAxisName SplitAxis(WireAxisRecord r)
        {
            var n = r.Name ?? string.Empty;
            return n.Length == 0
                ? new FocasAxisName(string.Empty, string.Empty)
                : new FocasAxisName(n[..1], n.Length > 1 ? n[1..] : string.Empty);
        }
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<FocasSpindleName>> GetSpindleNamesAsync(CancellationToken cancellationToken)
    {
        if (!_wire.IsConnected) return [];
        var result = await _wire.ReadSpindleNamesAsync(8, cancellationToken).ConfigureAwait(false);
        if (!result.IsOk || result.Value is null) return [];
        return result.Value.Select(SplitSpindle).Where(n => n.Name.Length > 0).ToList();

        static FocasSpindleName SplitSpindle(WireSpindleRecord r)
        {
            var n = r.Name ?? string.Empty;
            return new FocasSpindleName(
                Name: n.Length > 0 ? n[..1] : string.Empty,
                Suffix1: n.Length > 1 ? n[1..2] : string.Empty,
                Suffix2: n.Length > 2 ? n[2..3] : string.Empty,
                Suffix3: n.Length > 3 ? n[3..4] : string.Empty);
        }
    }

    /// <inheritdoc />
    public async Task<FocasDynamicSnapshot> ReadDynamicAsync(int axisIndex, CancellationToken cancellationToken)
    {
        // FOCAS axes are 1-based; cnc_rddynamic2 with axis 0 returns EW_4 (live-confirmed on the
        // 31i-B). The FixedTree poll already iterates 1..AxesCount, but enforce the contract so a
        // future caller can't silently request axis 0. 2026-06-25.
        if (axisIndex < 1)
            throw new ArgumentOutOfRangeException(nameof(axisIndex), axisIndex, "FOCAS axis index is 1-based.");
        RequireConnected();
        var result = await _wire.ReadDynamic2Async((short)axisIndex, cancellationToken).ConfigureAwait(false);
        ThrowIfRcNonZero(result.Rc, "cnc_rddynamic2", result.IsOk);
        var d = result.Value!;
        var pos = d.Axis ?? new WireAxisPosition(0, 0, 0, 0);
        return new FocasDynamicSnapshot(
            AxisIndex: axisIndex,
            AlarmFlags: d.Alarm,
            ProgramNumber: d.ProgramNumber,
            MainProgramNumber: d.MainProgramNumber,
            SequenceNumber: d.SequenceNumber,
            ActualFeedRate: d.FeedRate,
            ActualSpindleSpeed: d.SpindleSpeed,
            AbsolutePosition: pos.Absolute,
            MachinePosition: pos.Machine,
            RelativePosition: pos.Relative,
            DistanceToGo: pos.Distance);
    }

    /// <inheritdoc />
    public async Task<FocasProgramInfo> GetProgramInfoAsync(CancellationToken cancellationToken)
    {
        RequireConnected();
        var nameResult = await _wire.ReadExecutingProgramNameAsync(cancellationToken).ConfigureAwait(false);
        var blkResult = await _wire.ReadBlockCountAsync(cancellationToken).ConfigureAwait(false);
        // Use the raw short variant — FocasProgramInfo.Mode stores the integer code so the
        // managed ToText path in FocasOpMode can map it for display.
        var modeResult = await _wire.ReadOperationModeCodeAsync(cancellationToken).ConfigureAwait(false);

        // The fixed-tree bootstrap probe (FocasDriver.SafeTryProbe) classifies the
        // ProgramInfo capability as "supported" iff this method returns non-null. A CNC
        // series without cnc_exeprgname2 / cnc_rdopmode answers EW_FUNC / EW_NOOPT, so
        // throw when neither the program-name nor the op-mode read succeeded — otherwise
        // SafeTryProbe records a false-positive capability and the driver emits Program/
        // OperationMode/ subtrees that only ever return BadDeviceFailure.
        if (!nameResult.IsOk && !modeResult.IsOk)
            throw new InvalidOperationException(
                $"cnc_exeprgname2 failed EW_{nameResult.Rc} and cnc_rdopmode failed EW_{modeResult.Rc}.");

        var wireName = nameResult.Value;
        return new FocasProgramInfo(
            Name: wireName?.Name ?? string.Empty,
            ONumber: wireName?.ONumber ?? 0,
            BlockCount: blkResult.IsOk ? blkResult.Value : 0,
            Mode: modeResult.IsOk ? modeResult.Value : 0);
    }

    /// <inheritdoc />
    public async Task<FocasTimer> GetTimerAsync(FocasTimerKind kind, CancellationToken cancellationToken)
    {
        RequireConnected();
        var result = await _wire.ReadTimerAsync((short)kind, cancellationToken).ConfigureAwait(false);
        ThrowIfRcNonZero(result.Rc, $"cnc_rdtimer kind={kind}", result.IsOk);
        var t = result.Value!;
        return new FocasTimer(kind, t.Minutes, t.Milliseconds);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<FocasServoLoad>> GetServoLoadsAsync(CancellationToken cancellationToken)
    {
        if (!_wire.IsConnected) return [];
        var result = await _wire.ReadServoMeterAsync(32, cancellationToken).ConfigureAwait(false);
        if (!result.IsOk || result.Value is null) return [];
        return result.Value
            .Select(m => new FocasServoLoad(m.Name ?? string.Empty, m.Value / Math.Pow(10.0, m.Decimal)))
            .Where(s => s.AxisName.Length > 0)
            .ToList();
    }

    /// <inheritdoc />
    public Task<IReadOnlyList<int>> GetSpindleLoadsAsync(CancellationToken cancellationToken) =>
        ReadSpindleMetricAsync((sel, ct) => _wire.ReadSpindleLoadAsync(sel, ct), cancellationToken);

    /// <inheritdoc />
    public Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken) =>
        ReadSpindleMetricAsync((sel, ct) => _wire.ReadSpindleMaxRpmAsync(sel, ct), cancellationToken);

    /// <inheritdoc />
    // Reads via FocasWireClient.ReadPositionFiguresAsync; returns an empty list when the read
    // fails (non-zero RC — e.g. the series lacks cnc_getfigure) or is not connected. Never
    // throws — figure reads degrade to the configured PositionDecimalPlaces rather than faulting.
    public async Task<IReadOnlyList<int>> GetPositionFiguresAsync(CancellationToken cancellationToken)
    {
        if (!_wire.IsConnected) return [];
        try
        {
            var result = await _wire.ReadPositionFiguresAsync(cancellationToken).ConfigureAwait(false);
            return result.IsOk && result.Value is { } figures ? figures : [];
        }
        catch (FocasWireException)
        {
            return [];
        }
    }

    private static async Task<IReadOnlyList<int>> ReadSpindleMetricAsync(
        Func<short, CancellationToken, Task<FocasResult<IReadOnlyList<WireSpindleMetric>>>> call,
        CancellationToken cancellationToken)
    {
        var result = await call(-1, cancellationToken).ConfigureAwait(false);
        if (!result.IsOk || result.Value is null) return [];
        var list = new List<int>();
        foreach (var m in result.Value)
        {
            // Fanuc pads unused spindle slots with 0 — stop at the first trailing zero so the
            // list length matches the configured spindle count.
            if (m.Value == 0 && list.Count > 0) break;
            list.Add(m.Value);
        }
        return list;
    }

    /// <summary>Disposes the wire client and releases all resources.</summary>
    public void Dispose() => _wire.Dispose();

    // ---- PMC / Parameter / Macro read paths ------------------------------------------

    private async Task<(object? value, uint status)> ReadPmcAsync(
        FocasAddress address, FocasDataType type, CancellationToken cancellationToken)
    {
        var area = FocasPmcAreaLookup.FromLetter(address.PmcLetter ?? string.Empty);
        if (area is null) return (null, FocasStatusMapper.BadNodeIdUnknown);
        var dataType = FocasPmcDataTypeLookup.FromFocasDataType(type);
        var start = (ushort)address.Number;
        // pmc_rdpmcrng returns (end-start+1) BYTES. A multi-byte slot (Word/Long/Real/Double) needs
        // the range widened to its byte width, else the value parser gets too few bytes → 0 values
        // → spurious BadOutOfRange (live-confirmed on the 31i-B: a Word read with end=start returned
        // a single byte). Bit/Byte stay width 1 so end==start. 2026-06-25.
        var end = (ushort)(start + FocasWireClient.PmcByteWidth((short)dataType) - 1);

        try
        {
            var result = await _wire.ReadPmcRangeAsync(area.Value, dataType, start, end, cancellationToken)
                .ConfigureAwait(false);
            if (!result.IsOk || result.Value is null)
                return (null, FocasStatusMapper.MapFocasReturn(result.Rc));
            var values = result.Value.Values;
            if (values.Count == 0) return (null, FocasStatusMapper.BadOutOfRange);
            var raw = values[0];
            var mapped = type switch
            {
                FocasDataType.Bit => (object)(((long)raw >> (address.BitIndex ?? 0) & 1L) != 0),
                FocasDataType.Byte => (object)(sbyte)(raw & 0xFFL),
                FocasDataType.Int16 => (object)(short)raw,
                FocasDataType.Int32 => (object)(int)raw,
                FocasDataType.Float32 => (object)BitConverter.Int32BitsToSingle((int)raw),
                FocasDataType.Float64 => (object)BitConverter.Int64BitsToDouble(raw),
                _ => (object)raw,
            };
            return (mapped, FocasStatusMapper.Good);
        }
        catch (FocasWireException ex)
        {
            return (null, ex.Rc is short rc ? FocasStatusMapper.MapFocasReturn(rc) : FocasStatusMapper.BadCommunicationError);
        }
    }

    private async Task<(object? value, uint status)> ReadParameterAsync(
        FocasAddress address, FocasDataType type, CancellationToken cancellationToken)
    {
        try
        {
            switch (type)
            {
                case FocasDataType.Byte:
                    var b = await _wire.ReadParameterByteAsync((short)address.Number, 0, cancellationToken).ConfigureAwait(false);
                    return b.IsOk ? ((object)(sbyte)b.Value, FocasStatusMapper.Good) : (null, FocasStatusMapper.MapFocasReturn(b.Rc));
                case FocasDataType.Int16:
                    var s = await _wire.ReadParameterInt16Async((short)address.Number, 0, cancellationToken).ConfigureAwait(false);
                    return s.IsOk ? ((object)s.Value, FocasStatusMapper.Good) : (null, FocasStatusMapper.MapFocasReturn(s.Rc));
                case FocasDataType.Float32:
                    var f = await _wire.ReadParameterFloat32Async((short)address.Number, 0, cancellationToken).ConfigureAwait(false);
                    return f.IsOk ? ((object)f.Value, FocasStatusMapper.Good) : (null, FocasStatusMapper.MapFocasReturn(f.Rc));
                case FocasDataType.Float64:
                    var d = await _wire.ReadParameterFloat64Async((short)address.Number, 0, cancellationToken).ConfigureAwait(false);
                    return d.IsOk ? ((object)d.Value, FocasStatusMapper.Good) : (null, FocasStatusMapper.MapFocasReturn(d.Rc));
                case FocasDataType.Bit when address.BitIndex is int bit:
                    var bi = await _wire.ReadParameterInt32Async((short)address.Number, 0, cancellationToken).ConfigureAwait(false);
                    if (!bi.IsOk) return (null, FocasStatusMapper.MapFocasReturn(bi.Rc));
                    return ((object)((bi.Value >> bit & 1) != 0), FocasStatusMapper.Good);
                default:
                    var i = await _wire.ReadParameterInt32Async((short)address.Number, 0, cancellationToken).ConfigureAwait(false);
                    return i.IsOk ? ((object)i.Value, FocasStatusMapper.Good) : (null, FocasStatusMapper.MapFocasReturn(i.Rc));
            }
        }
        catch (FocasWireException ex)
        {
            return (null, ex.Rc is short rc ? FocasStatusMapper.MapFocasReturn(rc) : FocasStatusMapper.BadCommunicationError);
        }
    }

    private async Task<(object? value, uint status)> ReadMacroAsync(
        FocasAddress address, CancellationToken cancellationToken)
    {
        try
        {
            var result = await _wire.ReadMacroAsync((short)address.Number, cancellationToken).ConfigureAwait(false);
            if (!result.IsOk || result.Value is null)
                return (null, FocasStatusMapper.MapFocasReturn(result.Rc));
            var m = result.Value;
            // Macro value is scaled-decimal: the real value is Value / 10^Decimal.
            var scaled = m.Value / Math.Pow(10.0, m.Decimal);
            return ((object)scaled, FocasStatusMapper.Good);
        }
        catch (FocasWireException ex)
        {
            return (null, ex.Rc is short rc ? FocasStatusMapper.MapFocasReturn(rc) : FocasStatusMapper.BadCommunicationError);
        }
    }

    private void RequireConnected()
    {
        if (!_wire.IsConnected)
            throw new InvalidOperationException("FOCAS wire session not connected.");
    }

    private static void ThrowIfRcNonZero(short rc, string call, bool isOk)
    {
        if (!isOk) throw new InvalidOperationException($"{call} failed EW_{rc}.");
    }
}

/// <summary>Factory producing <see cref="WireFocasClient"/> instances — one per configured device.</summary>
public sealed class WireFocasClientFactory : IFocasClientFactory
{
    private readonly ILogger<FocasWireClient>? _logger;

    /// <summary>Initializes a new instance of the WireFocasClientFactory without a logger.</summary>
    public WireFocasClientFactory() : this(logger: null) { }

    /// <summary>
    ///     Construct the factory with a logger that every created <see cref="WireFocasClient"/>
    ///     forwards to its <see cref="FocasWireClient"/> — the wire client already emits Debug
    ///     entries per FOCAS response, but the previous no-arg factory path discarded them.
    /// </summary>
    /// <param name="logger">Optional logger for debug output from wire client responses.</param>
    public WireFocasClientFactory(ILogger<FocasWireClient>? logger)
    {
        _logger = logger;
    }

    /// <inheritdoc />
    // No-op — the wire backend is always usable at config time.
    public void EnsureUsable() { }

    /// <inheritdoc />
    public IFocasClient Create() => new WireFocasClient(_logger);
}
