#include <Arduino.h>
#include <ArduinoLog.h>
#include <Component.h>
#include <enums.h>
#include <macros.h>

#include "./PHApp.h"
#include "./config-modbus.h"
#include "./config.h"
#include "Settings.h"
#include "esp32_compat.h"
#include <components/Logger.h>

#include <components/OmronE5.h>
#include <components/OmronE5Types.h>

#ifdef ENABLE_MODBUS_TCP
#include <modbus/ModbusTCP.h>
#include <modbus/ModbusTypes.h>
#endif

#include <LittleFS.h>
#include <components/RestServer.h>

#if defined(ENABLE_AMPERAGE_BUDGET_MANAGER)
void PHApp_onWarmupComplete(Component *owner) {
  PHApp *app = static_cast<PHApp *>(owner);
  if (app) {
    bool wasInitializing = false;
#ifdef ENABLE_PROFILE_TEMPERATURE
    for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; i++) {
      if (app->tempProfiles[i] != nullptr &&
          app->tempProfiles[i]->getCurrentStatus() ==
              PlotStatus::INITIALIZING) {
        wasInitializing = true;
        break;
      }
    }
#endif

    if (wasInitializing) {
      // TODO: Implement any logic that should occur when the warmup is
      // complete.
      LS_INFO("PHApp received warmup complete notification from "
              "AmperageBudgetManager.");
#ifdef ENABLE_FEEDBACK_BUZZER
      if (app->feedbackBuzzer_0) {
        app->feedbackBuzzer_0->setMode(
            FeedbackBuzzer::E_BuzzerMode::MODE_FAST_BLINK, 3000);
      }
#endif
    }
  }
}
#endif

#define MB_R_APP_STATE_REG 9
#define MB_R_SYSTEM_CMD_PRINT_RESET 1
#define MB_R_SYSTEM_CMD_PRINT_REGS 2
#define MB_R_SYSTEM_CMD_PRINT_MEMORY 5
#define MB_R_SYSTEM_CMD_PRINT_VFD 6

#ifdef ENABLE_PROFILER
uint32_t PHApp::initialFreeHeap = 0;
uint64_t PHApp::initialCpuTicks = 0;
#endif

#ifndef LOG_LEVEL
#define LOG_LEVEL LOG_LEVEL_VERBOSE
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Network Servers
//
#if defined(ENABLE_WEBSERVER)
WiFiServer server(80);
#endif

