#pragma once

#include <Arduino.h>

#include "config.h"
#include "fan_characterize.h"
#include "pid_autotuner.h"
#include "pid_controller.h"
#include "settings_store.h"
#include "tuning_store.h"

class ThermalController {
public:
  enum class HeaterBlock : uint8_t { None, Cutoff, Corner, Autotune, FanChars };

  ThermalController()
      : heatPi_(HEAT_PI_KP, HEAT_PI_KI, 0.0f, 0.0f, 100.0f),
        autotuner_(),
        fanchars_(),
        targetTempC_(TARGET_TEMP_C),
        heaterDutyPercent_(0.0f),
        heaterAllowancePercent_(100.0f),
        cornerSpreadC_(0.0f),
        lastMaxTempC_(0.0f),
        regulatingFanPwm_(0),
        stirFanPwm_(FAN_STIR_PWM),
        fanManualPwm_(0),
        fanPwm_(0),
        tuningLoaded_(false),
        fanIdleOverride_(false),
        fanManualActive_(false),
        sensorWarmValid_(false),
        cutoffActive_(false),
        failSafeActive_(true),
        heaterCycleStartMs_(0),
        lastHeaterUpdateMs_(0),
        heaterOn_(false),
        lastAvgTempC_(0.0f),
        heaterBlock_(HeaterBlock::None),
        fanTestActive_(false),
        fanTestPwm_(0),
        fanTestEndMs_(0) {}

  void begin() {
    pinMode(FAN_PIN, OUTPUT);
    pinMode(HEATER_PIN, OUTPUT);
    digitalWrite(HEATER_PIN, LOW);
    writeFan(0);

    heatPi_.setSetpoint(targetTempC_);
    heatPi_.reset();
    heaterCycleStartMs_ = millis();
    lastHeaterUpdateMs_ = 0;
    failSafeActive_ = true;
    cornerSpreadC_ = 0.0f;
    lastMaxTempC_ = 0.0f;
    sensorWarmValid_ = false;
    heaterAllowancePercent_ = 100.0f;
    if (isIdle()) {
      forceHeaterOff();
    }
    applyFan(millis());

    TuningData stored;
    if (tuningLoad(stored)) {
      applyTuning(stored);
      Serial.println(F("Loaded learned PI from EEPROM"));
      printTuning();
    }

    SettingsData settings;
    if (settingsLoad(settings)) {
      if (settings.stirFanPwm > 0) {
        stirFanPwm_ = settings.stirFanPwm;
      }
      if (settings.targetC >= TARGET_MIN_C && settings.targetC <= TARGET_MAX_C) {
        setTarget(settings.targetC, false);
      }
    }
  }

  void applyTuning(const TuningData &data) {
    heatPi_.setTunings(data.heatKp, data.heatKi, 0.0f);
    tuningLoaded_ = true;
  }

  void clearTuning() {
    tuningLoaded_ = false;
    heatPi_.setTunings(HEAT_PI_KP, HEAT_PI_KI, 0.0f);
    tuningClear();
    heatPi_.reset();
    Serial.println(F("PI reset to defaults"));
  }

  void printTuning() const {
    Serial.print(F("Heat PI Kp="));
    Serial.print(heatPi_.kp(), 3);
    Serial.print(F(" Ki="));
    Serial.print(heatPi_.ki(), 4);
    Serial.print(F("  tuned="));
    Serial.println(tuningLoaded_ ? F("yes") : F("no"));
  }

  void saveTuningToEeprom() {
    TuningData data;
    data.magic = TUNING_MAGIC;
    data.heatKp = heatPi_.kp();
    data.heatKi = heatPi_.ki();
    tuningSave(data);
    tuningLoaded_ = true;
    Serial.println(F("Saved PI to EEPROM"));
  }

  float heatKp() const { return heatPi_.kp(); }
  float heatKi() const { return heatPi_.ki(); }

  bool isTuningLoaded() const { return tuningLoaded_; }

  bool startAutotune(float setpointC) {
    if (autotuner_.isActive() || fanchars_.isActive()) {
      return false;
    }
    cutoffActive_ = false;
    heatPi_.reset();
    return autotuner_.start(setpointC);
  }

