#define NOMINMAX
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <zmq.h>

#include "../XYParserApi.h"

#include <array>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <vector>

namespace {

using Clock = std::chrono::steady_clock;

enum class DemoMode {
    Mode64Tcp,
    Mode8Serial,
    Mode2Tcp
};

#if defined(XY_WORKFLOW_DEMO_64)
constexpr DemoMode kDemoMode = DemoMode::Mode64Tcp;
constexpr wchar_t kProgramName[] = L"XYParser64Demo";
constexpr char kDefaultTcpHost[] = "127.0.0.1";
constexpr wchar_t kDefaultDataComPort[] = L"";
constexpr wchar_t kDefaultTriggerComPort[] = L"COM44";
constexpr double kDefaultVref = 4.5;
#elif defined(XY_WORKFLOW_DEMO_2)
constexpr DemoMode kDemoMode = DemoMode::Mode2Tcp;
constexpr wchar_t kProgramName[] = L"XYParser2Demo";
constexpr char kDefaultTcpHost[] = "127.0.0.1";
constexpr wchar_t kDefaultDataComPort[] = L"";
constexpr wchar_t kDefaultTriggerComPort[] = L"";
constexpr double kDefaultVref = 2.42;
#elif defined(XY_WORKFLOW_DEMO_8)
constexpr DemoMode kDemoMode = DemoMode::Mode8Serial;
constexpr wchar_t kProgramName[] = L"XYParser8Demo";
constexpr char kDefaultTcpHost[] = "127.0.0.1";
constexpr wchar_t kDefaultDataComPort[] = L"COM44";
constexpr wchar_t kDefaultTriggerComPort[] = L"";
constexpr double kDefaultVref = 4.5;
#else
#error Define either XY_WORKFLOW_DEMO_64, XY_WORKFLOW_DEMO_2 or XY_WORKFLOW_DEMO_8 for this target.
#endif

struct ZmqRoundTripStats {
    std::uint64_t sent_packets = 0;
    std::uint64_t sent_bytes = 0;
    std::uint64_t received_packets = 0;
    std::uint64_t received_bytes = 0;
    std::uint64_t timeout_count = 0;
    std::uint64_t invalid_payload_count = 0;
};

struct DemoOptions {
    DemoMode mode = kDemoMode;
    std::string tcp_host = kDefaultTcpHost;
    int tcp_port = 5086;
    std::string algorithm_host = "127.0.0.1";
    int algorithm_port = 8100;
    int algorithm_timeout_ms = 1000;
    std::wstring data_com_port = kDefaultDataComPort;
    std::wstring trigger_com_port = kDefaultTriggerComPort;
    int serial_baud_rate = 460800;
    int sample_rate = 250;
    int gain = 6;
    double vref = kDefaultVref;
    int bypass_checksum = 1;
    int train_duration_ms = 3000;
};

std::string FormatIpv4Endpoint(const sockaddr_in& addr)
{
    char address_text[INET_ADDRSTRLEN] = {};
    if (InetNtopA(AF_INET,
                  const_cast<IN_ADDR*>(&addr.sin_addr),
                  address_text,
                  static_cast<DWORD>(std::size(address_text))) == nullptr) {
        std::snprintf(address_text, std::size(address_text), "<invalid-ip>");
    }
    std::ostringstream stream;
    stream << address_text << ':' << ntohs(addr.sin_port);
    return stream.str();
}

class ParserHandleGuard {
public:
    explicit ParserHandleGuard(XYParserHandle handle) : handle_(handle) {}
    ~ParserHandleGuard()
    {
        if (handle_ != nullptr) {
            XYParser_DestroyParser(handle_);
        }
    }

    ParserHandleGuard(const ParserHandleGuard&) = delete;
    ParserHandleGuard& operator=(const ParserHandleGuard&) = delete;

    XYParserHandle get() const { return handle_; }

private:
    XYParserHandle handle_ = nullptr;
};

class WinsockRuntime {
public:
    WinsockRuntime()
    {
        WSADATA wsa_data{};
        ok_ = (WSAStartup(MAKEWORD(2, 2), &wsa_data) == 0);
    }

    ~WinsockRuntime()
    {
        if (ok_) {
            WSACleanup();
        }
    }

    bool ok() const { return ok_; }

private:
    bool ok_ = false;
};

class TcpClient {
public:
    ~TcpClient() { Close(); }

    bool Connect(const std::string& host, int port)
    {
        addrinfo hints{};
        hints.ai_family = AF_UNSPEC;
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_protocol = IPPROTO_TCP;

        addrinfo* result = nullptr;
        const std::string port_text = std::to_string(port);
        if (getaddrinfo(host.c_str(), port_text.c_str(), &hints, &result) != 0) {
            return false;
        }

        bool connected = false;
        for (addrinfo* current = result; current != nullptr; current = current->ai_next) {
            SOCKET sock = socket(current->ai_family, current->ai_socktype, current->ai_protocol);
            if (sock == INVALID_SOCKET) {
                continue;
            }

            if (connect(sock, current->ai_addr, static_cast<int>(current->ai_addrlen)) == 0) {
                DWORD timeout_ms = 200;
                setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeout_ms), sizeof(timeout_ms));
                setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&timeout_ms), sizeof(timeout_ms));
                socket_ = sock;
                connected = true;
                break;
            }

            closesocket(sock);
        }

        freeaddrinfo(result);
        return connected;
    }

    bool Send(const std::uint8_t* data, std::size_t size)
    {
        if (socket_ == INVALID_SOCKET) {
            return false;
        }

        std::size_t sent_total = 0;
        while (sent_total < size) {
            const int sent = send(socket_,
                                  reinterpret_cast<const char*>(data + sent_total),
                                  static_cast<int>(size - sent_total),
                                  0);
            if (sent <= 0) {
                return false;
            }
            sent_total += static_cast<std::size_t>(sent);
        }
        return true;
    }

    int Receive(std::uint8_t* buffer, int capacity, bool& disconnected)
    {
        disconnected = false;
        if (socket_ == INVALID_SOCKET) {
            disconnected = true;
            return -1;
        }

        const int received = recv(socket_, reinterpret_cast<char*>(buffer), capacity, 0);
        if (received > 0) {
            return received;
        }
        if (received == 0) {
            disconnected = true;
            return -1;
        }

        const int error = WSAGetLastError();
        if (error == WSAETIMEDOUT || error == WSAEWOULDBLOCK) {
            return 0;
        }

        disconnected = true;
        return -1;
    }

    void Close()
    {
        if (socket_ != INVALID_SOCKET) {
            closesocket(socket_);
            socket_ = INVALID_SOCKET;
        }
    }

private:
    SOCKET socket_ = INVALID_SOCKET;
};

std::string BuildZmqTcpEndpoint(const std::string& host, int port)
{
    return "tcp://" + host + ':' + std::to_string(port);
}

class ZmqDuplexClient {
public:
    ~ZmqDuplexClient() { Close(); }

    bool Open(const std::string& remote_host, int remote_port, int timeout_ms)
    {
        Close();
        last_error_text_.clear();
        configured_remote_endpoint_ = BuildZmqTcpEndpoint(remote_host, remote_port);

        context_ = zmq_ctx_new();
        if (context_ == nullptr) {
            last_error_text_ = zmq_strerror(zmq_errno());
            return false;
        }

        socket_ = zmq_socket(context_, ZMQ_DEALER);
        if (socket_ == nullptr) {
            last_error_text_ = zmq_strerror(zmq_errno());
            Close();
            return false;
        }

        const int linger_ms = 0;
        zmq_setsockopt(socket_, ZMQ_LINGER, &linger_ms, sizeof(linger_ms));
        zmq_setsockopt(socket_, ZMQ_RCVTIMEO, &timeout_ms, sizeof(timeout_ms));
        zmq_setsockopt(socket_, ZMQ_SNDTIMEO, &timeout_ms, sizeof(timeout_ms));

        identity_ = "xyparser-" + std::to_string(GetCurrentProcessId()) + '-' + std::to_string(GetTickCount64());
        if (zmq_setsockopt(socket_,
                           ZMQ_IDENTITY,
                           identity_.data(),
                           static_cast<int>(identity_.size())) != 0) {
            last_error_text_ = zmq_strerror(zmq_errno());
            Close();
            return false;
        }

        if (zmq_connect(socket_, configured_remote_endpoint_.c_str()) != 0) {
            last_error_text_ = zmq_strerror(zmq_errno());
            Close();
            return false;
        }

        return true;
    }