#ifdef ENABLE_MODBUS_TCP
ModbusServerTCPasync mb;
#endif

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Omron E5
//
#undef ENABLE_TRUTH_COLLECTOR

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Factory : Instances
//
#define ADD_RELAY(relayNum, relayPin, relayKey, relayAddr)                     \
  relay_##relayNum = new Relay(this, relayPin, relayKey, relayAddr);           \
  components.push_back(relay_##relayNum);

#ifdef ENABLE_SOLENOID_0
#define ADD_SOLENOID(solenoidNum, solenoidPin, solenoidKey, solenoidAddr)      \
  solenoid_##solenoidNum =                                                     \
      new Solenoid(this, solenoidPin, solenoidKey, solenoidAddr);              \
  components.push_back(solenoid_##solenoidNum);
#endif

#define ADD_POT(potNum, potPin, potKey, potAddr, ...)                          \
  pot_##potNum = new POT(this, potPin, potKey, potAddr, ##__VA_ARGS__);        \
  components.push_back(pot_##potNum);

#define ADD_POS3ANALOG(posNum, switchPin1, switchPin2, switchKey, switchAddr)  \
  pos3Analog_##posNum =                                                        \
      new Pos3Analog(this, switchPin1, switchPin2, switchKey, switchAddr);     \
  components.push_back(pos3Analog_##posNum);

#ifdef ENABLE_PID
#define ADD_PID(pidNum, nameStr, doPin, csPin, clkPin, outPin, key)            \
  pidController_##pidNum =                                                     \
      new PIDController(key, nameStr, doPin, csPin, clkPin, outPin);           \
  components.push_back(pidController_##pidNum);
#endif

Logger *g_logger = nullptr;

void PHApp::printRegisters() {
  L_VERBOSE(F("--- Entering PHApp::printRegisters ---"));

#if ENABLED(HAS_MODBUS_REGISTER_DESCRIPTIONS)
  Log.setShowLevel(false);
  Serial.print("| Name | ID  | Address | RW |  Function Code | Number "
               "Addresses |Register Description| \n");
  Serial.print("|------|----------|----|----|----|----|-------|\n");
  short size = components.size();
  L_VERBOSE(F("PHApp::printRegisters - Processing %d components..."), size);
  for (int i = 0; i < size; i++) {
    Component *component = components[i];
    if (!component) {
      L_ERROR(F("PHApp::printRegisters - Found NULL component at index %d"), i);
      continue;
    }
    L_VERBOSE(F("PHApp::printRegisters - Component %d: ID=%d, Name=%s"), i,
              component->id, component->name.c_str());
    // if (!(component->nFlags & 1 << OBJECT_NET_CAPS::E_NCAPS_MODBUS)) // <--
    // Modbus flag check might be different now
    // {
    //   continue;
    // }
    // Log.verbose("| %s | %d | %d | %s | %d | %d | %s |\n", // <-- Calls to
    // removed ModbusGateway methods
    //             component->name.c_str(),
    //             component->id,
    //             component->getAddress(),
    //             component->getRegisterMode(),
    //             component->getFunctionCode(),
    //             component->getNumberAddresses(),
    //             component->getRegisterDescription().c_str());
    Log.verbose("| %s | %d | - | - | - | - | - |\n", // <-- Simplified output
                component->name.c_str(), component->id);
  }
  Log.setShowLevel(true);
#endif
  L_VERBOSE(F("--- Exiting PHApp::printRegisters ---"));
}
short PHApp::reset(short val0, short val1) {
  _state = APP_STATE::RESET;
  _error = E_OK;

#if defined(ESP32) ||                                                          \
    defined(ESP8266) // Use ESP.restart() for ESP32 and ESP8266
  ESP.restart();
#else
  return E_NOT_IMPLEMENTED;
#endif
  return E_OK;
}
short PHApp::list(short val0, short val1) {
  uchar s = components.size();
  for (uchar i = 0; i < s; i++) {
    Component *component = components[i];
    if (component) {
      L_VERBOSE("PHApp::list - %d | %s (ID: %d)", i, component->name.c_str(),
                component->id);
    } else {
      Log.warningln("PHApp::list - NULL component at index %d", i);
    }
  }
  return E_OK;
}
short PHApp::setup() {
  _state = APP_STATE::RESET;
  _error = E_OK;
#ifdef ENABLE_PROFILER
  if (initialFreeHeap == 0 && initialCpuTicks == 0) {
    initialFreeHeap = ESP.getFreeHeap();
    initialCpuTicks = esp_cpu_get_cycle_count();
  }
#endif

#ifndef DISABLE_SERIAL_LOGGING
  // Serial Setup
  Serial.begin(SERIAL_BAUD_RATE);
  while (!Serial && !Serial.available()) {
  }
#endif

#ifdef ENABLE_LOGGER
  logger_0 = new Logger(this, COMPONENT_KEY_LOGGER, MB_ADDR_LOGGER_0);
  g_logger = logger_0;
  components.push_back(logger_0);
#endif

#ifndef DISABLE_SERIAL_LOGGING
  // Log Setup
#if defined(ENABLE_LOGGER)
#if defined(ENABLE_LOGGING_TARGET_PRINT)
  if (logger_0) {
    Serial.begin(SERIAL_BAUD_RATE);
    logger_0->addPrintTarget(&Serial);
  }
#endif
  Log.begin(LOG_LEVEL, logger_0);
#else
  Log.begin(LOG_LEVEL, &Serial);
#endif
  Log.setShowLevel(true);
#else
  // Log Setup (without Serial)
  Log.begin(LOG_LEVEL_WARNING, nullptr);
  Log.setShowLevel(false);
#endif

  L_INFO("PHApp::setup() started. Logger configured.");

  // Application stuff
#ifdef ENABLE_RUNTIME_STATE
  runtimeState = new RuntimeState(this, COMPONENT_KEY_RUNTIME_STATE);
  components.push_back(runtimeState);
#endif

// Components
#ifdef ENABLE_SERIAL_BRIDGE
  bridge = new Bridge(this);
  components.push_back(bridge);
#endif
#ifndef DISABLE_SERIAL_LOGGING
  com_serial = new SerialMessage(Serial, bridge);
  components.push_back(com_serial);
#endif
  // Network
  short networkSetupResult = setupNetwork();
  if (networkSetupResult != E_OK) {
    L_ERROR("Network setup failed with error code: %d", networkSetupResult);
  }

#ifdef ENABLE_SETTINGS
  appSettings = new Settings(this);
  components.push_back(appSettings);

#endif

#ifdef ENABLE_LOGGER
#if defined(ENABLE_LOGGING_TARGET_WEBSOCKET) && defined(ENABLE_WEBSERVER) &&   \
    defined(ENABLE_WEBSOCKET)
  if (logger_0 && webServer) {
    logger_0->addWebSocketTarget(webServer);
    L_INFO("WebSocket logging target added.");
  }
#endif
#if defined(ENABLE_LOGGING_TARGET_FILE) && defined(ENABLE_LITTLEFS)
  if (logger_0) {
    logger_0->addFileTarget();
    L_INFO("File logging target added.");
  }
#endif
#endif

  // Components
#ifdef ENABLE_RELAYS
#ifdef AUX_RELAY_0
  ADD_RELAY(0, AUX_RELAY_0, COMPONENT_KEY_RELAY_0, MB_ADDR_AUX_5);
#endif
#ifdef AUX_RELAY_1
  ADD_RELAY(1, AUX_RELAY_1, COMPONENT_KEY_RELAY_1, MB_ADDR_AUX_6);
#endif
#endif

#ifdef ENABLE_POT0
  ADD_POT(0, MB_ANALOG_0, COMPONENT_KEY_ANALOG_0, MB_ADDR_AUX_2,
          POTDampingAlgorithm::DAMPING_EMA, true);
#endif

#ifdef ENABLE_POT1
  ADD_POT(1, MB_ANALOG_1, COMPONENT_KEY_ANALOG_1, MB_ADDR_AUX_3,
          POTDampingAlgorithm::DAMPING_EMA, true);
#endif

#if (defined(AUX_ANALOG_3POS_SWITCH_0) && (defined(AUX_ANALOG_3POS_SWITCH_1)))
  ADD_POS3ANALOG(0, AUX_ANALOG_3POS_SWITCH_0, AUX_ANALOG_3POS_SWITCH_1,
                 COMPONENT_KEY_MB_ANALOG_3POS_SWITCH_0, MB_ADDR_AUX_3);
#endif

#if (defined(MB_ANALOG_3POS_SWITCH_2) && (defined(MB_ANALOG_3POS_SWITCH_3)))
  // ADD_POS3ANALOG(1, MB_ANALOG_3POS_SWITCH_2, MB_ANALOG_3POS_SWITCH_3,
  // COMPONENT_KEY_MB_ANALOG_3POS_SWITCH_1, MB_R_SWITCH_1); // <-- Temporarily
  // disable
#endif

#ifdef MB_GPIO_MB_MAP_7
  // --- Define configuration for the MB_GPIO group ---
  std::vector<GPIO_PinConfig> gpioConfigs;
  gpioConfigs.reserve(2); // Reserve space for 2 elements
  gpioConfigs.push_back(GPIO_PinConfig(
      E_GPIO_7,                         // pinNumber: The physical pin to manage
      E_GPIO_TYPE_ANALOG_INPUT,         // pinType: Treat as analog input
      300,                              // startAddress: Modbus register address
      E_FN_CODE::FN_READ_HOLD_REGISTER, // type: Map to a Holding Register
      MB_ACCESS_READ_ONLY,              // access: Allow Modbus read only
      1000,        // opIntervalMs: Update interval in milliseconds
      "GPIO_6",    // name: Custom name for this pin
      "GPIO_Group" // group: Group  name for this pin
      ));

  gpioConfigs.push_back(GPIO_PinConfig(
      E_GPIO_15,                        // pinNumber: The physical pin to manage
      E_GPIO_TYPE_ANALOG_INPUT,         // pinType: Treat as analog input
      301,                              // startAddress: Modbus register address
      E_FN_CODE::FN_READ_HOLD_REGISTER, // type: Map to a Holding Register
      MB_ACCESS_READ_ONLY,              // access: Allow Modbus read only
      1000,        // opIntervalMs: Update interval in milliseconds
      "GPIO_15",   // name: Custom name for this pin
      "GPIO_Group" // group: Group name for this pin
      ));
  const short gpioGroupId = COMPONENT_KEY_GPIO_MAP; // Using defined key
  gpio_0 = new MB_GPIO(this, gpioGroupId, gpioConfigs);
  components.push_back(gpio_0);
#endif

#ifdef PIN_ANALOG_LEVEL_SWITCH_0
  analogLevelSwitch_0 =
      new AnalogLevelSwitch(this,                      // owner
                            PIN_ANALOG_LEVEL_SWITCH_0, // analogPin
                            ALS_0_NUM_LEVELS,          // numLevels
                            ALS_0_ADC_STEP,            // levelStep
                            ALS_0_ADC_OFFSET,          // adcValueOffset
                            ID_ANALOG_LEVEL_SWITCH_0,  // id
                            ALS_0_MB_ADDR              // modbusAddress
      );
  if (analogLevelSwitch_0) {
    components.push_back(analogLevelSwitch_0);
    L_INFO(F("AnalogLevelSwitch_0 initialized. Pin:%d, ID:%d, Levels:%d, "
             "Step:%d, Offset:%d, Smooth:%d(Fixed), Debounce:%d(Fixed), MB:%d"),
           PIN_ANALOG_LEVEL_SWITCH_0, ID_ANALOG_LEVEL_SWITCH_0,
           ALS_0_NUM_LEVELS, ALS_0_ADC_STEP, ALS_0_ADC_OFFSET,
           ALS_SMOOTHING_SIZE, ALS_DEBOUNCE_COUNT, ALS_0_MB_ADDR);
  } else {
    L_ERROR(F("AnalogLevelSwitch_0 initialization failed."));
  }
#endif

#ifdef ENABLE_STATUS
  statusLight_0 = new StatusLight(
      this, STATUS_WARNING_PIN, COMPONENT_KEY_FEEDBACK_0,
      MB_MONITORING_STATUS_FEEDBACK_0); // Keep original address for now, add to
                                        // config-modbus.h later if needed
  components.push_back(statusLight_0);
  statusLight_1 = new StatusLight(
      this, STATUS_ERROR_PIN, COMPONENT_KEY_FEEDBACK_1,
      MB_MONITORING_STATUS_FEEDBACK_1); // Keep original address for now
  components.push_back(statusLight_1);
#else
  statusLight_0 = NULL;
  statusLight_1 = NULL;
#endif
  L_INFO("PHApp::setup - Base App::setup() called.");

#ifdef PIN_LED_FEEDBACK_0
  ledFeedback_0 = new LEDFeedback(this,                  // owner
                                  PIN_LED_FEEDBACK_0,    // pin
                                  LED_PIXEL_COUNT_0,     // pixelCount
                                  ID_LED_FEEDBACK_0,     // id
                                  LED_FEEDBACK_0_MB_ADDR // modbusAddress
  );
  if (ledFeedback_0) {
    components.push_back(ledFeedback_0);
    L_INFO(F("LEDFeedback_0 initialized. Pin:%d, Count:%d, ID:%d, MB:%d"),
           PIN_LED_FEEDBACK_0, LED_PIXEL_COUNT_0, ID_LED_FEEDBACK_0,
           LED_FEEDBACK_0_MB_ADDR);
  } else {
    L_ERROR(F("LEDFeedback_0 initialization failed."));
  }
#endif

#ifdef ENABLE_JOYSTICK
  joystick_0 = new Joystick(this,               // owner
                            PIN_JOYSTICK_UP,    // UP pin
                            PIN_JOYSTICK_DOWN,  // DOWN pin
                            PIN_JOYSTICK_LEFT,  // LEFT pin
                            PIN_JOYSTICK_RIGHT, // RIGHT pin
                            MB_ADDR_AUX_7       // modbusAddress
  );
  if (joystick_0) {
    components.push_back(joystick_0);
  } else {
    L_ERROR(F("Joystick_0 initialization failed."));
  }
#endif

#ifdef PIN_AUX_1

  pushButton_1 = new PushButton(this, PIN_AUX_1, COMPONENT_KEY_PUSH_BUTTON_0,
                                MB_IREG_ANALOG_0);
  components.push_back(pushButton_1);

#endif

#ifdef ENABLE_FEEDBACK_3C
  feedback3C_0 = new Feedback3C(this, GPIO_PIN_CH4, GPIO_PIN_CH5, GPIO_PIN_CH6,
                                COMPONENT_KEY_FEEDBACK_0, MB_ADDR_FEEDBACK_0);
  components.push_back(feedback3C_0);
#endif

#ifdef ENABLE_FEEDBACK_BUZZER
  feedbackBuzzer_0 = new FeedbackBuzzer(
      this, GPIO_PIN_CH3, COMPONENT_KEY_FEEDBACK_1, MB_ADDR_AUX_9);
  components.push_back(feedbackBuzzer_0);
#endif

// Systems : Hydraulic Cylinder - Loadcell sensor - RS485
#ifdef ENABLE_SOLENOID_0
  uint32_t solenoid0Addr =
      appSettings->get("SOLENOID_0_MB_ADDR", (uint32_t)MB_ADDR_SOLENOID_0);
  ADD_SOLENOID(0, PIN_SOLENOID_0, COMPONENT_KEY_SOLENOID_0, solenoid0Addr);
#endif

#ifdef ENABLE_SOLENOID_1
  uint32_t solenoid1Addr =
      appSettings->get("SOLENOID_1_MB_ADDR", (uint32_t)MB_ADDR_SOLENOID_1);
  ADD_SOLENOID(1, PIN_SOLENOID_1, COMPONENT_KEY_SOLENOID_1, MB_ADDR_SOLENOID_1);
#endif

#ifdef ENABLE_LOADCELL_0
  uint32_t slaveId0 =
      appSettings->get("LOADCELL_SLAVE_ID_0", (uint32_t)LOADCELL_SLAVE_ID_0);
  loadCell_0 = new Loadcell(this, slaveId0, LOADCELL_READ_INTERVAL,
                            COMPONENT_KEY_LOADCELL_0);
  components.push_back(static_cast<RTU_Base *>(loadCell_0));
#endif

#ifdef ENABLE_LOADCELL_1
  uint32_t slaveId1 =
      appSettings->get("LOADCELL_SLAVE_ID_1", (uint32_t)LOADCELL_SLAVE_ID_1);
  loadCell_1 = new Loadcell(this, slaveId1, LOADCELL_READ_INTERVAL,
                            COMPONENT_KEY_LOADCELL_1);
  components.push_back(static_cast<RTU_Base *>(loadCell_1));
#endif

#ifdef ENABLE_PRESS_CYLINDER
  pressCylinder_0 =
      new PressCylinder(this, COMPONENT_KEY_PRESS_CYLINDER_0,
                        MB_ADDR_PRESS_CYLINDER_0, joystick_0, pushButton_1);
#ifdef ENABLE_LOADCELL_0
  if (loadCell_0)
    pressCylinder_0->addLoadcell(loadCell_0);
#endif
#ifdef ENABLE_LOADCELL_1
  if (loadCell_1)
    pressCylinder_0->addLoadcell(loadCell_1);
#endif
#ifdef ENABLE_SOLENOID_0
  if (solenoid_0)
    pressCylinder_0->addSolenoid(solenoid_0);
#endif
#ifdef ENABLE_SOLENOID_1
  if (solenoid_1)
    pressCylinder_0->addSolenoid(solenoid_1);
#endif
  components.push_back(pressCylinder_0);
#endif

#ifdef ENABLE_OPERATOR_SWITCH
  operatorSwitch_0 = new OperatorSwitch(
      this, PIN_OPERATOR_SWITCH_STOP, PIN_OPERATOR_SWITCH_CYCLE,
      COMPONENT_KEY_OPERATOR_SWITCH, MB_ADDR_AUX_8);
  components.push_back(operatorSwitch_0);
#endif

#ifdef ENABLE_MODBUS_MIRROR
  modbusMirror_0 = new ModbusMirror(this, COMPONENT_KEY_MODBUS_MIRROR);
  components.push_back(modbusMirror_0);
#endif

#ifdef ENABLE_IFTTT
  iftttWebhook = new IFTTTWebhook(this, COMPONENT_KEY_IFTTT);
  components.push_back(iftttWebhook);
#endif

#ifdef ENABLE_NETWORK_VALUE_TEST
  L_INFO("PHApp::setup - Initializing NetworkValueTest...");
  networkValueTest =
      new NetworkValueTest(this, COMPONENT_KEY_TEST_NV, MB_ADDR_AUX_TEST_NV);
  networkValueTest->owner = this;
  components.push_back(networkValueTest);
#endif

#ifdef ENABLE_NETWORK_VALUE_TEST_PB
  L_INFO("PHApp::setup - Initializing NetworkValueTestPB...");
  networkValueTestPB = new NetworkValueTestPB(this, COMPONENT_KEY_TEST_NV_PB,
                                              MB_ADDR_AUX_TEST_NV + 10);
  networkValueTestPB->owner = this;
  components.push_back(networkValueTestPB);
#endif

  // Motors
#ifdef ENABLE_SAKO_VFD
  vfd_0 = new SAKO_VFD(this, MB_SAKO_VFD_SLAVE_ID, MB_SAKO_VFD_READ_INTERVAL);
  components.push_back(vfd_0);
#endif

#ifdef ENABLE_DELTA_VFD
  vfd_2 =
      new DELTA_VFD(this, MB_DELTA_VFD_SLAVE_ID, MB_DELTA_VFD_READ_INTERVAL);
  components.push_back(vfd_2);
#endif

  // Temperature
#ifdef ENABLE_PROFILE_TEMPERATURE
  for (ushort i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i) {
    // Assign unique ID: COMPONENT_KEY_PROFILE_START (910) + slot index
    ushort profileComponentId = COMPONENT_KEY_PROFILE_START + i;
    ushort baseAddr =
        MB_HREG_TEMP_PROFILE_BASE + (i * TEMP_PROFILE_REGISTER_COUNT);
    tempProfiles[i] =
        new TemperatureProfile(this, i, profileComponentId, baseAddr);
    tempProfiles[i]->setEventsDelegate(this);
    components.push_back(tempProfiles[i]);
  }

#endif

#ifdef ENABLE_PROFILE_PRESSURE
  for (ushort i = 0; i < PROFILE_PRESSURE_COUNT; ++i) {
    ushort profileComponentId = COMPONENT_KEY_PRESSURE_PROFILE_START + i;
    ushort baseAddr =
        MB_HREG_PRESSURE_PROFILE_BASE + (i * PRESSURE_PROFILE_REGISTER_COUNT);
    pressureProfiles[i] =
        new PressureProfile(this, i, profileComponentId, baseAddr);
    pressureProfiles[i]->setEventsDelegate(this);
    components.push_back(pressureProfiles[i]);
  }
#endif

#ifdef ENABLE_PROFILE_SIGNAL_PLOT
  for (int i = 0; i < PROFILE_SIGNAL_PLOT_COUNT; i++) {
    ushort profileComponentId = COMPONENT_KEY_SIGNAL_PLOT_START + i;
    ushort modbusAddress =
        MB_HREG_SIGNAL_PLOT_BASE + (i * SIGNAL_PLOT_REGISTER_COUNT);
    signalPlots[i] = new SignalPlot(this, i, profileComponentId, modbusAddress);
    signalPlots[i]->setEventsDelegate(this);
    components.push_back(signalPlots[i]);
#ifdef ENABLE_MODBUS_TCP
    if (modbusManager != nullptr) {
      signalPlots[i]->setModbusTCP(modbusManager);
    } else {
      L_ERROR("PHApp::setup - SignalPlot %d: modbusManager is nullptr", i);
    }
#endif
  }
#endif

#ifdef ENABLE_PID
  const int8_t PID2_THERMO_DO = 19;  // Example MISO pin
  const int8_t PID2_THERMO_CS = 5;   // Example Chip Select pin
  const int8_t PID2_THERMO_CLK = 18; // Example SCK pin
  const int8_t PID2_OUTPUT_PIN = 23; // Example PWM/Output pin
  ADD_PID(0, "PID Temp Controller 2", PID2_THERMO_DO, PID2_THERMO_CS,
          PID2_THERMO_CLK, PID2_OUTPUT_PIN, COMPONENT_KEY_PID_2);
#endif
// RS485
#ifdef ENABLE_RS485
  rs485 = new RS485(this);
  components.push_back(rs485);
#endif

#ifdef ENABLE_AMPERAGE_BUDGET_MANAGER
  pidManagerAmperage =
      new AmperageBudgetManager(this, MB_ADDR_AMPERAGE_BUDGET_BASE);
#if defined(ENABLE_OMRON_E5) && defined(ENABLE_PROFILE_TEMPERATURE)
  pidManagerAmperage->setCanUseCallback(&PHApp_canUsePID);
  pidManagerAmperage->setOnWarmupCompleteCallback(&PHApp_onWarmupComplete);
#endif
  components.push_back(pidManagerAmperage);
#endif

// Systems : Extruder
#ifdef ENABLE_EXTRUDER
  extruder_0 = new Extruder(this, vfd_0, nullptr, nullptr, nullptr);
  components.push_back(extruder_0);
#endif

// Systems : Injector
#ifdef ENABLE_PLUNGER
  plunger_0 = new Plunger(this, vfd_2, joystick_0, pot_0, pot_1);
  components.push_back(plunger_0);
  plunger_0->setEventsDelegate(this);
#endif

#ifdef ENABLE_INFLUXDB
  influxDb_0 = new InfluxDB(this, COMPONENT_KEY_INFLUXDB);
  components.push_back(influxDb_0);
#endif

  registerComponents(bridge);

#ifdef ENABLE_BRIDGE
  serial_register(bridge);
#endif

  App::setup();

#ifdef ENABLE_SETTINGS
  loadAppSettings();
#endif

  init(0, 0);
  onRun();
  return E_OK;
}

short PHApp::onRun() {
  App::onRun();

#ifdef ENABLE_MODBUS_TCP
  for (Component *comp : components) {
    if (comp && comp->hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS)) {
      comp->mb_tcp_register(modbusManager);
    }
  }
#endif

  load(0, 0);

#ifdef ENABLE_RELAYS
#ifdef AUX_RELAY_0
  relay_0->setValue(1);
#endif
#endif
  ///////////////////////////////////////////////////////////////////////////////////////////////
  //
  // Post initialization
  //
#ifdef ENABLE_WEBSERVER
  registerRoutes(webServer);
#endif

#if ENABLED(ENABLE_AMPERAGE_BUDGET_MANAGER, ENABLE_OMRON_E5)
  RTU_Base *const *devices = rs485->deviceManager.getDevices();
  int numDevicesInManager = rs485->deviceManager.getMaxDevices();
  for (int i = 0; i < numDevicesInManager; ++i) {
    if (devices[i] != nullptr) {
      Component *comp = devices[i];
      if (comp == nullptr || comp->type != COMPONENT_TYPE::COMPONENT_TYPE_PID) {
        continue;
      }

      if (!pidManagerAmperage->addManagedDevice(static_cast<OmronE5 *>(comp))) {
        L_ERROR("Failed to add OmronE5 device to AmperageBudgetManager");
      }
    }
  }
#endif

#if ENABLED(ENABLE_TEMPERATURE_PROFILES, ENABLE_OMRON_E5, ENABLE_MODBUS_TCP)
  tempProfiles[0]->disable();
  if (tempProfiles[0] && rs485) {
    RTU_Base *const *devices = rs485->deviceManager.getDevices();
    int numDevicesInManager = rs485->deviceManager.getMaxDevices();
    int targetRegisterIndex = 0; // Dedicated index for _targetRegisters
    for (int i = 0; i < numDevicesInManager; i++) {
      Component *comp = devices[i];
      if (comp == nullptr || comp->type != COMPONENT_TYPE::COMPONENT_TYPE_PID) {
        continue;
      }
    }
  }
#endif

#ifdef ENABLE_NETWORK_VALUE_TEST
  // networkValueTest->init();
#endif

#ifdef ENABLE_RUNTIME_STATE
  if (runtimeState) {
    for (auto *component : components) {
      if (component && component->hasPersistence(E_PF_ENABLED)) {
        runtimeState->addManagedComponent(component);
      }
    }
  }
#endif
  restoreState();
  // setAllOmronComWrite(true, false);
  return E_OK;
}
short PHApp::serial_register(Bridge *bridge) {
  /*
  bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this,
  C_STR("list"), (ComponentFnPtr)&PHApp::list);
  bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this,
  C_STR("reset"), (ComponentFnPtr)&PHApp::reset);
  bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this,
  C_STR("printRegisters"), (ComponentFnPtr)&PHApp::printRegisters);
  bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this,
  C_STR("load"), (ComponentFnPtr)&PHApp::load);
  */
  return E_OK;
}
short PHApp::onWarning(short code) { return E_OK; }
short PHApp::onError(short id, short code) {
  if (code == getLastError()) {
    return code;
  }
  Log.error(F("* App:onError - component=%d code=%d" CR), id, code);
  setLastError(code);
#ifdef ENABLE_STATUS
  if (statusLight_0) {
    switch (id) {
    case COMPONENT_KEY_PLUNGER: {

#if defined(ENABLE_PLUNGER)
    case (short)PlungerState::IDLE: {
      statusLight_1->set(0, 0);
      break;
    }
    case (short)PlungerState::JAMMED: {
      statusLight_1->set(1, 1);
      break;
    }
    default: {
      statusLight_1->set(1, 0);
      break;
    }
    }
#endif
    }
  }
}
#endif
return code;
}
short PHApp::onStop(short val) { return E_OK; }
short PHApp::clearError() {
  setLastError(E_OK);
  return E_OK;
}

short PHApp::loop() {
  App::loop();

  if (millis() - _last_cycle_loop_ms >= LOOP_CYCLE_INTERVAL) {
    _last_cycle_loop_ms = millis();
    loopCycle();
#ifdef ENABLE_PROFILE_TEMPERATURE
    loopProfiles();
#endif
  }

#ifdef ENABLE_RUNTIME_STATE
  if (runtimeState &&
      (millis() - _last_runtime_save_ms >= RUNTIME_STATE_SAVE_INTERVAL)) {
    _last_runtime_save_ms = millis();
    updateRuntimeState();
  }
#endif

#ifdef ENABLE_INFLUXDB
  testInfluxDB();
#endif

#ifdef ENABLE_WEBSERVER
  loopWeb();
#endif
#ifdef ENABLE_MODBUS_TCP
  loopModbus();
#endif
  return E_OK;
}

short PHApp::loopWeb() {
#if defined(ENABLE_WIFI) && defined(ENABLE_WEBSERVER)
  if (webServer != nullptr) {
    webServer->loop();
  }
#endif
  return E_OK;
}
short PHApp::getAppState(short val) { return _state; }
PHApp::PHApp() : App() {
  name = "PHApp";
  webServer = nullptr;
  pidController_0 = nullptr;
  bridge = nullptr;
  com_serial = nullptr;
  pot_0 = nullptr;
  pot_1 = nullptr;
  pot_2 = nullptr;
  statusLight_0 = nullptr;
  statusLight_1 = nullptr;
  relay_0 = nullptr;
  relay_1 = nullptr;
  relay_2 = nullptr;
  relay_3 = nullptr;
  relay_4 = nullptr;
  relay_5 = nullptr;
  relay_6 = nullptr;
  relay_7 = nullptr;
  pos3Analog_0 = nullptr;
  pos3Analog_1 = nullptr;
  modbusManager = nullptr;
  logPrinter = nullptr;
  rs485 = nullptr;
  joystick_0 = nullptr;
  networkValueTest = nullptr;
  networkValueTestPB = nullptr;
  feedback3C_0 = nullptr;
  feedbackBuzzer_0 = nullptr;
  operatorSwitch_0 = nullptr;
  solenoid_0 = nullptr;
  pressCylinder_0 = nullptr;
  appSettings = nullptr;
#ifdef ENABLE_PROFILE_TEMPERATURE
  // Initialize the array elements to nullptr
  for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i) {
    tempProfiles[i] = nullptr;
  }
#endif

#ifdef ENABLE_PROFILE_PRESSURE
  for (int i = 0; i < PROFILE_PRESSURE_COUNT; ++i) {
    pressureProfiles[i] = nullptr;
  }
#endif

#ifdef ENABLE_PROFILE_SIGNAL_PLOT
  for (int i = 0; i < PROFILE_SIGNAL_PLOT_COUNT; ++i) {
    signalPlots[i] = nullptr;
  }
#endif

#ifdef ENABLE_INFLUXDB
  influxDb_0 = nullptr;
#endif

  // WiFi settings are now initialized by WiFiNetworkSettings constructor
}
void PHApp::cleanupComponents() {
  L_INFO("PHApp::cleanupComponents - Cleaning up %d components...",
         components.size());
  for (Component *comp : components) {
    delete comp;
  }
  components.clear(); // Clear the vector AFTER deleting objects

  // Nullify pointers that were manually managed or outside the vector
  bridge = nullptr;
  com_serial = nullptr;
  pot_0 = nullptr;
  pot_1 = nullptr;
  pot_2 = nullptr;
  statusLight_0 = nullptr;
  statusLight_1 = nullptr;
  relay_0 = nullptr;
  relay_1 = nullptr;
  relay_2 = nullptr;
  relay_3 = nullptr;
  relay_4 = nullptr;
  relay_5 = nullptr;
  relay_6 = nullptr;
  relay_7 = nullptr;
  pos3Analog_0 = nullptr;
  pos3Analog_1 = nullptr;
  pidController_0 = nullptr;
  modbusManager = nullptr;
  joystick_0 = nullptr;
  networkValueTest = nullptr;
  networkValueTestPB = nullptr;
  feedback3C_0 = nullptr;
  feedbackBuzzer_0 = nullptr;
  operatorSwitch_0 = nullptr;
  solenoid_0 = nullptr;
  pressCylinder_0 = nullptr;
  appSettings = nullptr;
#ifdef ENABLE_PROFILE_TEMPERATURE
  // Clean up temperature profiles (they were also added to components vector,
  // so already deleted there) Ensure the pointers in the array are nulled
  for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i) {
    tempProfiles[i] = nullptr;
  }
#endif
#ifdef ENABLE_PROFILE_PRESSURE
  for (int i = 0; i < PROFILE_PRESSURE_COUNT; ++i) {
    pressureProfiles[i] = nullptr;
  }
#endif
#ifdef ENABLE_WEBSERVER
  if (webServer) {
    delete webServer;
    webServer = nullptr;
  }
#endif
#ifdef ENABLE_RS485
  rs485 = nullptr; // RS485 interface was in the vector, already deleted
#endif
#ifdef ENABLE_MODBUS_MIRROR
  delete modbusMirror_0;
#endif
#ifdef ENABLE_INFLUXDB
  delete influxDb_0;
#endif
#ifdef ENABLE_IFTTT
  delete iftttWebhook;
#endif
  L_INFO("PHApp::cleanupComponents - Cleanup complete.");
}
PHApp::~PHApp() { cleanupComponents(); }
short PHApp::setAppState(short newState) {
  if (_state != newState) {
    _state = (APP_STATE)newState;
  }
  return E_OK;
}

#ifdef ENABLE_SETTINGS
short PHApp::loadAppSettings() {
  if (!appSettings->load("/settings.json")) {
    L_WARN("Failed to load settings, using defaults.");
  }

  // Initialize App Settings
  setAppWarmup(appSettings->get("ALWAYS_WARMUP", true), false);
  setAppPidLag(appSettings->get("PID_LAG_COMPENSATION", true), false);

#if defined(ENABLE_OMRON_E5) && defined(ENABLE_RS485)
  if (!rs485) {
    L_ERROR("RS485 not initialized, cannot apply settings to Omron devices.");
    return E_NOT_INITIALIZED;
  }

  RTU_Base *const *devices = rs485->deviceManager.getDevices();
  int numDevicesInManager = rs485->deviceManager.getMaxDevices();

  for (int i = 0; i < numDevicesInManager; ++i) {
    RTU_Base *device = devices[i];
    if (device == nullptr ||
        device->type != COMPONENT_TYPE::COMPONENT_TYPE_PID) {
      continue;
    }

    bool foundInSettings = false;
    bool shouldBeEnabled = false;

    for (uint8_t p = 0; p < appSettings->partitionCount; ++p) {
      PartitionConfig &partition = appSettings->partitions[p];
      for (uint8_t c = 0; c < partition.controllerCount; ++c) {
        ControllerConfig &controller = partition.controllers[c];
        if (device->slaveId == controller.slaveId) {
          foundInSettings = true;
          shouldBeEnabled = controller.enabled;
          break;
        }
      }
      if (foundInSettings) {
        break;
      }
    }

    if (foundInSettings) {
      if (shouldBeEnabled) {
        device->enable();
      } else {
        device->disable();
      }
    } else {
      device->disable();
    }
  }
#endif
  return E_OK;
}
#endif

#ifdef ENABLE_PID
short PHApp::getPid2Register(short offset, short unused) {
  if (!pidController_0) {
    L_ERROR("Serial Command Error: PID Controller 2 not initialized.");
    return E_INVALID_PARAMETER; // Use defined error code
  }
  if (offset < 0 || offset >= PID_2_REGISTER_COUNT) {
    L_ERROR("Serial Command Error: Invalid PID2 offset %d.", offset);
    return E_INVALID_PARAMETER;
  }

  short address = MB_HREG_PID_2_BASE_ADDRESS + offset;
  short value = pidController_0->mb_tcp_read(address);
  Log.noticeln("PID2 Register Offset %d (Addr %d) Value: %d", offset, address,
               value);
  // Optionally send value back over serial if needed by the protocol
  return E_OK;
}

short PHApp::setPid2Register(short offset, short value) {
  if (!pidController_0) {
    L_ERROR("Serial Command Error: PID Controller 2 not initialized.");
    return E_INVALID_PARAMETER; // Use defined error code
  }
  if (offset < 0 || offset >= PID_2_REGISTER_COUNT) {
    L_ERROR("Serial Command Error: Invalid PID2 offset %d.", offset);
    return E_INVALID_PARAMETER;
  }

  short address = MB_HREG_PID_2_BASE_ADDRESS + offset;
  short result = pidController_0->mb_tcp_write(address, value);

  if (result == E_OK) {
    Log.noticeln("PID2 Register Offset %d (Addr %d) set to: %d", offset,
                 address, value);
  } else {
    L_ERROR("PID2 Register Offset %d (Addr %d) failed to set to %d. Error: %d",
            offset, address, value, result);
  }
  return result;
}

#endif

short PHApp::onMessage(int id, E_CALLS verb, E_MessageFlags flags, void *user,
                       Component *src) {
#if defined(ENABLE_INFLUXDB) && defined(ENABLE_OMRON_E5)
  // Intercept messages from Omron PID controllers to log their data.
  if (verb == E_CALLS::EC_USER && src &&
      src->type == COMPONENT_TYPE::COMPONENT_TYPE_PID) {
    // Log.infoln("PHApp::onMessage - OmronE5");
    //  We know it's an OmronE5, so we can safely cast it.
    OmronE5 *omron = static_cast<OmronE5 *>(src);
    uint16_t sp, pv;

    // Ensure both SP and PV are valid before logging
    if (omron->getSP(sp) && omron->getPV(pv)) {
      Point p("pid_controller"); // Create a local Point
      p.addTag("slaveId", String(omron->slaveId));
      p.addField("sp", sp);
      p.addField("pv", pv);
      p.addField("isHeating", omron->isHeating());
      influxDb_0->write(p);
    } else {
      Log.errorln("PHApp::onMessage - OmronE5 - SP or PV not valid");
    }
  }
#endif

#if ENABLED(ENABLE_RS485, ENABLE_WEBSERVER, ENABLE_WEBSOCKET)
  if (verb == E_CALLS::EC_USER && user != nullptr && webServer != nullptr) {
    MB_UpdateData *update = static_cast<MB_UpdateData *>(user);
    return webServer->onMessage(id, E_CALLS::EC_USER, E_MessageFlags::E_MF_NONE,
                                user, src);
  }
  if (verb == E_CALLS::EC_PROTOBUF_UPDATE && user != nullptr) {
    PB_UpdateData *update = static_cast<PB_UpdateData *>(user);
    return webServer->onMessage(id, E_CALLS::EC_PROTOBUF_UPDATE,
                                E_MessageFlags::E_MF_NONE, user, src);
  }
#endif
  if (verb == E_CALLS::EC_DISPATCH && user != nullptr) {
    JsonDocument *doc = static_cast<JsonDocument *>(user);
    this->broadcast(BROADCAST_ERROR_MESSAGE, *doc);
    return E_OK;
  }
  return App::onMessage(id, verb, flags, user, src);
}

/**
 * @brief Retrieves a component by its ID.
 *
 * @param id The ID of the component to retrieve.
 * @return A pointer to the component with the specified ID, or nullptr if not
 * found.
 * @note Top-Level PHApp cant be part of components vector, so we need to handle
 * it separately.
 */
Component *PHApp::byId(ushort id) {
  Component *comp = App::byId(id);
  if (comp) {
    return comp;
  } else if (id == COMPONENT_KEY_APP) {
    return this;
  }
  return nullptr;
}

short PHApp::init(short arg1, short arg2) {

#if ENABLED(ENABLE_AMPERAGE_BUDGET_MANAGER, ENABLE_OMRON_E5)
  RTU_Base *const *devices = rs485->deviceManager.getDevices();
  int numDevicesInManager = rs485->deviceManager.getMaxDevices();
  for (int i = 0; i < numDevicesInManager; ++i) {
    if (devices[i] != nullptr) {
      Component *comp = devices[i];
      if (comp == nullptr || comp->type != COMPONENT_TYPE::COMPONENT_TYPE_PID) {
        continue;
      }

      OmronE5 *omronE5 = static_cast<OmronE5 *>(comp);
      omronE5->stop();
      omronE5->setSP(0);
    }
  }
#endif
  return E_OK;
}

#ifdef ENABLE_PROFILE_SIGNAL_PLOT
void PHApp::startSignalPlot(short slotId) {
  if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT &&
      signalPlots[slotId] != nullptr) {
    L_INFO("PHApp: Starting SignalPlot in slot %d (triggered by "
           "TemperatureProfile).",
           slotId);
    signalPlots[slotId]->start();
  } else {
    Log.warningln("PHApp: Could not start SignalPlot. Invalid slotId %d or "
                  "plot not initialized.",
                  slotId);
  }
}