  void stopAutotune() { autotuner_.abort(); }

  bool isAutotuning() const { return autotuner_.isActive(); }

  uint32_t autotuneElapsedMs(uint32_t nowMs) const { return autotuner_.elapsedMs(nowMs); }

  const char *autotunePhaseName() const { return autotuner_.phaseName(); }

  uint8_t autotuneCycleCount() const { return autotuner_.cycleCount(); }

  uint8_t autotunePeriodCount() const { return autotuner_.periodCount(); }

  float autotunePreheatTargetC() const { return autotuner_.preheatTargetC(); }

  bool startFanCharacterize(float maxCornerC, float avgTempC) {
    if (autotuner_.isActive() || fanchars_.isActive()) {
      return false;
    }
    stopFanTest();
    cutoffActive_ = false;
    heatPi_.reset();
    setTarget(0.0f, false);
    return fanchars_.start(maxCornerC, avgTempC);
  }

  void stopFanCharacterize() {
    fanchars_.abort();
    forceHeaterOff();
    writeFan(0);
  }

  bool isFanCharacterizeActive() const { return fanchars_.isActive(); }

  uint32_t fanCharacterizeElapsedMs(uint32_t nowMs) const { return fanchars_.elapsedMs(nowMs); }

  const char *fanCharacterizePhaseName() const { return fanchars_.phaseName(); }

  uint8_t fanCharacterizeProfileIndex() const { return fanchars_.profileIndex(); }

  uint8_t fanCharacterizeProfileCount() const { return fanchars_.profileCount(); }

  uint8_t fanCharacterizeFanPwm() const { return fanchars_.currentFanPwm(); }

  bool isFanCharacterizeRefineRun() const { return fanchars_.isRefineRun(); }

  float fanCharacterizeHeaterPct() const { return fanchars_.heaterPct(); }

  uint8_t stirFanPwm() const { return stirFanPwm_; }

  bool isFanManualOverride() const { return fanManualActive_ && !isIdle(); }

  bool setRegulatingFanManual(uint8_t pwm) {
    if (isIdle()) {
      return false;
    }
    fanManualPwm_ = pwm;
    fanManualActive_ = true;
    return true;
  }

  void clearRegulatingFanManual() {
    fanManualActive_ = false;
  }

  bool setStirFanPwm(uint8_t pwm, bool persist = true) {
    if (pwm == 0) {
      return false;
    }
    stirFanPwm_ = pwm;
    if (persist) {
      settingsSaveStirFan(pwm);
    }
    return true;
  }

  void logFanCharacterizeIfDue(const float *sensorTemps, const bool *sensorValid, uint8_t sensorCount,
                               float avgTempC, float minTempC, float maxTempC, float spreadC,
                               uint32_t nowMs) {
    fanchars_.logIfDue(sensorTemps, sensorValid, sensorCount, avgTempC, minTempC, maxTempC,
                       spreadC, nowMs);
  }

  bool saveStirFanFromCharacterize() {
    if (fanchars_.phase() != FanCharacterize::Phase::Done) {
      return false;
    }
    const uint8_t winner = fanchars_.winnerFanPwm();
    if (winner == 0) {
      return false;
    }
    stirFanPwm_ = winner;
    settingsSaveStirFan(winner);
    Serial.print(F("stir fan "));
    Serial.println(winner);
    fanchars_.reset();
    return true;
  }

  bool commitAutotuneIfDone() {
    if (autotuner_.phase() != PidAutotuner::Phase::Done) {
      return false;
    }

    heatPi_.setTunings(autotuner_.resultKp(), autotuner_.resultKi(), 0.0f);
    heatPi_.reset();

    TuningData data;
    data.magic = TUNING_MAGIC;
    data.heatKp = autotuner_.resultKp();
    data.heatKi = autotuner_.resultKi();
    tuningSave(data);
    tuningLoaded_ = true;
    autotuner_.reset();
    Serial.println(F("Saved heat PI to EEPROM"));
    return true;
  }