    bool Send(const std::uint8_t* data, std::size_t size)
    {
        if (socket_ == nullptr) {
            last_error_text_ = "socket not open";
            return false;
        }

        const Clock::time_point now = Clock::now();
        std::cout << "[AlgorithmZmqTx] payloadBytes=" << size;
        if (last_send_time_.has_value()) {
            const double delta_ms =
                std::chrono::duration<double, std::milli>(now - *last_send_time_).count();
            std::cout << " deltaMs=" << delta_ms;
        }
        std::cout << std::endl;
        last_send_time_ = now;

        const int sent_identity = zmq_send(socket_,
                                           identity_.data(),
                                           static_cast<int>(identity_.size()),
                                           ZMQ_SNDMORE);
        if (sent_identity != static_cast<int>(identity_.size())) {
            last_error_text_ = zmq_strerror(zmq_errno());
            return false;
        }

        const int sent_empty = zmq_send(socket_, nullptr, 0, ZMQ_SNDMORE);
        if (sent_empty != 0) {
            last_error_text_ = zmq_strerror(zmq_errno());
            return false;
        }

        const int sent_payload = zmq_send(socket_, data, size, 0);
        if (sent_payload != static_cast<int>(size)) {
            last_error_text_ = zmq_strerror(zmq_errno());
            return false;
        }
        last_error_text_.clear();
        return true;
    }

    int Receive(std::uint8_t* buffer, int capacity, bool& timed_out)
    {
        timed_out = false;
        if (socket_ == nullptr) {
            last_error_text_ = "socket not open";
            return -1;
        }

        std::vector<std::uint8_t> first_frame;
        bool has_more = false;
        if (!ReceiveFrame(first_frame, has_more, 0)) {
            const int error = zmq_errno();
            last_error_text_ = zmq_strerror(error);
            if (error == EAGAIN) {
                timed_out = true;
                return 0;
            }
            return -1;
        }

        std::vector<std::uint8_t> payload_frame;
        if (has_more) {
            if (!first_frame.empty()) {
                last_error_text_ = "unexpected non-empty delimiter frame";
                return -1;
            }
            bool payload_has_more = false;
            if (!ReceiveFrame(payload_frame, payload_has_more, 0)) {
                const int error = zmq_errno();
                last_error_text_ = zmq_strerror(error);
                if (error == EAGAIN) {
                    timed_out = true;
                    return 0;
                }
                return -1;
            }
            if (payload_has_more) {
                last_error_text_ = "unexpected multipart payload";
                return -1;
            }
        } else {
            payload_frame = std::move(first_frame);
        }

        if (payload_frame.size() > static_cast<std::size_t>(capacity)) {
            last_error_text_ = "payload too large";
            return -1;
        }

        std::memcpy(buffer, payload_frame.data(), payload_frame.size());
        last_error_text_.clear();
        return static_cast<int>(payload_frame.size());
    }

    int ReceiveNonBlocking(std::vector<std::uint8_t>& payload, bool& no_message)
    {
        no_message = false;
        if (socket_ == nullptr) {
            last_error_text_ = "socket not open";
            return -1;
        }

        std::vector<std::uint8_t> first_frame;
        bool has_more = false;
        if (!ReceiveFrame(first_frame, has_more, ZMQ_DONTWAIT)) {
            const int error = zmq_errno();
            last_error_text_ = zmq_strerror(error);
            if (error == EAGAIN) {
                no_message = true;
                return 0;
            }
            return -1;
        }

        std::vector<std::uint8_t> payload_frame;
        if (has_more) {
            if (!first_frame.empty()) {
                last_error_text_ = "unexpected non-empty delimiter frame";
                return -1;
            }
            bool payload_has_more = false;
            if (!ReceiveFrame(payload_frame, payload_has_more, 0)) {
                last_error_text_ = zmq_strerror(zmq_errno());
                return -1;
            }
            if (payload_has_more) {
                last_error_text_ = "unexpected multipart payload";
                return -1;
            }
        } else {
            payload_frame = std::move(first_frame);
        }

        payload = std::move(payload_frame);
        last_error_text_.clear();
        return static_cast<int>(payload.size());
    }

    const std::string& last_error() const { return last_error_text_; }
    const std::string& configured_remote_endpoint() const { return configured_remote_endpoint_; }
    const std::string& identity() const { return identity_; }

    void Close()
    {
        if (socket_ != nullptr) {
            zmq_close(socket_);
            socket_ = nullptr;
        }
        if (context_ != nullptr) {
            zmq_ctx_term(context_);
            context_ = nullptr;
        }
        last_send_time_.reset();
    }

private:
    bool ReceiveFrame(std::vector<std::uint8_t>& frame, bool& has_more, int flags)
    {
        zmq_msg_t message;
        zmq_msg_init(&message);
        const int received = zmq_msg_recv(&message, socket_, flags);
        if (received < 0) {
            zmq_msg_close(&message);
            return false;
        }

        const auto* data = static_cast<const std::uint8_t*>(zmq_msg_data(&message));
        const std::size_t size = zmq_msg_size(&message);
        frame.assign(data, data + size);

        int more = 0;
        size_t more_size = sizeof(more);
        zmq_getsockopt(socket_, ZMQ_RCVMORE, &more, &more_size);
        has_more = (more != 0);
        zmq_msg_close(&message);
        return true;
    }

    void* context_ = nullptr;
    void* socket_ = nullptr;
    std::string last_error_text_;
    std::string configured_remote_endpoint_;
    std::string identity_;
    std::optional<Clock::time_point> last_send_time_;
};

class SerialPort {
public:
    ~SerialPort() { Close(); }

    bool Open(const std::wstring& port_name, int baud_rate)
    {
        const std::wstring full_name = (port_name.rfind(L"\\\\.\\", 0) == 0)
                                           ? port_name
                                           : (L"\\\\.\\" + port_name);

        handle_ = CreateFileW(full_name.c_str(),
                              GENERIC_READ | GENERIC_WRITE,
                              0,
                              nullptr,
                              OPEN_EXISTING,
                              0,
                              nullptr);
        if (handle_ == INVALID_HANDLE_VALUE) {
            handle_ = nullptr;
            return false;
        }

        DCB dcb{};
        dcb.DCBlength = sizeof(dcb);
        if (!GetCommState(handle_, &dcb)) {
            Close();
            return false;
        }

        dcb.BaudRate = static_cast<DWORD>(baud_rate);
        dcb.ByteSize = 8;
        dcb.Parity = NOPARITY;
        dcb.StopBits = ONESTOPBIT;
        dcb.fBinary = TRUE;
        dcb.fParity = FALSE;

        if (!SetCommState(handle_, &dcb)) {
            Close();
            return false;
        }

        COMMTIMEOUTS timeouts{};
        timeouts.ReadIntervalTimeout = 20;
        timeouts.ReadTotalTimeoutConstant = 50;
        timeouts.ReadTotalTimeoutMultiplier = 0;
        timeouts.WriteTotalTimeoutConstant = 200;
        timeouts.WriteTotalTimeoutMultiplier = 0;
        if (!SetCommTimeouts(handle_, &timeouts)) {
            Close();
            return false;
        }

        SetupComm(handle_, 64 * 1024, 64 * 1024);
        PurgeComm(handle_, PURGE_RXCLEAR | PURGE_TXCLEAR);
        return true;
    }

    bool Write(const std::uint8_t* data, std::size_t size)
    {
        if (handle_ == nullptr) {
            return false;
        }

        std::size_t written_total = 0;
        while (written_total < size) {
            DWORD written = 0;
            if (!WriteFile(handle_,
                           data + written_total,
                           static_cast<DWORD>(size - written_total),
                           &written,
                           nullptr)) {
                return false;
            }
            if (written == 0) {
                return false;
            }
            written_total += static_cast<std::size_t>(written);
        }
        return true;
    }

    int Read(std::uint8_t* buffer, int capacity, bool& disconnected)
    {
        disconnected = false;
        if (handle_ == nullptr) {
            disconnected = true;
            return -1;
        }

        DWORD bytes_read = 0;
        if (!ReadFile(handle_, buffer, static_cast<DWORD>(capacity), &bytes_read, nullptr)) {
            disconnected = true;
            return -1;
        }
        return static_cast<int>(bytes_read);
    }

