using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ConeCalorimeter.Services;
using Serilog;

namespace ConeCalorimeter.ViewModels;

public sealed class CValueCalibrationViewModel : PageViewModel
{
    private const ushort BaselineOxygenRegister = 268;
    private const ushort TemperatureRegister = 282;
    private const ushort PressureDifferenceRegister = 284;
    private const ushort CalibrationOxygenRegister = 286;
    private const ushort CValueRegister = 308;
    private const double OxygenMinimum = 0;
    private const double OxygenMaximum = 30;
    private const double TemperatureMinimum = 200;
    private const double TemperatureMaximum = 1500;
    private const double PressureDifferenceMinimum = -5000;
    private const double PressureDifferenceMaximum = 5000;
    private const double CValueMinimum = 0;
    private const double CValueMaximum = 1000;
    private const double FractionalOxygenMaximum = 0.3;
    private const double PlausibleOxygenMinimum = 1;
    private const double DisplayZeroTolerance = 0.00005;
    private const string FourDecimalDisplayFormat = "0.0000";
    private const ushort CirculatingWaterCoil = 49;
    private const ushort SamplingPumpCoil = 50;
    private const ushort IgniterCoil = 53;
    private const ushort FanCoil = 54;
    private const ushort MethaneValveCoil = 55;
    private const ushort BaselineCollectionCoil = 60;
    private const ushort BaselineEndCoil = 62;
    private const ushort CalibrationStartCoil = 70;
    private const ushort CalibrationEndCoil = 72;
    private static readonly TimeSpan PairedActionPulseDuration = TimeSpan.FromMilliseconds(300);
    private const string CirculatingWaterAction = "循环水";
    private const string SamplingPumpAction = "取样泵";
    private const string IgniterAction = "点火器";
    private const string FanAction = "风机";
    private const string MethaneValveAction = "甲烷阀";
    private const string BaselineCollectionAction = "基线采集";
    private const string CalibrationStartAction = "标定开始";
    private static readonly TimeSpan DiagnosticLogInterval = TimeSpan.FromSeconds(5);
    private static readonly string DiagnosticLogFilePath = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "ConeCalorimeter",
        "Logs",
        "cvalue-calibration-diagnostics.log");
    private static readonly ModbusFloatByteOrder[] FloatByteOrders =
    [
        ModbusFloatByteOrder.Abcd,
        ModbusFloatByteOrder.Cdab,
        ModbusFloatByteOrder.Badc,
        ModbusFloatByteOrder.Dcba
    ];

    private readonly Action _closeAction;
    private readonly Action _helpAction;
    private readonly Action _baselineCollectionCompletedAction;
    private readonly Action _calibrationCompletedAction;
    private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
    private readonly DispatcherTimer _refreshTimer;
    private readonly DispatcherTimer _pairedActionCompletionTimer;
    private readonly Dictionary<string, CValueCalibrationActionViewModel> _actionsByLabel = [];
    private readonly Dictionary<string, PairedActionWaitState> _pairedActionWaitStates = [];
    private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
    private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];
    private readonly Dictionary<ushort, string> _lastFileDiagnosticByRegister = [];
    private readonly Dictionary<ushort, DateTime> _lastFileDiagnosticAtByRegister = [];
    private ModbusFloatByteOrder? _calibrationFloatByteOrder;
    private string _baselineOxygenText = "";
    private string _temperatureText = "";
    private string _pressureDifferenceText = "";
    private string _calibrationOxygenText = "";
    private string _cValueText = "";
    private string _lastAction = "待机";

    public CValueCalibrationViewModel(
        Action closeAction,
        Action helpAction,
        Action baselineCollectionCompletedAction,
        Action calibrationCompletedAction,
        ITcpDeviceConnectionService tcpDeviceConnectionService) : base("C值标定")
    {
        _closeAction = closeAction;
        _helpAction = helpAction;
        _baselineCollectionCompletedAction = baselineCollectionCompletedAction;
        _calibrationCompletedAction = calibrationCompletedAction;
        _tcpDeviceConnectionService = tcpDeviceConnectionService;
        CloseCommand = new RelayCommand(_closeAction);
        HelpCommand = new RelayCommand(_helpAction);
        ActionCommand = new RelayCommand<string>(ExecuteAction);

        TopActions =
        [
            CreateAction(CirculatingWaterAction, CirculatingWaterAction, CirculatingWaterAction, showStateText: false),
            CreateAction(MethaneValveAction, MethaneValveAction, MethaneValveAction, showStateText: false),
            CreateAction(FanAction, FanAction, FanAction, showStateText: false),
            CreateAction(IgniterAction, IgniterAction, IgniterAction, showStateText: false),
            CreateAction(SamplingPumpAction, SamplingPumpAction, SamplingPumpAction, showStateText: false)
        ];

        BottomActions =
        [
            CreateAction(CalibrationStartAction, CalibrationStartAction, "标定结束"),
            CreateAction(BaselineCollectionAction, BaselineCollectionAction, "基线结束")
        ];
        _actionsByLabel[CalibrationStartAction].UpdateStatus(false);
        _actionsByLabel[BaselineCollectionAction].UpdateStatus(false);
        _pairedActionWaitStates[CalibrationStartAction] = new PairedActionWaitState(
            CalibrationEndCoil,
            "标定中",
            "标定完成",
            _calibrationCompletedAction);
        _pairedActionWaitStates[BaselineCollectionAction] = new PairedActionWaitState(
            BaselineEndCoil,
            "基线采集中",
            "基线采集完成",
            _baselineCollectionCompletedAction);

        _pairedActionCompletionTimer = new DispatcherTimer
        {
            Interval = TimeSpan.FromMilliseconds(250)
        };
        _pairedActionCompletionTimer.Tick += (_, _) => RefreshPairedActionCompletions();

        RefreshDeviceValues();
        _refreshTimer = new DispatcherTimer
        {
            Interval = TimeSpan.FromSeconds(1)
        };
        _refreshTimer.Tick += (_, _) => RefreshDeviceValues();
        _refreshTimer.Start();
    }

    public ObservableCollection<CValueCalibrationActionViewModel> TopActions { get; }

    public ObservableCollection<CValueCalibrationActionViewModel> BottomActions { get; }

    public IRelayCommand CloseCommand { get; }

    public IRelayCommand HelpCommand { get; }

    public IRelayCommand<string> ActionCommand { get; }

    public string BaselineOxygenText
    {
        get => _baselineOxygenText;
        private set => SetProperty(ref _baselineOxygenText, value);
    }

    public string TemperatureText
    {
        get => _temperatureText;
        private set => SetProperty(ref _temperatureText, value);
    }

    public string PressureDifferenceText
    {
        get => _pressureDifferenceText;
        private set => SetProperty(ref _pressureDifferenceText, value);
    }

    public string HeatInputText { get; } = "5.0000";

    public string CalibrationOxygenText
    {
        get => _calibrationOxygenText;
        private set => SetProperty(ref _calibrationOxygenText, value);
    }

    public string CValueText
    {
        get => _cValueText;
        private set => SetProperty(ref _cValueText, value);
    }

    public string LastAction
    {
        get => _lastAction;
        private set => SetProperty(ref _lastAction, value);
    }

    private void ExecuteAction(string? action)
    {
        if (string.IsNullOrWhiteSpace(action))
        {
            return;
        }

        if (IsPairedAction(action))
        {
            StartPairedAction(action);
            return;
        }

        LastAction = action;
        ToggleHoldAction(action);
    }

    private CalibrationDataReadStatus RefreshDeviceValues()
    {
        BaselineOxygenText = ReadOxygenPercentText(
            "BaselineOxygen",
            BaselineOxygenRegister,
            FourDecimalDisplayFormat);
        var temperature = ReadRangedFloatDisplay(
            "Temperature",
            "Te",
            TemperatureRegister,
            TemperatureMinimum,
            TemperatureMaximum,
            FourDecimalDisplayFormat);
        TemperatureText = temperature.Text;
        RememberCalibrationFloatByteOrder(temperature);

        var pressureDifference = ReadRangedFloatDisplay(
            "PressureDifference",
            "Δp",
            PressureDifferenceRegister,
            PressureDifferenceMinimum,
            PressureDifferenceMaximum,
            FourDecimalDisplayFormat);
        PressureDifferenceText = pressureDifference.Text;

        var calibrationOxygen = ReadOxygenPercentDisplay(
            "CalibrationOxygen",
            "XO2",
            CalibrationOxygenRegister,
            FourDecimalDisplayFormat);

        CalibrationOxygenText = calibrationOxygen.Text;
        RememberCalibrationFloatByteOrder(calibrationOxygen);

        var cValue = ReadRangedFloatDisplay(
            "CValue",
            "C",
            CValueRegister,
            CValueMinimum,
            CValueMaximum,
            FourDecimalDisplayFormat);
        CValueText = cValue.Text;

        RefreshHoldActionStates();
        return new CalibrationDataReadStatus(pressureDifference, calibrationOxygen, cValue);
    }

    private string ReadRangedFloatText(
        string label,
        ushort registerAddress,
        double minimum,
        double maximum,
        string format)
    {
        return ReadRangedFloatDisplay(label, label, registerAddress, minimum, maximum, format).Text;
    }

    private DisplayValueReadResult ReadRangedFloatDisplay(
        string label,
        string displayLabel,
        ushort registerAddress,
        double minimum,
        double maximum,
        string format)
    {
        if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
        {
            LogFloatReadFailure(label, registerAddress, "读取失败");
            return DisplayValueReadResult.Failed(displayLabel, "读取失败");
        }

        var preferredByteOrder = _calibrationFloatByteOrder;
        if (!ModbusFloatSelector.TrySelectRangedFloat(
                result,
                minimum,
                maximum,
                out var byteOrder,
                out var value,
                out var matchCount,
                preferredByteOrder))
        {
            LogFloatDiagnostic(label, registerAddress, result, null, matchCount, preferredByteOrder);
            return DisplayValueReadResult.Failed(displayLabel, "无有效值");
        }

        LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount, preferredByteOrder);
        return DisplayValueReadResult.Valid(
            displayLabel,
            NormalizeDisplayValue(value).ToString(format, CultureInfo.InvariantCulture),
            byteOrder);
    }

    private string ReadOxygenPercentText(
        string label,
        ushort registerAddress,
        string format)
    {
        return ReadOxygenPercentDisplay(label, label, registerAddress, format).Text;
    }

    private DisplayValueReadResult ReadOxygenPercentDisplay(
        string label,
        string displayLabel,
        ushort registerAddress,
        string format)
    {
        if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
        {
            LogFloatReadFailure(label, registerAddress, "读取失败");
            return DisplayValueReadResult.Failed(displayLabel, "读取失败");
        }

        var preferredByteOrder = _calibrationFloatByteOrder;
        if (!TrySelectOxygenPercent(result, preferredByteOrder, out var byteOrder, out var value, out var matchCount))
        {
            LogFloatDiagnostic(label, registerAddress, result, null, matchCount, preferredByteOrder);
            return DisplayValueReadResult.Failed(displayLabel, "无有效值");
        }

        LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount, preferredByteOrder);
        return DisplayValueReadResult.Valid(
            displayLabel,
            NormalizeDisplayValue(value).ToString(format, CultureInfo.InvariantCulture),
            byteOrder);
    }

    private static bool TrySelectOxygenPercent(
        ModbusFloatReadResult result,
        ModbusFloatByteOrder? preferredByteOrder,
        out ModbusFloatByteOrder byteOrder,
        out double value,
        out int matchCount)
    {
        var candidates = FloatByteOrders
            .Select(candidate => CreateOxygenCandidate(candidate, result.GetValue(candidate)))
            .Where(candidate => candidate.IsValid)
            .ToArray();

        matchCount = candidates.Length;

        var selected = preferredByteOrder is { } plausiblePreferred
            ? candidates.FirstOrDefault(candidate => candidate.ByteOrder == plausiblePreferred && candidate.IsPlausible)
            : default;

        if (!selected.IsValid)
        {
            selected = candidates.FirstOrDefault(
                candidate => candidate.ByteOrder == ModbusFloatByteOrder.Abcd && candidate.IsPlausible);
        }

        if (!selected.IsValid)
        {
            var plausibleCandidates = candidates
                .Where(candidate => candidate.IsPlausible)
                .ToArray();

            if (plausibleCandidates.Length == 1)
            {
                selected = plausibleCandidates[0];
            }
        }

        if (!selected.IsValid)
        {
            selected = preferredByteOrder is { } validPreferred
                ? candidates.FirstOrDefault(candidate => candidate.ByteOrder == validPreferred)
                : default;
        }

        if (!selected.IsValid)
        {
            selected = candidates.FirstOrDefault(candidate => candidate.ByteOrder == ModbusFloatByteOrder.Abcd);
        }

        if (!selected.IsValid && candidates.Length == 1)
        {
            selected = candidates[0];
        }

        if (!selected.IsValid)
        {
            byteOrder = default;
            value = double.NaN;
            return false;
        }

        byteOrder = selected.ByteOrder;
        value = selected.PercentValue;
        return true;
    }

    private static OxygenPercentCandidate CreateOxygenCandidate(
        ModbusFloatByteOrder byteOrder,
        double rawValue)
    {
        if (!double.IsFinite(rawValue) || rawValue < OxygenMinimum)
        {
            return default;
        }

        double percentValue;
        if (rawValue > OxygenMinimum && rawValue <= FractionalOxygenMaximum)
        {
            percentValue = rawValue * 100;
        }
        else if (rawValue <= OxygenMaximum)
        {
            percentValue = rawValue;
        }
        else
        {
            return default;
        }

        return new OxygenPercentCandidate(
            byteOrder,
            percentValue,
            percentValue >= PlausibleOxygenMinimum && percentValue <= OxygenMaximum,
            true);
    }

    private void LogFloatDiagnostic(
        string label,
        ushort registerAddress,
        ModbusFloatReadResult result,
        ModbusFloatByteOrder? selectedByteOrder,
        int matchCount,
        ModbusFloatByteOrder? preferredByteOrder)
    {
        var selectedText = selectedByteOrder?.ToString() ?? "none";
        var preferredText = preferredByteOrder?.ToString() ?? "none";
        var statusText = selectedByteOrder is null ? "无有效值" : "读取成功";
        var diagnosticMessage =
            $"CValueCalibration label={label} register=D{registerAddress} endpoint={_tcpDeviceConnectionService.Endpoint} "
            + $"status={statusText} raw=[{result.RawHex}] "
            + $"ABCD={FormatDiagnosticFloat(result.Abcd)} CDAB={FormatDiagnosticFloat(result.Cdab)} "
            + $"BADC={FormatDiagnosticFloat(result.Badc)} DCBA={FormatDiagnosticFloat(result.Dcba)} "
            + $"preferred={preferredText} selected={selectedText} matches={matchCount}";

        WriteFloatDiagnosticToFile(registerAddress, diagnosticMessage);

        if (selectedByteOrder is null)
        {
            if (!_loggedInvalidFloatDiagnostics.Add(registerAddress))
            {
                return;
            }
        }
        else if (!_loggedFloatDiagnostics.Add(registerAddress))
        {
            return;
        }

        Debug.WriteLine(
            $"C value calibration float {label} register {registerAddress} raw [{result.RawHex}], "
            + $"ABCD={result.Abcd:G9}, CDAB={result.Cdab:G9}, "
            + $"BADC={result.Badc:G9}, DCBA={result.Dcba:G9}, "
            + $"preferred={preferredText}, selected={selectedText}, matches={matchCount}.");
        if (selectedByteOrder is null)
        {
            Log.Warning(
                "C value calibration float has no valid decoded value. Label={Label}, Register=D{RegisterAddress}, Raw={RawHex}, ABCD={Abcd}, CDAB={Cdab}, BADC={Badc}, DCBA={Dcba}, Preferred={PreferredByteOrder}, Matches={MatchCount}.",
                label,
                registerAddress,
                result.RawHex,
                result.Abcd,
                result.Cdab,
                result.Badc,
                result.Dcba,
                preferredText,
                matchCount);
            return;
        }

        Log.Information(
            "C value calibration float decoded. Label={Label}, Register=D{RegisterAddress}, Raw={RawHex}, ABCD={Abcd}, CDAB={Cdab}, BADC={Badc}, DCBA={Dcba}, Preferred={PreferredByteOrder}, Selected={SelectedByteOrder}, Matches={MatchCount}.",
            label,
            registerAddress,
            result.RawHex,
            result.Abcd,
            result.Cdab,
            result.Badc,
            result.Dcba,
            preferredText,
            selectedText,
            matchCount);
    }

    private void LogFloatReadFailure(string label, ushort registerAddress, string statusText)
    {
        var diagnosticMessage =
            $"CValueCalibration label={label} register=D{registerAddress} endpoint={_tcpDeviceConnectionService.Endpoint} "
            + $"status={statusText} connection=\"{_tcpDeviceConnectionService.StatusText}\"";

        var shouldWriteApplicationLog = WriteFloatDiagnosticToFile(registerAddress, diagnosticMessage);
        Debug.WriteLine($"C value calibration float {label} register {registerAddress} {statusText}.");
        if (shouldWriteApplicationLog)
        {
            Log.Warning(
                "C value calibration float read failed. Label={Label}, Register=D{RegisterAddress}, Endpoint={Endpoint}, Status={Status}, ConnectionStatus={ConnectionStatus}.",
                label,
                registerAddress,
                _tcpDeviceConnectionService.Endpoint,
                statusText,
                _tcpDeviceConnectionService.StatusText);
        }
    }

    private bool WriteFloatDiagnosticToFile(ushort registerAddress, string message)
    {
        var now = DateTime.Now;
        if (_lastFileDiagnosticByRegister.TryGetValue(registerAddress, out var lastMessage)
            && string.Equals(lastMessage, message, StringComparison.Ordinal)
            && _lastFileDiagnosticAtByRegister.TryGetValue(registerAddress, out var lastLoggedAt)
            && now - lastLoggedAt < DiagnosticLogInterval)
        {
            return false;
        }

        _lastFileDiagnosticByRegister[registerAddress] = message;
        _lastFileDiagnosticAtByRegister[registerAddress] = now;

        try
        {
            var directory = Path.GetDirectoryName(DiagnosticLogFilePath);
            if (!string.IsNullOrWhiteSpace(directory))
            {
                Directory.CreateDirectory(directory);
            }

            File.AppendAllText(
                DiagnosticLogFilePath,
                $"{now:yyyy-MM-dd HH:mm:ss.fff} {message}{Environment.NewLine}");
            return true;
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"C value calibration diagnostic log write failed: {ex.Message}");
            Log.Warning(ex, "C value calibration diagnostic log write failed. Path={DiagnosticLogFilePath}.", DiagnosticLogFilePath);
            return false;
        }
    }

    private void RememberCalibrationFloatByteOrder(DisplayValueReadResult result)
    {
        if (result.ByteOrder is { } byteOrder)
        {
            _calibrationFloatByteOrder = byteOrder;
        }
    }

    private static string FormatDiagnosticFloat(double value)
    {
        return value.ToString("G9", CultureInfo.InvariantCulture);
    }

    private static double NormalizeDisplayValue(double value)
    {
        return Math.Abs(value) < DisplayZeroTolerance ? 0 : value;
    }

    private CValueCalibrationActionViewModel CreateAction(
        string label,
        string inactiveDisplayText,
        string activeDisplayText,
        bool showStateText = true)
    {
        var action = new CValueCalibrationActionViewModel(
            label,
            ActionCommand,
            inactiveDisplayText,
            activeDisplayText,
            showStateText);
        _actionsByLabel[label] = action;
        return action;
    }

    private void RefreshHoldActionStates()
    {
        UpdateActionStatus(CirculatingWaterAction, CirculatingWaterCoil);
        UpdateActionStatus(MethaneValveAction, MethaneValveCoil);
        UpdateActionStatus(FanAction, FanCoil);
        UpdateActionStatus(IgniterAction, IgniterCoil);
        UpdateActionStatus(SamplingPumpAction, SamplingPumpCoil);
    }

    private bool UpdateActionStatus(string action, ushort coilAddress)
    {
        if (!_actionsByLabel.TryGetValue(action, out var actionViewModel))
        {
            return false;
        }

        if (_tcpDeviceConnectionService.TryReadCoil(coilAddress, out var isActive))
        {
            actionViewModel.UpdateStatus(isActive);
            return true;
        }

        actionViewModel.UpdateStatus(actionViewModel.IsActive, false);
        return false;
    }

    private void ToggleHoldAction(string action)
    {
        if (!TryGetHoldActionCoil(action, out var coilAddress))
        {
            return;
        }

        if (!_tcpDeviceConnectionService.TryReadCoil(coilAddress, out var isActive))
        {
            LastAction = $"{action}状态读取失败";
            Debug.WriteLine($"C value calibration action '{action}' state read failed.");
            return;
        }

        var nextValue = !isActive;
        if (_tcpDeviceConnectionService.TryWriteCoil(coilAddress, nextValue))
        {
            LastAction = $"{action}控制完成";
            if (!UpdateActionStatus(action, coilAddress) && _actionsByLabel.TryGetValue(action, out var actionViewModel))
            {
                actionViewModel.UpdateStatus(nextValue);
            }

            return;
        }

        LastAction = $"{action}控制失败";
        Debug.WriteLine($"C value calibration action '{action}' write failed.");
    }

    private void StartPairedAction(string action)
    {
        if (!TryGetPairedActionCoils(action, out var startCoilAddress, out var endCoilAddress, out var endActionText))
        {
            return;
        }

        if (!_actionsByLabel.TryGetValue(action, out var actionViewModel))
        {
            return;
        }

        if (actionViewModel.IsBusy)
        {
            return;
        }

        if (!_pairedActionWaitStates.TryGetValue(action, out var waitState))
        {
            return;
        }

        if (!TryStartCoilPulse(startCoilAddress, action))
        {
            LastAction = $"{action}失败";
            Debug.WriteLine($"C value calibration action '{action}' write failed.");
            return;
        }

        var completionIsAlreadyActive = _tcpDeviceConnectionService.TryReadCoil(endCoilAddress, out var isComplete) && isComplete;
        waitState.Begin(completionIsAlreadyActive);
        actionViewModel.BeginWaiting(waitState.WaitingText);
        _pairedActionCompletionTimer.Start();
        LastAction = waitState.WaitingText;

        if (completionIsAlreadyActive)
        {
            Debug.WriteLine(
                $"C value calibration completion coil {endCoilAddress} was already active when '{action}' started; "
                + "waiting for it to reset before accepting completion.");
        }
    }

    private void RefreshPairedActionCompletions()
    {
        var hasWaitingAction = false;

        foreach (var (action, waitState) in _pairedActionWaitStates)
        {
            if (!waitState.IsWaiting)
            {
                continue;
            }

            hasWaitingAction = true;

            if (!_actionsByLabel.TryGetValue(action, out var actionViewModel))
            {
                continue;
            }

            if (!_tcpDeviceConnectionService.TryReadCoil(waitState.CompletionCoilAddress, out var isComplete))
            {
                LastAction = $"{waitState.WaitingText}，完成状态读取失败";
                Debug.WriteLine(
                    $"C value calibration completion coil {waitState.CompletionCoilAddress} read failed for '{action}'.");
                continue;
            }

            if (!isComplete)
            {
                waitState.MarkCompletionInactive();
                continue;
            }

            if (waitState.RequiresInactiveBeforeCompletion && !waitState.HasSeenInactiveCompletionState)
            {
                continue;
            }

            CompletePairedAction(action, waitState, actionViewModel);
        }

        if (!hasWaitingAction)
        {
            _pairedActionCompletionTimer.Stop();
        }
    }

    private void CompletePairedAction(
        string action,
        PairedActionWaitState waitState,
        CValueCalibrationActionViewModel actionViewModel)
    {
        var calibrationDataStatus = RefreshDeviceValues();
        var shutdownSummary = action == CalibrationStartAction
            ? CloseCalibrationActuatorsAfterCompletion()
            : string.Empty;
        LastAction = action == CalibrationStartAction && !calibrationDataStatus.HasAllValues
            ? AppendActionSummary(waitState.CompletedText, calibrationDataStatus.MissingSummary, shutdownSummary)
            : AppendActionSummary(waitState.CompletedText, shutdownSummary);
        waitState.End();

        try
        {
            waitState.CompletedAction();
        }
        finally
        {
            actionViewModel.FinishWaiting();
        }
    }

    private string CloseCalibrationActuatorsAfterCompletion()
    {
        var failedActions = new List<string>(2);

        CloseHoldActionAfterCompletion(MethaneValveAction, MethaneValveCoil, failedActions);
        CloseHoldActionAfterCompletion(SamplingPumpAction, SamplingPumpCoil, failedActions);

        return failedActions.Count == 0
            ? "已关闭甲烷阀和取样泵"
            : $"{string.Join("、", failedActions)}关闭失败";
    }

    private void CloseHoldActionAfterCompletion(
        string action,
        ushort coilAddress,
        List<string> failedActions)
    {
        if (_tcpDeviceConnectionService.TryWriteCoil(coilAddress, false))
        {
            if (!UpdateActionStatus(action, coilAddress) && _actionsByLabel.TryGetValue(action, out var actionViewModel))
            {
                actionViewModel.UpdateStatus(false);
            }

            return;
        }

        failedActions.Add(action);
        Debug.WriteLine($"C value calibration action '{action}' auto close failed after calibration completion.");
    }

    private static string AppendActionSummary(string message, params string[] summaries)
    {
        foreach (var summary in summaries)
        {
            if (!string.IsNullOrWhiteSpace(summary))
            {
                message = $"{message}，{summary}";
            }
        }

        return message;
    }

    private bool TryStartCoilPulse(ushort coilAddress, string actionText)
    {
        if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
        {
            return false;
        }

        _ = ReleaseCoilAfterPulseAsync(coilAddress, actionText);
        return true;
    }

    private async Task ReleaseCoilAfterPulseAsync(ushort coilAddress, string actionText)
    {
        await Task.Delay(PairedActionPulseDuration);
        if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, false))
        {
            Debug.WriteLine($"C value calibration action '{actionText}' pulse release failed.");
        }
    }

    private static bool TryGetHoldActionCoil(string action, out ushort coilAddress)
    {
        switch (action)
        {
            case CirculatingWaterAction:
                coilAddress = CirculatingWaterCoil;
                return true;
            case MethaneValveAction:
                coilAddress = MethaneValveCoil;
                return true;
            case FanAction:
                coilAddress = FanCoil;
                return true;
            case IgniterAction:
                coilAddress = IgniterCoil;
                return true;
            case SamplingPumpAction:
                coilAddress = SamplingPumpCoil;
                return true;
            default:
                coilAddress = 0;
                return false;
        }
    }

    private static bool TryGetPairedActionCoils(
        string action,
        out ushort startCoilAddress,
        out ushort endCoilAddress,
        out string endActionText)
    {
        switch (action)
        {
            case BaselineCollectionAction:
                startCoilAddress = BaselineCollectionCoil;
                endCoilAddress = BaselineEndCoil;
                endActionText = "基线结束";
                return true;
            case CalibrationStartAction:
                startCoilAddress = CalibrationStartCoil;
                endCoilAddress = CalibrationEndCoil;
                endActionText = "标定结束";
                return true;
            default:
                startCoilAddress = 0;
                endCoilAddress = 0;
                endActionText = string.Empty;
                return false;
        }
    }

    private static bool IsPairedAction(string action)
    {
        return action is BaselineCollectionAction or CalibrationStartAction;
    }

    private readonly record struct OxygenPercentCandidate(
        ModbusFloatByteOrder ByteOrder,
        double PercentValue,
        bool IsPlausible,
        bool IsValid);

    private readonly record struct DisplayValueReadResult(
        string DisplayLabel,
        string Text,
        bool HasValue,
        string FailureDescription,
        ModbusFloatByteOrder? ByteOrder)
    {
        public static DisplayValueReadResult Valid(
            string displayLabel,
            string text,
            ModbusFloatByteOrder byteOrder)
        {
            return new DisplayValueReadResult(displayLabel, text, true, string.Empty, byteOrder);
        }

        public static DisplayValueReadResult Failed(string displayLabel, string failureDescription)
        {
            return new DisplayValueReadResult(displayLabel, string.Empty, false, failureDescription, null);
        }
    }

    private readonly record struct CalibrationDataReadStatus(
        DisplayValueReadResult PressureDifference,
        DisplayValueReadResult CalibrationOxygen,
        DisplayValueReadResult CValue)
    {
        public bool HasAllValues => PressureDifference.HasValue && CalibrationOxygen.HasValue && CValue.HasValue;

        public string MissingSummary
        {
            get
            {
                var failures = new List<string>(3);
                AddFailure(failures, PressureDifference);
                AddFailure(failures, CalibrationOxygen);
                AddFailure(failures, CValue);
                return string.Join("；", failures);
            }
        }

        private static void AddFailure(List<string> failures, DisplayValueReadResult result)
        {
            if (!result.HasValue)
            {
                failures.Add($"{result.DisplayLabel}{result.FailureDescription}");
            }
        }
    }

    private sealed class PairedActionWaitState(
        ushort completionCoilAddress,
        string waitingText,
        string completedText,
        Action completedAction)
    {
        public ushort CompletionCoilAddress { get; } = completionCoilAddress;

        public string WaitingText { get; } = waitingText;

        public string CompletedText { get; } = completedText;

        public Action CompletedAction { get; } = completedAction;

        public bool IsWaiting { get; private set; }

        public bool RequiresInactiveBeforeCompletion { get; private set; }

        public bool HasSeenInactiveCompletionState { get; private set; }

        public void Begin(bool completionIsAlreadyActive)
        {
            IsWaiting = true;
            RequiresInactiveBeforeCompletion = completionIsAlreadyActive;
            HasSeenInactiveCompletionState = !completionIsAlreadyActive;
        }

        public void MarkCompletionInactive()
        {
            HasSeenInactiveCompletionState = true;
        }

        public void End()
        {
            IsWaiting = false;
            RequiresInactiveBeforeCompletion = false;
            HasSeenInactiveCompletionState = false;
        }
    }
}
