using MessagePack;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;

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

/// <summary>
///     <see cref="IFocasClient"/> implementation that forwards every operation over a
///     <see cref="FocasIpcClient"/> to a <c>Driver.FOCAS.Host</c> process. Keeps the
///     <c>Fwlib32.dll</c> P/Invoke out of the main server process so a native crash
///     blast-radius stops at the Host boundary.
/// </summary>
/// <remarks>
///     Session lifecycle: <see cref="ConnectAsync"/> sends <c>OpenSessionRequest</c> and
///     caches the returned <c>SessionId</c>. Subsequent <see cref="ReadAsync"/> /
///     <see cref="WriteAsync"/> / <see cref="ProbeAsync"/> calls thread that session id
///     onto each request DTO. <see cref="Dispose"/> sends <c>CloseSessionRequest</c> +
///     disposes the underlying pipe.
/// </remarks>
public sealed class IpcFocasClient : IFocasClient
{
    private readonly FocasIpcClient _ipc;
    private readonly FocasCncSeries _series;
    private long _sessionId;
    private bool _connected;

    public IpcFocasClient(FocasIpcClient ipc, FocasCncSeries series = FocasCncSeries.Unknown)
    {
        _ipc = ipc ?? throw new ArgumentNullException(nameof(ipc));
        _series = series;
    }

    public bool IsConnected => _connected;