    void Close()
    {
        if (handle_ != nullptr) {
            CloseHandle(handle_);
            handle_ = nullptr;
        }
    }

private:
    HANDLE handle_ = nullptr;
};

std::string Narrow(const std::wstring& text)
{
    if (text.empty()) {
        return {};
    }

    const int size = WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, nullptr, 0, nullptr, nullptr);
    if (size <= 1) {
        return {};
    }

    std::vector<char> buffer(static_cast<std::size_t>(size), '\0');
    WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, buffer.data(), size, nullptr, nullptr);
    std::string result(buffer.data());
    return result;
}

bool ParseInt(const std::wstring& text, int& value)
{
    wchar_t* end = nullptr;
    const long parsed = std::wcstol(text.c_str(), &end, 10);
    if (end == nullptr || *end != L'\0') {
        return false;
    }
    value = static_cast<int>(parsed);
    return true;
}

bool ParseDouble(const std::wstring& text, double& value)
{
    wchar_t* end = nullptr;
    value = std::wcstod(text.c_str(), &end);
    return end != nullptr && *end == L'\0';
}

bool ParseBool(const std::wstring& text, bool& value)
{
    if (text == L"1" || text == L"true" || text == L"TRUE") {
        value = true;
        return true;
    }
    if (text == L"0" || text == L"false" || text == L"FALSE") {
        value = false;
        return true;
    }
    return false;
}

void PrintUsage()
{
    std::wcout
        << kProgramName << L" usage:\n";
#if defined(XY_WORKFLOW_DEMO_64) || defined(XY_WORKFLOW_DEMO_2)
    std::wcout
        << L"  " << kProgramName << L" [--tcp-host 127.0.0.1] [--tcp-port 5086]\n"
        << L"                  [--trigger-com COM44]\n"
        << L"                  [--algorithm-host 127.0.0.1 --algorithm-port 8100]\n"
        << L"                  [--train-duration-ms 3000]\n";
#else
    std::wcout
        << L"  " << kProgramName << L" [--data-com COM44] [--baud 460800]\n"
        << L"                  [--algorithm-host 127.0.0.1 --algorithm-port 8100]\n";
#endif
    std::wcout
        << L"\n"
        << L"\n"
        << L"Notes:\n"
#if defined(XY_WORKFLOW_DEMO_64)
        << L"  - Default TCP host is 127.0.0.1 and default trigger serial is COM44.\n"
        << L"  - Receive 64ch stream from TCP, optional trigger serial for TRAIN_0/TRAIN_1.\n"
#elif defined(XY_WORKFLOW_DEMO_2)
        << L"  - Default TCP host is 127.0.0.1 and trigger serial is optional.\n"
        << L"  - Receive 2ch stream from TCP, map FP1/FP2 to 64ch before algorithm and Welch.\n"
#else
        << L"  - Default 8ch data serial is COM44.\n"
        << L"  - Receive 8ch stream from serial, then run 8ch -> 64ch -> algorithm data -> Welch.\n"
#endif
        << L"  - Sample rate and gain use built-in defaults.\n"
        << L"  - Impedance and Welch/PSD are enabled immediately after startup.\n"
        << L"  - Algorithm path always uses ZMQ DEALER/ROUTER: demo connects to algorithm server and waits for response.\n";
}

bool ParseArguments(int argc, wchar_t* argv[], DemoOptions& options)
{
    if (argc < 1) {
        return false;
    }

    for (int i = 1; i < argc; ++i) {
        const std::wstring arg = argv[i];
        if (arg == L"--tcp-host" && i + 1 < argc) {
            options.tcp_host = Narrow(argv[++i]);
        } else if (arg == L"--tcp-port" && i + 1 < argc) {
            if (!ParseInt(argv[++i], options.tcp_port)) {
                return false;
            }
        } else if (arg == L"--algorithm-host" && i + 1 < argc) {
            options.algorithm_host = Narrow(argv[++i]);
        } else if ((arg == L"--algorithm-port" || arg == L"--algorithm-udp-port") && i + 1 < argc) {
            if (!ParseInt(argv[++i], options.algorithm_port)) {
                return false;
            }
        } else if (arg == L"--algorithm-timeout-ms" && i + 1 < argc) {
            if (!ParseInt(argv[++i], options.algorithm_timeout_ms)) {
                return false;
            }
        } else if (arg == L"--data-com" && i + 1 < argc) {
            options.data_com_port = argv[++i];
        } else if (arg == L"--trigger-com" && i + 1 < argc) {
            options.trigger_com_port = argv[++i];
        } else if (arg == L"--baud" && i + 1 < argc) {
            if (!ParseInt(argv[++i], options.serial_baud_rate)) {
                return false;
            }
        } else if (arg == L"--vref" && i + 1 < argc) {
            if (!ParseDouble(argv[++i], options.vref)) {
                return false;
            }
        } else if (arg == L"--bypass-checksum" && i + 1 < argc) {
            if (!ParseInt(argv[++i], options.bypass_checksum)) {
                return false;
            }
        } else if (arg == L"--train-duration-ms" && i + 1 < argc) {
            if (!ParseInt(argv[++i], options.train_duration_ms)) {
                return false;
            }
        } else if (arg == L"--help" || arg == L"-h") {
            return false;
        } else {
            return false;
        }
    }

    if (options.algorithm_port <= 0) {
        return false;
    }

#if defined(XY_WORKFLOW_DEMO_64) || defined(XY_WORKFLOW_DEMO_2)
    return options.tcp_port > 0;
#else
    return !options.data_com_port.empty();
#endif
}

std::string DescribeParserError(XYParserHandle parser)
{
    const char* error = XYParser_GetLastError(parser);
    return (error != nullptr && error[0] != '\0') ? error : std::string("no extra error");
}

void PrintBytes(const char* label, const std::uint8_t* data, std::size_t size)
{
    std::ostringstream stream;
    stream << label << " [";
    for (std::size_t i = 0; i < size; ++i) {
        if (i > 0) {
            stream << ' ';
        }
        stream << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(data[i]);
    }
    stream << ']';
    std::cout << stream.str() << std::endl;
}

void PrintImpedanceSummary(const XYParserImpedanceSummary& summary)
{
    std::cout << "[Impedance] sampleRate=" << summary.sample_rate
              << " window=" << summary.window_sample_count;

    std::array<XYParserLeadChannelNumber, XYPARSER_MAX_CHANNELS> leads{};
    const int lead_count = XYParser_GetLeadMap(summary.channel_count,
                                               leads.data(),
                                               static_cast<int>(leads.size()));

    if (summary.channel_count == 8 && lead_count == 8) {
        for (int i = 0; i < lead_count; ++i) {
            const XYParserLeadChannelNumber lead = leads[static_cast<std::size_t>(i)];
            const char* lead_name = XYParser_GetLeadName(lead);
            std::cout << ' ' << (lead_name != nullptr ? lead_name : "lead?")
                      << '=' << summary.impedance_values[lead];
        }
        std::cout << std::endl;
        return;
    }

    int printed = 0;
    for (int i = 0; i < XYPARSER_MAX_CHANNELS; ++i) {
        if (summary.impedance_values[i] == 0) {
            continue;
        }
        const auto lead = static_cast<XYParserLeadChannelNumber>(i);
        const char* lead_name = XYParser_GetLeadName(lead);
        std::cout << ' ' << (lead_name != nullptr ? lead_name : "lead?")
                  << '=' << summary.impedance_values[i];
        ++printed;
    }
    if (printed == 0 && lead_count > 0) {
        for (int i = 0; i < lead_count; ++i) {
            const XYParserLeadChannelNumber lead = leads[static_cast<std::size_t>(i)];
            const char* lead_name = XYParser_GetLeadName(lead);
            std::cout << ' ' << (lead_name != nullptr ? lead_name : "lead?")
                      << '=' << summary.impedance_values[lead];
        }
    }
    std::cout << std::endl;
}

int FindPeakFrequencyIndex(const XYParserWelchSummary& summary, XYParserLeadChannelNumber lead)
{
    if (summary.frequency_count == 0) {
        return -1;
    }

    const std::size_t lead_index = static_cast<std::size_t>(lead);
    std::uint32_t best_index = 0;
    double best_value = summary.psd_values[lead_index][0];
    for (std::uint32_t i = 1; i < summary.frequency_count; ++i) {
        if (summary.psd_values[lead_index][i] > best_value) {
            best_value = summary.psd_values[lead_index][i];
            best_index = i;
        }
    }
    return static_cast<int>(best_index);
}