void PHApp::stopSignalPlot(short slotId) {
  if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT &&
      signalPlots[slotId] != nullptr) {
    L_INFO("PHApp: Stopping SignalPlot in slot %d (triggered by "
           "TemperatureProfile).",
           slotId);
    signalPlots[slotId]->stop();
  } else {
    Log.warningln("PHApp: Could not stop SignalPlot. Invalid slotId %d or plot "
                  "not initialized.",
                  slotId);
  }
}

void PHApp::enableSignalPlot(short slotId, bool enable) {
  if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT &&
      signalPlots[slotId] != nullptr) {
    if (enable) {
      L_INFO("PHApp: Enabling SignalPlot in slot %d (triggered by "
             "TemperatureProfile).",
             slotId);
      signalPlots[slotId]->enable(enable);
    } else {
      L_INFO("PHApp: Disabling SignalPlot in slot %d (triggered by "
             "TemperatureProfile).",
             slotId);
      signalPlots[slotId]->disable();
    }
  } else {
    Log.warningln("PHApp: Could not enable/disable SignalPlot. Invalid slotId "
                  "%d or plot not initialized.",
                  slotId);
  }
}

void PHApp::pauseSignalPlot(short slotId) {
  if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT &&
      signalPlots[slotId] != nullptr) {
    L_INFO("PHApp: Pausing SignalPlot in slot %d (triggered by "
           "TemperatureProfile).",
           slotId);
    signalPlots[slotId]->pause();
  } else {
    Log.warningln("PHApp: Could not pause SignalPlot. Invalid slotId %d or "
                  "plot not initialized.",
                  slotId);
  }
}