  void setTarget(float targetC, bool persist = true) {
    targetTempC_ = targetC;
    heatPi_.setSetpoint(targetC);
    heatPi_.reset();
    fanManualActive_ = false;
    cutoffActive_ = false;
    if (targetC > 0.0f) {
      fanIdleOverride_ = false;
    } else {
      forceHeaterOff();
      fanIdleOverride_ = false;
    }
    applyFan(millis());
    if (persist && targetC >= TARGET_MIN_C && targetC <= TARGET_MAX_C) {
      settingsSaveTarget(targetC);
    }
  }

  void noteSensorMax(float maxTempC) {
    lastMaxTempC_ = maxTempC;
    sensorWarmValid_ = true;
  }

  bool setFanOff() {
    if (!isIdle()) {
      return false;
    }
    fanIdleOverride_ = false;
    applyFan(millis());
    return true;
  }

  void setFanIdle() {
    if (!isIdle()) {
      return;
    }
    if (!sensorWarmValid_ || lastMaxTempC_ >= IDLE_AUTO_FAN_OFF_TEMP_C) {
      return;
    }
    fanIdleOverride_ = true;
    applyFan(millis());
  }

  bool isFanOff() const {
    return isIdle() && sensorWarmValid_ && lastMaxTempC_ < IDLE_AUTO_FAN_OFF_TEMP_C &&
           !fanIdleOverride_;
  }

  bool isIdleCooling() const {
    return isIdle() &&
           (!sensorWarmValid_ || lastMaxTempC_ >= IDLE_AUTO_FAN_OFF_TEMP_C);
  }

  float target() const { return targetTempC_; }

  bool isIdle() const { return targetTempC_ <= 0.0f; }

  float emergencyCutoffC() const { return emergencyCutoffForTarget(targetTempC_); }

  float cutoffThreshold() const {
    if (isIdle()) {
      return INFINITY;
    }
    return emergencyCutoffC();
  }

  bool isCutoffActive() const { return cutoffActive_; }

  bool isFailSafeActive() const { return failSafeActive_; }

  float heaterDutyPercent() const { return heaterDutyPercent_; }

  float heaterAllowance() const { return heaterAllowancePercent_; }

  float cornerSpread() const { return cornerSpreadC_; }

  uint8_t fanPwm() const { return fanPwm_; }

  bool isHeaterSsrOn() const { return heaterOn_; }

  bool isFanTestActive(uint32_t nowMs) const {
    return fanTestActive_ && nowMs < fanTestEndMs_;
  }

  bool startFanTest(uint8_t pwm, uint32_t nowMs, uint32_t durationMs = 15000) {
    fanTestPwm_ = pwm;
    fanTestEndMs_ = nowMs + durationMs;
    fanTestActive_ = true;
    writeFan(pwm);
    return true;
  }

  void stopFanTest() { fanTestActive_ = false; }

  float maxHeatStopAt(float avgTempC) const {
    if (isIdle()) {
      return INFINITY;
    }
    return maxHeatStopTemp(avgTempC);
  }

  const char *heaterBlockReason() const {
    switch (heaterBlock_) {
    case HeaterBlock::Cutoff:
      return "cutoff";
    case HeaterBlock::Corner:
      return "corner";
    case HeaterBlock::Autotune:
      return "autotune";
    case HeaterBlock::FanChars:
      return "fanchars";
    default:
      return "none";
    }
  }

  const char *regulatingModeName() const {
    return tuningLoaded_ ? "regulating" : "manual";
  }

  void update(float avgTempC, float maxTempC, float cornerSpreadC, uint32_t nowMs) {
    failSafeActive_ = false;
    noteSensorMax(maxTempC);
    lastAvgTempC_ = avgTempC;
    heaterBlock_ = HeaterBlock::None;

    cornerSpreadC_ =
        SPREAD_EMA_ALPHA * cornerSpreadC_ +
        (1.0f - SPREAD_EMA_ALPHA) * cornerSpreadC;

    if (fanchars_.isActive()) {
      updateFanCharacterize(avgTempC, maxTempC, nowMs);
      return;
    }

    if (autotuner_.isActive()) {
      updateAutotune(avgTempC, maxTempC, nowMs);
      return;
    }

    if (isIdle()) {
      forceHeaterOff();
      cutoffActive_ = false;
      heatPi_.reset();
      lastHeaterUpdateMs_ = nowMs;
      applyFan(nowMs);
      return;
    }

    updateRegulating(avgTempC, maxTempC, nowMs);
    lastHeaterUpdateMs_ = nowMs;
    applyHeaterBurst(nowMs);
    writeFan(regulatingFanPwm_);
  }