XYParserLeadChannelNumber FindDominantWelchLead(const XYParserWelchSummary& summary)
{
    std::array<XYParserLeadChannelNumber, XYPARSER_MAX_CHANNELS> leads{};
    const int lead_count = XYParser_GetLeadMap(summary.channel_count,
                                               leads.data(),
                                               static_cast<int>(leads.size()));
    const int channel_count = (lead_count > 0) ? lead_count : static_cast<int>(summary.channel_count);
    XYParserLeadChannelNumber best_lead = LeadChannel_FP1;
    double best_psd = -1.0;

    for (int channel_index = 0; channel_index < channel_count; ++channel_index) {
        XYParserLeadChannelNumber lead = static_cast<XYParserLeadChannelNumber>(channel_index);
        if (lead_count > 0) {
            lead = leads[static_cast<std::size_t>(channel_index)];
        }

        const int peak_index = FindPeakFrequencyIndex(summary, lead);
        if (peak_index < 0) {
            continue;
        }

        const double peak_psd = summary.psd_values[lead][static_cast<std::size_t>(peak_index)];
        if (peak_psd > best_psd) {
            best_psd = peak_psd;
            best_lead = lead;
        }
    }

    return best_lead;
}

void PrintWelchSummary(const XYParserWelchSummary& summary)
{
    const XYParserLeadChannelNumber lead = FindDominantWelchLead(summary);
    const int peak_index = FindPeakFrequencyIndex(summary, lead);
    const char* alpha_name = XYParser_GetWelchBandName(2);
    const char* gamma_name = XYParser_GetWelchBandName(4);
    const char* lead_name = XYParser_GetLeadName(lead);

    std::ostringstream stream;
    stream << "[Welch] sampleRate=" << summary.sample_rate
           << " window=" << summary.window_sample_count
           << " freqCount=" << summary.frequency_count
           << " lead=" << (lead_name != nullptr ? lead_name : "lead?");
    if (peak_index >= 0) {
        stream << " peakHz=" << summary.frequencies[static_cast<std::size_t>(peak_index)]
               << " peakPsd=" << summary.psd_values[lead][static_cast<std::size_t>(peak_index)];
    }
    stream << ' ' << (alpha_name != nullptr ? alpha_name : "alpha")
           << '=' << summary.band_values[2][lead]
           << ' ' << (gamma_name != nullptr ? gamma_name : "gamma")
           << '=' << summary.band_values[4][lead];

    static std::string last_welch_line;
    const std::string current_line = stream.str();
    if (current_line == last_welch_line) {
        return;
    }

    last_welch_line = current_line;
    std::cout << current_line << std::endl;
}

#if defined(XY_WORKFLOW_DEMO_2)
void Print2ChAlgorithmPreview(const char* tag,
                              const double* values,
                              std::size_t sample_count,
                              std::size_t stride)
{
    if (values == nullptr || stride < 2) {
        return;
    }

    std::ostringstream stream;
    stream << tag;
    const std::size_t preview_count = (sample_count < 5) ? sample_count : 5;
    for (std::size_t sample_index = 0; sample_index < preview_count; ++sample_index) {
        const std::size_t sample_offset = sample_index * stride;
        stream << " s" << sample_index
               << "(FP1=" << values[sample_offset + static_cast<std::size_t>(LeadChannel_FP1)]
               << ",FP2=" << values[sample_offset + static_cast<std::size_t>(LeadChannel_FP2)]
               << ')';
    }
    std::cout << stream.str() << std::endl;
}
#endif

void DrainImpedance(XYParserHandle parser)
{
    std::array<XYParserImpedanceSummary, 4> summaries{};
    while (true) {
        const int count = XYParser_ReadImpedance(parser, summaries.data(), static_cast<int>(summaries.size()));
        if (count <= 0) {
            break;
        }
        for (int i = 0; i < count; ++i) {
            PrintImpedanceSummary(summaries[static_cast<std::size_t>(i)]);
        }
    }
}

void DrainWelch(XYParserHandle parser)
{
    std::array<XYParserWelchSummary, 4> summaries{};
    while (true) {
        const int count = XYParser_ReadWelch(parser, summaries.data(), static_cast<int>(summaries.size()));
        if (count <= 0) {
            break;
        }
        for (int i = 0; i < count; ++i) {
            if (summaries[static_cast<std::size_t>(i)].ok != 0) {
                PrintWelchSummary(summaries[static_cast<std::size_t>(i)]);
            }
        }
    }
}

void PrintZmqStats(const ZmqRoundTripStats& stats)
{
    std::cout << "[AlgorithmZmq] txPackets=" << stats.sent_packets
              << " txBytes=" << stats.sent_bytes
              << " rxPackets=" << stats.received_packets
              << " rxBytes=" << stats.received_bytes
              << " timeouts=" << stats.timeout_count
              << " invalidPayloads=" << stats.invalid_payload_count
              << std::endl;
}

bool RunAlgorithmZmqRoundTrip(XYParserHandle parser,
                              ZmqDuplexClient& zmq_client,
                              const double* algorithm_data,
                              ZmqRoundTripStats& stats)
{
    const std::size_t payload_size = XYPARSER_FRAME_ALGORITHM_VALUE_COUNT * sizeof(double);
    if (!zmq_client.Send(reinterpret_cast<const std::uint8_t*>(algorithm_data), payload_size)) {
        std::cerr << "Send algorithm ZMQ payload failed"
                  << " remote=" << zmq_client.configured_remote_endpoint()
                  << " zmqError=" << zmq_client.last_error()
                  << std::endl;
        return false;
    }
    ++stats.sent_packets;
    stats.sent_bytes += payload_size;

    bool received_any_response = false;
    while (true) {
        std::vector<std::uint8_t> response_payload;
        bool no_message = false;
        const int received = zmq_client.ReceiveNonBlocking(response_payload, no_message);
        if (no_message) {
            break;
        }
        if (received <= 0) {
            std::cerr << "Receive algorithm ZMQ payload failed"
                      << " remote=" << zmq_client.configured_remote_endpoint()
                      << " zmqError=" << zmq_client.last_error()
                      << std::endl;
            PrintZmqStats(stats);
            return false;
        }
        if (received % static_cast<int>(sizeof(double)) != 0) {
            ++stats.invalid_payload_count;
            std::cerr << "Algorithm ZMQ payload size is not aligned to double" << std::endl;
            PrintZmqStats(stats);
            return false;
        }

        received_any_response = true;
        ++stats.received_packets;
        stats.received_bytes += static_cast<std::size_t>(received);
        std::cout << "[AlgorithmZmqRx] payloadBytes=" << received
                  << " doubles=" << (received / static_cast<int>(sizeof(double)))
                  << std::endl;
#if defined(XY_WORKFLOW_DEMO_2)
        static int rx_preview_count = 0;
        const std::size_t rx_sample_count =
            response_payload.size() / (static_cast<std::size_t>(XYPARSER_MAX_CHANNELS) * sizeof(double));
        if (rx_preview_count < 3 && rx_sample_count > 0) {
            Print2ChAlgorithmPreview("[AlgorithmRxPreview]",
                                     reinterpret_cast<const double*>(response_payload.data()),
                                     rx_sample_count,
                                     XYPARSER_MAX_CHANNELS);
            ++rx_preview_count;
        }
#endif
        const int algorithm_frame_count = XYParser_FeedAlgorithmData(parser,
                                                                     response_payload.data(),
                                                                     response_payload.size(),
                                                                     nullptr,
                                                                     0);
#if defined(XY_WORKFLOW_DEMO_2)
        std::cout << "[AlgorithmFeed] frameCount=" << algorithm_frame_count << std::endl;
#endif
    }

    if (received_any_response || stats.sent_packets <= 3 || (stats.sent_packets % 100) == 0) {
        PrintZmqStats(stats);
    }
    return true;
}