void PHApp::resumeSignalPlot(short slotId) {
  if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT &&
      signalPlots[slotId] != nullptr) {
    L_INFO("PHApp: Resuming SignalPlot in slot %d (triggered by "
           "TemperatureProfile).",
           slotId);
    signalPlots[slotId]->resume();
  } else {
    Log.warningln("PHApp: Could not resume SignalPlot. Invalid slotId %d or "
                  "plot not initialized.",
                  slotId);
  }
}

#endif // ENABLE_PROFILE_SIGNAL_PLOT

#ifdef ENABLE_INFLUXDB
void PHApp::testInfluxDB() {
  if (influxDb_0 && (millis() - _last_influx_test_ms >= 5000)) {
    _last_influx_test_ms = millis();

    Point p("test_data"); // Create a local Point
    p.addTag("device", "ESP32-S3");
    p.addTag("source", "1");

    p.addField("random_value", random(100));
    p.addField("random_value2", random(100));
    p.addField("on-off-1", random(2));
    p.addField("on-off-2", random(2));

    if (influxDb_0->write(p) == E_OK) {
      Log.infoln("PHApp: Sent test data to InfluxDB.");
    }
  }
}
#endif

bool PHApp::isSlave() const { return _is_slave; }

short PHApp::setSlave(bool value, bool sync) {
  _is_slave = value;

  bool enabled = !value; // When slave=true, disable components; when
                         // slave=false, enable them

#if defined(ENABLE_RS485) && defined(ENABLE_OMRON_E5)
  if (rs485) {
    RTU_Base *const *devices = rs485->deviceManager.getDevices();
    int numDevices = rs485->deviceManager.getMaxDevices();
    for (int i = 0; i < numDevices; ++i) {
      if (devices[i] != nullptr &&
          devices[i]->type == COMPONENT_TYPE::COMPONENT_TYPE_PID) {
        OmronE5 *omron = static_cast<OmronE5 *>(devices[i]);
        if (omron) {
          omron->enable(enabled);
        }
      }
    }
  }
#endif

#ifdef ENABLE_AMPERAGE_BUDGET_MANAGER
  if (pidManagerAmperage) {
    pidManagerAmperage->enable(enabled);
  }
#endif

#ifdef ENABLE_LOADCELL_0
  if (loadCell_0) {
    loadCell_0->enable(enabled);
  }
#endif

#ifdef ENABLE_LOADCELL_1
  if (loadCell_1) {
    loadCell_1->enable(enabled);
  }
#endif

#ifdef ENABLE_PROFILE_TEMPERATURE
  for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i) {
    if (tempProfiles[i] != nullptr) {
      tempProfiles[i]->enable(enabled);
    }
  }