  void enterFailSafe() {
    failSafeActive_ = true;
    cutoffActive_ = false;
    forceHeaterOff();
    applyFan(millis());
    heatPi_.reset();
    autotuner_.abort();
    fanchars_.abort();
  }

  void forceHeaterOff() {
    heaterDutyPercent_ = 0.0f;
    heaterAllowancePercent_ = 0.0f;
    heaterOn_ = false;
    digitalWrite(HEATER_PIN, LOW);
  }

  void writeFan(uint8_t pwm) {
    fanPwm_ = pwm;
    pinMode(FAN_PIN, OUTPUT);

    if (FAN_PWM_INVERT) {
      if (pwm == 0) {
        digitalWrite(FAN_PIN, HIGH);
        return;
      }
      if (pwm >= 254) {
        digitalWrite(FAN_PIN, LOW);
        return;
      }
      analogWrite(FAN_PIN, static_cast<uint8_t>(255 - pwm));
      return;
    }

    if (pwm == 0) {
      digitalWrite(FAN_PIN, LOW);
      return;
    }
    if (pwm >= 254) {
      digitalWrite(FAN_PIN, HIGH);
      return;
    }
    analogWrite(FAN_PIN, pwm);
  }

private:
  void updateFanCharacterize(float avgTempC, float maxTempC, uint32_t nowMs) {
    float duty = 0.0f;
    uint8_t fan = 0;
    fanchars_.update(avgTempC, maxTempC, cornerSpreadC_, nowMs, duty, fan);

    heaterDutyPercent_ = duty;
    heaterAllowancePercent_ = duty;
    heaterBlock_ = duty > 0.0f ? HeaterBlock::FanChars : HeaterBlock::None;
    applyHeaterBurst(nowMs);
    writeFan(fan);
    lastHeaterUpdateMs_ = nowMs;
  }

  void updateAutotune(float avgTempC, float maxTempC, uint32_t nowMs) {
    float duty = 0.0f;
    uint8_t fan = stirFanPwm_;
    autotuner_.update(avgTempC, maxTempC, cornerSpreadC_, nowMs, duty, fan);

    heaterDutyPercent_ = duty;
    heaterAllowancePercent_ = duty;
    heaterOn_ = duty >= 50.0f;
    digitalWrite(HEATER_PIN, heaterOn_ ? HIGH : LOW);
    heaterBlock_ = duty > 0.0f ? HeaterBlock::None : HeaterBlock::Autotune;
    writeFan(fanManualActive_ ? fanManualPwm_ : stirFanPwm_);
    lastHeaterUpdateMs_ = nowMs;
    commitAutotuneIfDone();
  }

  void updateRegulating(float avgTempC, float maxTempC, uint32_t nowMs) {
    const float cutoffC = emergencyCutoffC();
    if (maxTempC >= cutoffC) {
      cutoffActive_ = true;
      heaterDutyPercent_ = 0.0f;
      heaterAllowancePercent_ = 0.0f;
      regulatingFanPwm_ = FAN_MAX_PWM;
      heaterBlock_ = HeaterBlock::Cutoff;
      heatPi_.reset();
      return;
    }

    if (cutoffActive_ && maxTempC < cutoffC - CUTOFF_RECOVERY_BAND_C) {
      cutoffActive_ = false;
      heatPi_.reset();
    }

    if (cutoffActive_) {
      heaterBlock_ = HeaterBlock::Cutoff;
      regulatingFanPwm_ = FAN_MAX_PWM;
      return;
    }

    heaterAllowancePercent_ = allowanceFromMaxCorner(maxTempC, avgTempC);

    float duty = heatPi_.compute(avgTempC, nowMs);
    duty = clampPercent(duty);
    if (duty > heaterAllowancePercent_) {
      duty = heaterAllowancePercent_;
      if (heaterAllowancePercent_ < 100.0f) {
        heaterBlock_ = HeaterBlock::Corner;
      }
    }
    heaterDutyPercent_ = duty;

    regulatingFanPwm_ = fanManualActive_ ? fanManualPwm_ : stirFanPwm_;
  }