bool Send64GainAndSampleRate(TcpClient& client, int gain, int sample_rate)
{
    std::array<std::uint8_t, 32> command{};
    const int size = XYParser_Serialize64GainSampleRateCommand(static_cast<std::uint8_t>(gain),
                                                               static_cast<std::uint16_t>(sample_rate),
                                                               command.data(),
                                                               command.size());
    if (size <= 0) {
        return false;
    }
    PrintBytes("Send64GainSampleRate", command.data(), static_cast<std::size_t>(size));
    return client.Send(command.data(), static_cast<std::size_t>(size));
}

bool Send2GainAndSampleRate(TcpClient& client, int gain, int sample_rate)
{
    std::array<std::uint8_t, 32> command{};
    const int size = XYParser_Serialize2ChGainSampleRateCommand(static_cast<std::uint8_t>(gain),
                                                                static_cast<std::uint16_t>(sample_rate),
                                                                command.data(),
                                                                command.size());
    if (size <= 0) {
        return false;
    }
    PrintBytes("Send2GainSampleRate", command.data(), static_cast<std::size_t>(size));
    return client.Send(command.data(), static_cast<std::size_t>(size));
}

bool Send64ImpedanceSwitch(TcpClient& client, bool open)
{
    std::array<std::uint8_t, 32> command{};
    const int size = XYParser_Serialize64ImpedanceCommand(open ? 1 : 0, command.data(), command.size());
    if (size <= 0) {
        return false;
    }
    PrintBytes(open ? "Send64ImpedanceOpen" : "Send64ImpedanceClose",
               command.data(),
               static_cast<std::size_t>(size));
    return client.Send(command.data(), static_cast<std::size_t>(size));
}

bool Send2ImpedanceSwitch(TcpClient& client, bool open)
{
    std::array<std::uint8_t, 32> command{};
    const int size = XYParser_Serialize2ChImpedanceCommand(open ? 1 : 0, command.data(), command.size());
    if (size <= 0) {
        return false;
    }
    PrintBytes(open ? "Send2ImpedanceOpen" : "Send2ImpedanceClose",
               command.data(),
               static_cast<std::size_t>(size));
    return client.Send(command.data(), static_cast<std::size_t>(size));
}

bool Send8ImpedanceSwitch(SerialPort& port, bool open)
{
    std::array<std::uint8_t, 8> command{};
    const int size = XYParser_Serialize8ChImpedanceCommand(open ? 1 : 0, command.data(), command.size());
    if (size <= 0) {
        return false;
    }
    PrintBytes(open ? "Send8ImpedanceOpen" : "Send8ImpedanceClose",
               command.data(),
               static_cast<std::size_t>(size));
    return port.Write(command.data(), static_cast<std::size_t>(size));
}

bool Process64AlgorithmPath(XYParserHandle parser,
                            const XYParserFrameSummary& summary,
                            ZmqDuplexClient& zmq_client,
                            ZmqRoundTripStats& zmq_stats)
{
    std::array<double, XYPARSER_FRAME_ALGORITHM_VALUE_COUNT> algorithm_data{};
    if (XYParser_ConvertSampleFramesToAlgorithmData(&summary, algorithm_data.data()) == 0) {
        std::cerr << "ConvertSampleFramesToAlgorithmData failed: " << DescribeParserError(parser) << std::endl;
        return false;
    }
    return RunAlgorithmZmqRoundTrip(parser, zmq_client, algorithm_data.data(), zmq_stats);
}

bool Process8AlgorithmPath(XYParserHandle parser,
                           const XYParserFrameSummary& summary,
                           ZmqDuplexClient& zmq_client,
                           ZmqRoundTripStats& zmq_stats)
{
    XYParserFrameSummary converted{};
    if (XYParser_Convert8ChFramesTo64Ch(&summary, 1, &converted, 1) != 1) {
        std::cerr << "Convert8ChFramesTo64Ch failed" << std::endl;
        return false;
    }
    return Process64AlgorithmPath(parser, converted, zmq_client, zmq_stats);
}

bool Process2AlgorithmPath(XYParserHandle parser,
                           const XYParserFrameSummary& summary,
                           ZmqDuplexClient& zmq_client,
                           ZmqRoundTripStats& zmq_stats)
{
    XYParserFrameSummary converted{};
    if (XYParser_Convert2ChFramesTo64Ch(&summary, 1, &converted, 1) != 1) {
        std::cerr << "Convert2ChFramesTo64Ch failed" << std::endl;
        return false;
    }

    std::array<double, XYPARSER_FRAME_ALGORITHM_VALUE_COUNT> algorithm_data{};
    if (XYParser_ConvertSampleFramesToAlgorithmData(&converted, algorithm_data.data()) == 0) {
        std::cerr << "ConvertSampleFramesToAlgorithmData failed: " << DescribeParserError(parser) << std::endl;
        return false;
    }

#if defined(XY_WORKFLOW_DEMO_2)
    static int tx_preview_count = 0;
    if (tx_preview_count < 3) {
        Print2ChAlgorithmPreview("[AlgorithmTxPreview]",
                                 algorithm_data.data(),
                                 XYPARSER_SAMPLES_PER_FRAME,
                                 XYPARSER_FRAME_DATA_COLUMN_COUNT);
        ++tx_preview_count;
    }
#endif
    return RunAlgorithmZmqRoundTrip(parser, zmq_client, algorithm_data.data(), zmq_stats);
}

bool SendTrigger(SerialPort& port, XYParserTriggerType trigger_type)
{
    std::array<std::uint8_t, 8> command{};
    const int size = XYParser_SerializeTriggerCommand(static_cast<std::uint8_t>(trigger_type),
                                                      command.data(),
                                                      command.size());
    if (size <= 0) {
        return false;
    }
    PrintBytes("SendTrigger", command.data(), static_cast<std::size_t>(size));
    return port.Write(command.data(), static_cast<std::size_t>(size));
}

void PrintFrameProgress(const XYParserFrameSummary* summaries, int count)
{
    if (count <= 0) {
        return;
    }
    const XYParserFrameSummary& last = summaries[static_cast<std::size_t>(count - 1)];
#if defined(XY_WORKFLOW_DEMO_64) || defined(XY_WORKFLOW_DEMO_2)
    struct DeviceStateSnapshot {
        std::uint8_t impedance_enabled = 0;
        std::uint8_t current_gain = 0;
        std::uint16_t current_sample_rate_hz = 0;
        std::uint8_t cap_type = 0;
        std::uint8_t gnd_detached = 0;
    };

    const DeviceStateSnapshot current_state = {
        last.impedance_enabled,
        last.current_gain,
        last.current_sample_rate_hz,
        last.cap_type,
        last.gnd_detached};

    static bool has_last_state = false;
    static DeviceStateSnapshot last_state{};
    if (has_last_state &&
        current_state.impedance_enabled == last_state.impedance_enabled &&
        current_state.current_gain == last_state.current_gain &&
        current_state.current_sample_rate_hz == last_state.current_sample_rate_hz &&
        current_state.cap_type == last_state.cap_type &&
        current_state.gnd_detached == last_state.gnd_detached) {
        return;
    }

    last_state = current_state;
    has_last_state = true;
    std::cout << "[Feed] frameCount=" << count
              << " lastFrameIndex=" << last.frame_index
              << " impedanceEnabled=" << static_cast<int>(last.impedance_enabled)
              << " currentGain=" << static_cast<int>(last.current_gain)
              << " currentSampleRateHz=" << last.current_sample_rate_hz
              << " capType=" << static_cast<int>(last.cap_type)
              << " gndDetached=" << static_cast<int>(last.gnd_detached);
#else
    std::cout << "[Feed] frameCount=" << count
              << " lastFrameIndex=" << last.frame_index
              << " battery=" << static_cast<int>(last.battery);
#endif
    std::cout
              << std::endl;
}

const char* DescribeTriggerType(std::uint8_t trigger_type)
{
    switch (trigger_type) {
    case XYPARSER_TRIGGER_NONE:
        return "NONE";
    case XYPARSER_TRIGGER_TRAIN_0:
        return "TRAIN_0";
    case XYPARSER_TRIGGER_TRAIN_1:
        return "TRAIN_1";
    default:
        return "UNKNOWN";
    }
}