    public async Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken)
    {
        if (_connected) return;

        var resp = await _ipc.CallAsync<OpenSessionRequest, OpenSessionResponse>(
            FocasMessageKind.OpenSessionRequest,
            new OpenSessionRequest
            {
                HostAddress = $"{address.Host}:{address.Port}",
                TimeoutMs = (int)Math.Max(1, timeout.TotalMilliseconds),
                CncSeries = (int)_series,
            },
            FocasMessageKind.OpenSessionResponse,
            cancellationToken).ConfigureAwait(false);

        if (!resp.Success)
            throw new InvalidOperationException(
                $"FOCAS Host rejected OpenSession for {address}: {resp.ErrorCode ?? "?"} — {resp.Error}");

        _sessionId = resp.SessionId;
        _connected = true;
    }

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

        var resp = await _ipc.CallAsync<ReadRequest, ReadResponse>(
            FocasMessageKind.ReadRequest,
            new ReadRequest
            {
                SessionId = _sessionId,
                Address = ToDto(address),
                DataType = (int)type,
            },
            FocasMessageKind.ReadResponse,
            cancellationToken).ConfigureAwait(false);

        if (!resp.Success) return (null, resp.StatusCode);

        var value = DecodeValue(resp.ValueBytes, resp.ValueTypeCode);
        return (value, resp.StatusCode);
    }

    public async Task<uint> WriteAsync(
        FocasAddress address, FocasDataType type, object? value, CancellationToken cancellationToken)
    {
        if (!_connected) return FocasStatusMapper.BadCommunicationError;

        // PMC bit writes get the first-class RMW frame so the critical section stays on the Host.
        if (address.Kind == FocasAreaKind.Pmc && type == FocasDataType.Bit && address.BitIndex is int bit)
        {
            var bitResp = await _ipc.CallAsync<PmcBitWriteRequest, PmcBitWriteResponse>(
                FocasMessageKind.PmcBitWriteRequest,
                new PmcBitWriteRequest
                {
                    SessionId = _sessionId,
                    Address = ToDto(address),
                    BitIndex = bit,
                    Value = Convert.ToBoolean(value),
                },
                FocasMessageKind.PmcBitWriteResponse,
                cancellationToken).ConfigureAwait(false);
            return bitResp.StatusCode;
        }

        var resp = await _ipc.CallAsync<WriteRequest, WriteResponse>(
            FocasMessageKind.WriteRequest,
            new WriteRequest
            {
                SessionId = _sessionId,
                Address = ToDto(address),
                DataType = (int)type,
                ValueTypeCode = (int)type,
                ValueBytes = EncodeValue(value, type),
            },
            FocasMessageKind.WriteResponse,
            cancellationToken).ConfigureAwait(false);

        return resp.StatusCode;
    }

    public async Task<bool> ProbeAsync(CancellationToken cancellationToken)
    {
        if (!_connected) return false;
        try
        {
            var resp = await _ipc.CallAsync<ProbeRequest, ProbeResponse>(
                FocasMessageKind.ProbeRequest,
                new ProbeRequest { SessionId = _sessionId },
                FocasMessageKind.ProbeResponse,
                cancellationToken).ConfigureAwait(false);
            return resp.Healthy;
        }
        catch { return false; }
    }

    public void Dispose()
    {
        if (_connected)
        {
            try
            {
                _ipc.SendOneWayAsync(FocasMessageKind.CloseSessionRequest,
                    new CloseSessionRequest { SessionId = _sessionId }, CancellationToken.None)
                    .GetAwaiter().GetResult();
            }
            catch { /* best effort */ }
            _connected = false;
        }
        _ipc.DisposeAsync().AsTask().GetAwaiter().GetResult();
    }

    private static FocasAddressDto ToDto(FocasAddress addr) => new()
    {
        Kind = (int)addr.Kind,
        PmcLetter = addr.PmcLetter,
        Number = addr.Number,
        BitIndex = addr.BitIndex,
    };

    private static byte[]? EncodeValue(object? value, FocasDataType type)
    {
        if (value is null) return null;
        return type switch
        {
            FocasDataType.Bit     => MessagePackSerializer.Serialize(Convert.ToBoolean(value)),
            FocasDataType.Byte    => MessagePackSerializer.Serialize(Convert.ToByte(value)),
            FocasDataType.Int16   => MessagePackSerializer.Serialize(Convert.ToInt16(value)),
            FocasDataType.Int32   => MessagePackSerializer.Serialize(Convert.ToInt32(value)),
            FocasDataType.Float32 => MessagePackSerializer.Serialize(Convert.ToSingle(value)),
            FocasDataType.Float64 => MessagePackSerializer.Serialize(Convert.ToDouble(value)),
            FocasDataType.String  => MessagePackSerializer.Serialize(Convert.ToString(value) ?? string.Empty),
            _ => MessagePackSerializer.Serialize(Convert.ToInt32(value)),
        };
    }

    private static object? DecodeValue(byte[]? bytes, int typeCode)
    {
        if (bytes is null) return null;
        return typeCode switch
        {
            FocasDataTypeCode.Bit     => MessagePackSerializer.Deserialize<bool>(bytes),
            FocasDataTypeCode.Byte    => MessagePackSerializer.Deserialize<byte>(bytes),
            FocasDataTypeCode.Int16   => MessagePackSerializer.Deserialize<short>(bytes),
            FocasDataTypeCode.Int32   => MessagePackSerializer.Deserialize<int>(bytes),
            FocasDataTypeCode.Float32 => MessagePackSerializer.Deserialize<float>(bytes),
            FocasDataTypeCode.Float64 => MessagePackSerializer.Deserialize<double>(bytes),
            FocasDataTypeCode.String  => MessagePackSerializer.Deserialize<string>(bytes),
            _ => MessagePackSerializer.Deserialize<int>(bytes),
        };
    }
}

/// <summary>
///     Factory producing <see cref="IpcFocasClient"/>s. One pipe connection per
///     <c>IFocasClient</c> — matches the driver's one-client-per-device invariant. The
///     deployment wires this into the DI container in place of
///     <see cref="UnimplementedFocasClientFactory"/>.
/// </summary>
public sealed class IpcFocasClientFactory(Func<FocasIpcClient> ipcClientFactory, FocasCncSeries series = FocasCncSeries.Unknown)
    : IFocasClientFactory
{
    public IFocasClient Create() => new IpcFocasClient(ipcClientFactory(), series);
}