  static float clampPercent(float value) {
    if (value < 0.0f) {
      return 0.0f;
    }
    if (value > 100.0f) {
      return 100.0f;
    }
    return value;
  }

  bool shouldLimitMaxCorner(float avgTempC) const {
    return avgTempC >= targetTempC_ - CORNER_LIMIT_BAND_C;
  }

  float maxHeatStopTemp(float avgTempC) const {
    if (!shouldLimitMaxCorner(avgTempC)) {
      return emergencyCutoffC();
    }
    return emergencyCutoffC() - CORNER_STOP_MARGIN_C;
  }

  float allowanceFromMaxCorner(float maxTempC, float avgTempC) const {
    const float cutoffC = emergencyCutoffC();
    if (!shouldLimitMaxCorner(avgTempC)) {
      // Heat-up: only taper when a hot corner nears the dynamic cutoff
      if (maxTempC >= cutoffC - 3.0f) {
        const float headroom = cutoffC - maxTempC;
        return clampPercent((headroom / 3.0f) * 100.0f);
      }
      return 100.0f;
    }

    const float stopAt = maxHeatStopTemp(avgTempC);
    if (maxTempC >= stopAt) {
      return 0.0f;
    }

    const float headroom = stopAt - maxTempC;
    if (headroom >= MAX_TEMP_HEADROOM_C) {
      return 100.0f;
    }

    return clampPercent((headroom / MAX_TEMP_HEADROOM_C) * 100.0f);
  }

  void applyHeaterBurst(uint32_t nowMs) {
    if (heaterDutyPercent_ <= 0.0f) {
      forceHeaterOff();
      return;
    }

    if (nowMs - heaterCycleStartMs_ >= HEATER_CYCLE_MS) {
      heaterCycleStartMs_ = nowMs;
    }

    const float onFraction = heaterDutyPercent_ / 100.0f;
    const uint32_t onTimeMs = static_cast<uint32_t>(HEATER_CYCLE_MS * onFraction);
    const bool shouldHeat = (nowMs - heaterCycleStartMs_) < onTimeMs;

    if (shouldHeat != heaterOn_) {
      heaterOn_ = shouldHeat;
      digitalWrite(HEATER_PIN, heaterOn_ ? HIGH : LOW);
    }
  }

  void applyFan(uint32_t nowMs) {
    if (fanTestActive_) {
      if (nowMs < fanTestEndMs_) {
        writeFan(fanTestPwm_);
        return;
      }
      fanTestActive_ = false;
    }

    if (isIdle()) {
      if (sensorWarmValid_ && lastMaxTempC_ >= IDLE_AUTO_FAN_OFF_TEMP_C) {
        writeFan(FAN_MAX_PWM);
      } else if (fanIdleOverride_) {
        writeFan(FAN_IDLE_PWM);
      } else {
        writeFan(0);
      }
      return;
    }

    if (failSafeActive_ || cutoffActive_) {
      writeFan(FAN_MAX_PWM);
      return;
    }
  }

  PidController heatPi_;
  PidAutotuner autotuner_;
  FanCharacterize fanchars_;
  float targetTempC_;
  float heaterDutyPercent_;
  float heaterAllowancePercent_;
  float cornerSpreadC_;
  float lastMaxTempC_;
  uint8_t regulatingFanPwm_;
  uint8_t stirFanPwm_;
  uint8_t fanManualPwm_;
  uint8_t fanPwm_;
  bool tuningLoaded_;
  bool fanIdleOverride_;
  bool fanManualActive_;
  bool sensorWarmValid_;
  bool cutoffActive_;
  bool failSafeActive_;
  uint32_t heaterCycleStartMs_;
  uint32_t lastHeaterUpdateMs_;
  bool heaterOn_;
  float lastAvgTempC_;
  HeaterBlock heaterBlock_;
  bool fanTestActive_;
  uint8_t fanTestPwm_;
  uint32_t fanTestEndMs_;
};