void PrintActiveChannelSamples(const XYParserFrameSummary& summary)
{
    std::array<XYParserLeadChannelNumber, XYPARSER_MAX_CHANNELS> leads{};
    const int lead_count = XYParser_GetLeadMap(summary.channel_count,
                                               leads.data(),
                                               static_cast<int>(leads.size()));
    const int channel_count = (lead_count > 0) ? lead_count : static_cast<int>(summary.channel_count);

    for (std::uint8_t sample_index = 0; sample_index < summary.sample_count; ++sample_index) {
        std::ostringstream channel_stream;
        int active_channel_count = 0;

        for (int channel_index = 0; channel_index < channel_count; ++channel_index) {
            const double value_uv = summary.channel_values_uv[sample_index][channel_index];
            if (value_uv == 0.0) {
                continue;
            }

            XYParserLeadChannelNumber lead = static_cast<XYParserLeadChannelNumber>(channel_index);
            if (lead_count > 0) {
                lead = leads[static_cast<std::size_t>(channel_index)];
            }
            const char* lead_name = XYParser_GetLeadName(lead);

            if (active_channel_count > 0) {
                channel_stream << " | ";
            }
            channel_stream << (lead_name != nullptr ? lead_name : "lead?")
                           << '=' << value_uv << "uV";
            ++active_channel_count;
        }

        if (active_channel_count == 0) {
            continue;
        }

        const std::uint8_t trigger_type = summary.sample_trigger_types[sample_index];
        const std::uint8_t trigger_index = summary.sample_trigger_indices[sample_index];
        std::cout << "[ActiveSample] frame=" << summary.frame_index
                  << " sample=" << static_cast<int>(sample_index)
                  << " trigger=" << static_cast<int>(trigger_type)
                  << '(' << DescribeTriggerType(trigger_type) << ')'
                  << " triggerIndex=" << static_cast<int>(trigger_index)
                  << " channels=" << channel_stream.str()
                  << std::endl;
    }
}

void PrintTriggerEvents(const XYParserFrameSummary& summary)
{
    for (std::uint8_t sample_index = 0; sample_index < summary.sample_count; ++sample_index) {
        const std::uint8_t trigger_type = summary.sample_trigger_types[sample_index];
        const std::uint8_t trigger_index = summary.sample_trigger_indices[sample_index];
        if (trigger_type == XYPARSER_TRIGGER_NONE && trigger_index == 0) {
            continue;
        }

        std::cout << "[TriggerRx] frame=" << summary.frame_index
                  << " sample=" << static_cast<int>(sample_index)
                  << " trigger=" << static_cast<int>(trigger_type)
                  << '(' << DescribeTriggerType(trigger_type) << ')'
                  << " triggerIndex=" << static_cast<int>(trigger_index)
                  << std::endl;
    }
}

int Run64Workflow(const DemoOptions& options)
{
    constexpr auto kImpedanceDuration = std::chrono::seconds(0);
    constexpr int kImpedanceSampleRate = 250;
    constexpr int kImpedanceGain = 24;
    constexpr int kNormalSampleRate = 250;
    constexpr int kNormalGain = 6;
    WinsockRuntime winsock;
    if (!winsock.ok()) {
        std::cerr << "WSAStartup failed" << std::endl;
        return 1;
    }

    TcpClient data_client;
    if (!data_client.Connect(options.tcp_host, options.tcp_port)) {
        std::cerr << "Connect 64ch TCP failed: " << options.tcp_host << ':' << options.tcp_port << std::endl;
        return 1;
    }
    bool tcp_connected = true;
    const auto close_data_tcp = [&]() {
        if (!tcp_connected) {
            return;
        }
        std::cout << "Close 64ch TCP connection" << std::endl;
        data_client.Close();
        tcp_connected = false;
    };

    SerialPort trigger_port;
    const bool has_trigger_port = !options.trigger_com_port.empty();
    bool trigger_serial_open = false;
    const auto close_trigger_serial = [&]() {
        if (!trigger_serial_open) {
            return;
        }
        std::cout << "Close trigger serial" << std::endl;
        trigger_port.Close();
        trigger_serial_open = false;
    };
    if (has_trigger_port && !trigger_port.Open(options.trigger_com_port, options.serial_baud_rate)) {
        std::cerr << "Open trigger serial failed: " << Narrow(options.trigger_com_port) << std::endl;
        close_data_tcp();
        return 1;
    }
    trigger_serial_open = has_trigger_port;

    ParserHandleGuard parser(XYParser_CreateParser(64));
    if (parser.get() == nullptr) {
        std::cerr << "Create 64ch parser failed" << std::endl;
        close_data_tcp();
        return 1;
    }

    XYParser_SetAdcParams(parser.get(), options.vref, static_cast<double>(kNormalGain));
    XYParser_SetSampleRate(parser.get(), kNormalSampleRate);
    XYParser_SetBypassChecksum(parser.get(), options.bypass_checksum);
    XYParser_SetWelchDetection(parser.get(), 0);
    XYParser_SetImpedanceDetection(parser.get(), 0);

    if (!Send64ImpedanceSwitch(data_client, true)) {
        std::cerr << "Send 64ch impedance open command failed" << std::endl;
        close_data_tcp();
        return 1;
    }
    if (!Send64GainAndSampleRate(data_client, kImpedanceGain, kImpedanceSampleRate)) {
        std::cerr << "Send 64ch impedance gain/sample-rate command failed" << std::endl;
        close_data_tcp();
        return 1;
    }
    XYParser_SetSampleRate(parser.get(), kImpedanceSampleRate);
    XYParser_SetImpedanceDetection(parser.get(), 1);

    ZmqDuplexClient algorithm_zmq;
    bool algorithm_zmq_open = false;
    const auto close_algorithm_zmq = [&]() {
        if (!algorithm_zmq_open) {
            return;
        }
        std::cout << "Close algorithm ZMQ" << std::endl;
        algorithm_zmq.Close();
        algorithm_zmq_open = false;
    };
    const auto close_all = [&]() {
        close_algorithm_zmq();
        close_trigger_serial();
        close_data_tcp();
    };
    if (!algorithm_zmq.Open(options.algorithm_host,
                            options.algorithm_port,
                            options.algorithm_timeout_ms)) {
        std::cerr << "Open algorithm ZMQ client failed"
                  << " remote=" << BuildZmqTcpEndpoint(options.algorithm_host, options.algorithm_port)
                  << " zmqError=" << algorithm_zmq.last_error()
                  << std::endl;
        close_all();
        return 1;
    }
    algorithm_zmq_open = true;
    std::cout << "Algorithm ZMQ enabled: identity=" << algorithm_zmq.identity()
              << " algorithmHost=" << options.algorithm_host
              << " algorithmPort=" << options.algorithm_port
              << std::endl;
    std::cout << "64ch impedance enabled for "
              << std::chrono::duration_cast<std::chrono::seconds>(kImpedanceDuration).count()
              << " seconds before cyclic trigger" << std::endl;
    if (has_trigger_port) {
        std::cout << "64ch cyclic trigger armed: alternating TRAIN_0/TRAIN_1 every "
                  << options.train_duration_ms << " ms" << std::endl;
    }

    XYParserTriggerType next_trigger_type = XYPARSER_TRIGGER_TRAIN_0;
    bool impedance_phase = true;
    const Clock::time_point impedance_end_time = Clock::now() + kImpedanceDuration;
    Clock::time_point next_trigger_time = Clock::time_point::max();

    std::array<std::uint8_t, 8192> read_buffer{};
    std::array<XYParserFrameSummary, 32> frame_summaries{};
    ZmqRoundTripStats zmq_stats{};

    while (true) {
        if (!impedance_phase && has_trigger_port && Clock::now() >= next_trigger_time) {
            if (!SendTrigger(trigger_port, next_trigger_type)) {
                std::cerr << "Send "
                          << (next_trigger_type == XYPARSER_TRIGGER_TRAIN_0 ? "TRAIN_0" : "TRAIN_1")
                          << " failed" << std::endl;
                close_all();
                return 1;
            }
            next_trigger_type = (next_trigger_type == XYPARSER_TRIGGER_TRAIN_0)
                ? XYPARSER_TRIGGER_TRAIN_1
                : XYPARSER_TRIGGER_TRAIN_0;
            next_trigger_time = Clock::now() + std::chrono::milliseconds(options.train_duration_ms);
        }

        bool disconnected = false;
        const int received = data_client.Receive(read_buffer.data(), static_cast<int>(read_buffer.size()), disconnected);
        if (disconnected) {
            std::cout << "64ch TCP disconnected" << std::endl;
            break;
        }
        if (received <= 0) {
            continue;
        }

        const int frame_count = XYParser_Feed(parser.get(),
                                              read_buffer.data(),
                                              static_cast<std::size_t>(received),
                                              frame_summaries.data(),
                                              static_cast<int>(frame_summaries.size()));
        if (frame_count > 0) {
            PrintFrameProgress(frame_summaries.data(), frame_count);
        }
        if (impedance_phase) {
            DrainImpedance(parser.get());
            if (Clock::now() >= impedance_end_time) {
                if (!Send64GainAndSampleRate(data_client, kNormalGain, kNormalSampleRate)) {
                    std::cerr << "Send 64ch normal gain/sample-rate command failed" << std::endl;
                    close_all();
                    return 1;
                }
                if (!Send64ImpedanceSwitch(data_client, false)) {
                    std::cerr << "Send 64ch impedance close command failed" << std::endl;
                    close_all();
                    return 1;
                }
                XYParser_SetSampleRate(parser.get(), kNormalSampleRate);
                XYParser_SetImpedanceDetection(parser.get(), 0);
                XYParser_SetWelchDetection(parser.get(), 1);
                DrainImpedance(parser.get());
                std::cout << "64ch impedance disabled after "
                          << std::chrono::duration_cast<std::chrono::seconds>(kImpedanceDuration).count()
                          << " seconds, Welch/PSD and cyclic trigger resumed" << std::endl;
                impedance_phase = false;
                next_trigger_time = Clock::now();
            }
            continue;
        }
        for (int i = 0; i < frame_count; ++i) {
            PrintTriggerEvents(frame_summaries[static_cast<std::size_t>(i)]);
            Process64AlgorithmPath(parser.get(),
                                   frame_summaries[static_cast<std::size_t>(i)],
                                   algorithm_zmq,
                                   zmq_stats);
        }
        DrainWelch(parser.get());
    }

    close_all();
    return 0;
}