#endif

  if (sync) {
    updateRuntimeState();
  }
  return E_OK;
}

bool PHApp::getAllOmronStop() const { return _all_omron_stop; }

short PHApp::setAllOmronStop(bool value, bool sync) {
  _all_omron_stop = value;

#if defined(ENABLE_RS485) && defined(ENABLE_OMRON_E5)
  if (!rs485) {
    L_INFO("PHApp: No RS485 found");
    return E_OK;
  }

  RTU_Base *const *devices = rs485->deviceManager.getDevices();
  int numDevices = rs485->deviceManager.getMaxDevices();
  for (int i = 0; i < numDevices; ++i) {
    if (devices[i] != nullptr &&
        devices[i]->type == COMPONENT_TYPE::COMPONENT_TYPE_PID) {
      OmronE5 *omron = static_cast<OmronE5 *>(devices[i]);
      if (omron && omron->enabled()) {
        if (value) {
          omron->stop();
        } else {
          omron->run();
        }
      }
    }
  }

  updateRuntimeState();

#endif

  return E_OK;
}

bool PHApp::getAllOmronComWrite() const { return _all_omron_com_write; }

short PHApp::setAllOmronComWrite(bool value, bool sync) {
  _all_omron_com_write = value;

#if defined(ENABLE_RS485) && defined(ENABLE_OMRON_E5)
  if (!rs485) {
    L_INFO("PHApp: No RS485 found");
    return E_OK;
  }

  RTU_Base *const *devices = rs485->deviceManager.getDevices();
  int numDevices = rs485->deviceManager.getMaxDevices();
  for (int i = 0; i < numDevices; ++i) {
    if (devices[i] != nullptr &&
        devices[i]->type == COMPONENT_TYPE::COMPONENT_TYPE_PID) {
      OmronE5 *omron = static_cast<OmronE5 *>(devices[i]);
      if (omron && omron->enabled()) {
        omron->setCommsWriting(value);
      }
    }
  }
#endif
  if (sync) {
    updateRuntimeState();
  }
  return E_OK;
}

short PHApp::setAppWarmup(bool value, bool sync) {
  _app_warmup = value;
  if (appSettings) {
    appSettings->set("heating", "ALWAYS_WARMUP", value);
    appSettings->save();
  }
  if (sync) {
    updateRuntimeState();
  }
  return E_OK;
}

short PHApp::setAppPidLag(bool value, bool sync) {
  _app_pid_lag = value;
  if (appSettings) {
    appSettings->set("heating", "PID_LAG_COMPENSATION", value);
    appSettings->save();
  }
  if (sync) {
    updateRuntimeState();
  }
  return E_OK;
}

void app_main() {
  // Arduino will still call setup()/loop()
}