int Run2Workflow(const DemoOptions& options)
{
    constexpr auto kImpedanceDuration = std::chrono::seconds(10);
    constexpr int kImpedanceSampleRate = 250;
    constexpr int kImpedanceGain = 24;
    constexpr int kNormalSampleRate = 250;
    constexpr int kNormalGain = 6;
    WinsockRuntime winsock;
    if (!winsock.ok()) {
        std::cerr << "WSAStartup failed" << std::endl;
        return 1;
    }

    TcpClient data_client;
    if (!data_client.Connect(options.tcp_host, options.tcp_port)) {
        std::cerr << "Connect 2ch TCP failed: " << options.tcp_host << ':' << options.tcp_port << std::endl;
        return 1;
    }
    bool tcp_connected = true;
    const auto close_data_tcp = [&]() {
        if (!tcp_connected) {
            return;
        }
        std::cout << "Close 2ch TCP connection" << std::endl;
        data_client.Close();
        tcp_connected = false;
    };

    SerialPort trigger_port;
    const bool has_trigger_port = !options.trigger_com_port.empty();
    bool trigger_serial_open = false;
    const auto close_trigger_serial = [&]() {
        if (!trigger_serial_open) {
            return;
        }
        std::cout << "Close trigger serial" << std::endl;
        trigger_port.Close();
        trigger_serial_open = false;
    };
    if (has_trigger_port && !trigger_port.Open(options.trigger_com_port, options.serial_baud_rate)) {
        std::cerr << "Open trigger serial failed: " << Narrow(options.trigger_com_port) << std::endl;
        close_data_tcp();
        return 1;
    }
    trigger_serial_open = has_trigger_port;

    ParserHandleGuard parser(XYParser_CreateParser(2));
    if (parser.get() == nullptr) {
        std::cerr << "Create 2ch parser failed" << std::endl;
        close_data_tcp();
        return 1;
    }

    XYParser_SetAdcParams(parser.get(), options.vref, static_cast<double>(kNormalGain));
    XYParser_SetSampleRate(parser.get(), kNormalSampleRate);
    XYParser_SetBypassChecksum(parser.get(), options.bypass_checksum);
    XYParser_SetWelchDetection(parser.get(), 0);
    XYParser_SetImpedanceDetection(parser.get(), 0);

    if (!Send2ImpedanceSwitch(data_client, true)) {
        std::cerr << "Send 2ch impedance open command failed" << std::endl;
        close_data_tcp();
        return 1;
    }
    if (!Send2GainAndSampleRate(data_client, kImpedanceGain, kImpedanceSampleRate)) {
        std::cerr << "Send 2ch impedance gain/sample-rate command failed" << std::endl;
        close_data_tcp();
        return 1;
    }
    XYParser_SetSampleRate(parser.get(), kImpedanceSampleRate);
    XYParser_SetImpedanceDetection(parser.get(), 1);

    ZmqDuplexClient algorithm_zmq;
    bool algorithm_zmq_open = false;
    const auto close_algorithm_zmq = [&]() {
        if (!algorithm_zmq_open) {
            return;
        }
        std::cout << "Close algorithm ZMQ" << std::endl;
        algorithm_zmq.Close();
        algorithm_zmq_open = false;
    };
    const auto close_all = [&]() {
        close_algorithm_zmq();
        close_trigger_serial();
        close_data_tcp();
    };
    if (!algorithm_zmq.Open(options.algorithm_host,
                            options.algorithm_port,
                            options.algorithm_timeout_ms)) {
        std::cerr << "Open algorithm ZMQ client failed"
                  << " remote=" << BuildZmqTcpEndpoint(options.algorithm_host, options.algorithm_port)
                  << " zmqError=" << algorithm_zmq.last_error()
                  << std::endl;
        close_all();
        return 1;
    }
    algorithm_zmq_open = true;
    std::cout << "Algorithm ZMQ enabled: identity=" << algorithm_zmq.identity()
              << " algorithmHost=" << options.algorithm_host
              << " algorithmPort=" << options.algorithm_port
              << std::endl;
    std::cout << "2ch impedance enabled for "
              << std::chrono::duration_cast<std::chrono::seconds>(kImpedanceDuration).count()
              << " seconds before 2ch->64ch algorithm path" << std::endl;
    std::cout << "Welch waiting: impedance phase" << std::endl;
    if (has_trigger_port) {
        std::cout << "2ch cyclic trigger armed: alternating TRAIN_0/TRAIN_1 every "
                  << options.train_duration_ms << " ms" << std::endl;
    }

    XYParserTriggerType next_trigger_type = XYPARSER_TRIGGER_TRAIN_0;
    bool impedance_phase = true;
    bool logged_waiting_algorithm_rx = false;
    const Clock::time_point impedance_end_time = Clock::now() + kImpedanceDuration;
    Clock::time_point next_trigger_time = Clock::time_point::max();

    std::array<std::uint8_t, 8192> read_buffer{};
    std::array<XYParserFrameSummary, 32> frame_summaries{};
    ZmqRoundTripStats zmq_stats{};

    while (true) {
        if (!impedance_phase && has_trigger_port && Clock::now() >= next_trigger_time) {
            if (!SendTrigger(trigger_port, next_trigger_type)) {
                std::cerr << "Send "
                          << (next_trigger_type == XYPARSER_TRIGGER_TRAIN_0 ? "TRAIN_0" : "TRAIN_1")
                          << " failed" << std::endl;
                close_all();
                return 1;
            }
            next_trigger_type = (next_trigger_type == XYPARSER_TRIGGER_TRAIN_0)
                ? XYPARSER_TRIGGER_TRAIN_1
                : XYPARSER_TRIGGER_TRAIN_0;
            next_trigger_time = Clock::now() + std::chrono::milliseconds(options.train_duration_ms);
        }

        bool disconnected = false;
        const int received = data_client.Receive(read_buffer.data(), static_cast<int>(read_buffer.size()), disconnected);
        if (disconnected) {
            std::cout << "2ch TCP disconnected" << std::endl;
            break;
        }
        if (received <= 0) {
            continue;
        }

        const int frame_count = XYParser_Feed(parser.get(),
                                              read_buffer.data(),
                                              static_cast<std::size_t>(received),
                                              frame_summaries.data(),
                                              static_cast<int>(frame_summaries.size()));
        if (frame_count > 0) {
            PrintFrameProgress(frame_summaries.data(), frame_count);
        }
        if (impedance_phase) {
            DrainImpedance(parser.get());
            if (Clock::now() >= impedance_end_time) {
                if (!Send2GainAndSampleRate(data_client, kNormalGain, kNormalSampleRate)) {
                    std::cerr << "Send 2ch normal gain/sample-rate command failed" << std::endl;
                    close_all();
                    return 1;
                }
                if (!Send2ImpedanceSwitch(data_client, false)) {
                    std::cerr << "Send 2ch impedance close command failed" << std::endl;
                    close_all();
                    return 1;
                }
                XYParser_SetSampleRate(parser.get(), kNormalSampleRate);
                XYParser_SetImpedanceDetection(parser.get(), 0);
                XYParser_SetWelchDetection(parser.get(), 1);
                DrainImpedance(parser.get());
                std::cout << "2ch impedance disabled after "
                          << std::chrono::duration_cast<std::chrono::seconds>(kImpedanceDuration).count()
                          << " seconds, Welch/PSD and 2ch->64ch algorithm path resumed" << std::endl;
                std::cout << "Welch waiting: no algorithm rx yet" << std::endl;
                logged_waiting_algorithm_rx = true;
                impedance_phase = false;
                next_trigger_time = Clock::now();
            }
            continue;
        }
        for (int i = 0; i < frame_count; ++i) {
            PrintTriggerEvents(frame_summaries[static_cast<std::size_t>(i)]);
            Process2AlgorithmPath(parser.get(),
                                  frame_summaries[static_cast<std::size_t>(i)],
                                  algorithm_zmq,
                                  zmq_stats);
        }
        if (logged_waiting_algorithm_rx && zmq_stats.received_packets > 0) {
            std::cout << "Welch input ready: algorithm rx started" << std::endl;
            logged_waiting_algorithm_rx = false;
        }
        DrainWelch(parser.get());
    }

    close_all();
    return 0;
}

int Run8Workflow(const DemoOptions& options)
{
    constexpr auto kImpedanceDuration = std::chrono::seconds(60);
    WinsockRuntime winsock;
    if (!winsock.ok()) {
        std::cerr << "WSAStartup failed" << std::endl;
        return 1;
    }

    SerialPort data_port;
    if (!data_port.Open(options.data_com_port, options.serial_baud_rate)) {
        std::cerr << "Open 8ch serial failed: " << Narrow(options.data_com_port) << std::endl;
        return 1;
    }
    bool data_serial_open = true;
    const auto close_data_serial = [&]() {
        if (!data_serial_open) {
            return;
        }
        std::cout << "Close 8ch data serial" << std::endl;
        data_port.Close();
        data_serial_open = false;
    };

    ParserHandleGuard parser(XYParser_CreateParser(8));
    if (parser.get() == nullptr) {
        std::cerr << "Create 8ch parser failed" << std::endl;
        close_data_serial();
        return 1;
    }

    XYParser_SetAdcParams(parser.get(), options.vref, static_cast<double>(options.gain));
    XYParser_SetSampleRate(parser.get(), options.sample_rate);
    XYParser_SetBypassChecksum(parser.get(), options.bypass_checksum);
    XYParser_SetWelchDetection(parser.get(), 0);
    XYParser_SetImpedanceDetection(parser.get(), 1);

    if (!Send8ImpedanceSwitch(data_port, true)) {
        std::cerr << "Send 8ch impedance open command failed" << std::endl;
        close_data_serial();
        return 1;
    }

    ZmqDuplexClient algorithm_zmq;
    bool algorithm_zmq_open = false;
    const auto close_algorithm_zmq = [&]() {
        if (!algorithm_zmq_open) {
            return;
        }
        std::cout << "Close algorithm ZMQ" << std::endl;
        algorithm_zmq.Close();
        algorithm_zmq_open = false;
    };
    const auto close_all = [&]() {
        close_algorithm_zmq();
        close_data_serial();
    };
    if (!algorithm_zmq.Open(options.algorithm_host,
                            options.algorithm_port,
                            options.algorithm_timeout_ms)) {
        std::cerr << "Open algorithm ZMQ client failed"
                  << " remote=" << BuildZmqTcpEndpoint(options.algorithm_host, options.algorithm_port)
                  << " zmqError=" << algorithm_zmq.last_error()
                  << std::endl;
        close_all();
        return 1;
    }
    algorithm_zmq_open = true;
    std::cout << "Algorithm ZMQ enabled: identity=" << algorithm_zmq.identity()
              << " algorithmHost=" << options.algorithm_host
              << " algorithmPort=" << options.algorithm_port
              << std::endl;
    std::cout << "8ch impedance enabled for " << std::chrono::duration_cast<std::chrono::seconds>(kImpedanceDuration).count()
              << " seconds, then close and resume Welch/PSD" << std::endl;
    std::array<std::uint8_t, 8192> read_buffer{};
    std::array<XYParserFrameSummary, 32> frame_summaries{};
    ZmqRoundTripStats zmq_stats{};
    bool impedance_phase = true;
    const Clock::time_point impedance_end_time = Clock::now() + kImpedanceDuration;

    while (true) {
        bool disconnected = false;
        const int received = data_port.Read(read_buffer.data(), static_cast<int>(read_buffer.size()), disconnected);
        if (disconnected) {
            std::cout << "8ch serial disconnected or read failed" << std::endl;
            break;
        }
        if (received <= 0) {
            continue;
        }

        const int frame_count = XYParser_Feed(parser.get(),
                                              read_buffer.data(),
                                              static_cast<std::size_t>(received),
                                              frame_summaries.data(),
                                              static_cast<int>(frame_summaries.size()));
        if (frame_count > 0) {
            PrintFrameProgress(frame_summaries.data(), frame_count);
        }
        if (impedance_phase) {
            DrainImpedance(parser.get());
            if (Clock::now() >= impedance_end_time) {
                if (!Send8ImpedanceSwitch(data_port, false)) {
                    std::cerr << "Send 8ch impedance close command failed" << std::endl;
                    close_all();
                    return 1;
                }
                XYParser_SetImpedanceDetection(parser.get(), 0);
                XYParser_SetWelchDetection(parser.get(), 1);
                DrainImpedance(parser.get());
                std::cout << "8ch impedance disabled after "
                          << std::chrono::duration_cast<std::chrono::seconds>(kImpedanceDuration).count()
                          << " seconds, Welch/PSD resumed" << std::endl;
                impedance_phase = false;
            }
            continue;
        }
        for (int i = 0; i < frame_count; ++i) {
            PrintTriggerEvents(frame_summaries[static_cast<std::size_t>(i)]);
            Process8AlgorithmPath(parser.get(),
                                  frame_summaries[static_cast<std::size_t>(i)],
                                  algorithm_zmq,
                                  zmq_stats);
        }
        DrainWelch(parser.get());
    }

    close_all();
    return 0;
}

} // namespace

int wmain(int argc, wchar_t* argv[])
{
    DemoOptions options{};
#if defined(XY_WORKFLOW_DEMO_64)
    constexpr int kImpedanceEnabled = 0;
    constexpr int kParserGainDuringRun = 6;
#elif defined(XY_WORKFLOW_DEMO_2)
    constexpr int kImpedanceEnabled = 0;
    constexpr int kParserGainDuringRun = 6;
#else
    constexpr int kImpedanceEnabled = 1;
    constexpr int kParserGainDuringRun = 24;
#endif
    if (!ParseArguments(argc, argv, options)) {
        PrintUsage();
        return 1;
    }

    std::cout << "bypassChecksum=" << options.bypass_checksum
              << " sampleRate=" << options.sample_rate
              << " gain=" << options.gain
              << " algorithmZmq=1"
              << " impedance=" << kImpedanceEnabled
              << " welch=1"
              << std::endl;
    std::cout << "workflowState="
              << " impedanceEnabled=" << kImpedanceEnabled
              << " parserGain=" << kParserGainDuringRun
              << " deviceGainTarget=" << options.gain
              << std::endl;
    std::cout << "algorithmPayloadDoubles=" << XYPARSER_FRAME_ALGORITHM_VALUE_COUNT
              << " algorithmPayloadBytes=" << (XYPARSER_FRAME_ALGORITHM_VALUE_COUNT * sizeof(double))
              << std::endl;

#if defined(XY_WORKFLOW_DEMO_64)
    return Run64Workflow(options);
#elif defined(XY_WORKFLOW_DEMO_2)
    return Run2Workflow(options);
#else
    return Run8Workflow(options);
#endif
}
