#include "fan_characterize.h"

FanCharacterize::FanCharacterize()
    : phase_(Phase::Idle),
      maxCornerC_(FANCHARS_MAX_CORNER_C),
      coolAvgC_(FANCHARS_COOL_AVG_C),
      heaterPct_(FANCHARS_HEATER_PCT),
      profileIndex_(0),
      refineFanPwm_(0),
      refineRun_(false),
      sessionStartMs_(0),
      phaseStartMs_(0),
      lastLogMs_(0),
      spreadSum_(0.0f),
      spreadSamples_(0),
      resultCount_(0),
      winnerFanPwm_(0) {}

bool FanCharacterize::isActive() const {
  return phase_ == Phase::Precool || phase_ == Phase::Heat || phase_ == Phase::Hold ||
         phase_ == Phase::Cooldown;
}

uint32_t FanCharacterize::elapsedMs(uint32_t nowMs) const {
  if (sessionStartMs_ == 0) {
    return 0;
  }
  return nowMs - sessionStartMs_;
}

uint8_t FanCharacterize::currentFanPwm() const {
  if (profileIndex_ >= FANCHARS_COARSE_COUNT) {
    return refineFanPwm_;
  }
  return FANCHARS_COARSE_PWM[profileIndex_];
}

const char *FanCharacterize::phaseName() const {
  if (isRefineRun() && (phase_ == Phase::Heat || phase_ == Phase::Hold)) {
    return "refine";
  }
  switch (phase_) {
  case Phase::Precool:
    return "precool";
  case Phase::Heat:
    return "heat";
  case Phase::Hold:
    return "hold";
  case Phase::Cooldown:
    return "cool";
  default:
    return "";
  }
}

bool FanCharacterize::start(float maxCornerC, float avgTempC) {
  if (maxCornerC < 45.0f || maxCornerC > EMERGENCY_ABSOLUTE_MAX_C - 5.0f) {
    return false;
  }

  maxCornerC_ = maxCornerC;
  heaterPct_ = FANCHARS_HEATER_PCT;
  profileIndex_ = 0;
  refineFanPwm_ = 0;
  refineRun_ = false;
  resultCount_ = 0;
  winnerFanPwm_ = 0;
  sessionStartMs_ = millis();
  phaseStartMs_ = sessionStartMs_;
  lastLogMs_ = 0;
  resetProfileStats();

  phase_ = avgTempC > coolAvgC_ + FANCHARS_PRECOOL_MARGIN_C ? Phase::Precool : Phase::Heat;

  Serial.print(F("fanchars: "));
  Serial.print(FANCHARS_COARSE_COUNT);
  Serial.print(F(" fans + refine h="));
  Serial.println(heaterPct_, 0);
  return true;
}

void FanCharacterize::abort() {
  if (isActive()) {
    Serial.println(F("fanchars: stop"));
  }
  phase_ = Phase::Idle;
  sessionStartMs_ = 0;
}

void FanCharacterize::reset() {
  phase_ = Phase::Idle;
  sessionStartMs_ = 0;
}

bool FanCharacterize::update(float avgTempC, float maxTempC, float spreadC, uint32_t nowMs,
                             float &heaterDutyOut, uint8_t &fanPwmOut) {
  heaterDutyOut = 0.0f;
  fanPwmOut = 0;

  if (phase_ == Phase::Idle || phase_ == Phase::Done || phase_ == Phase::Failed) {
    return false;
  }

  if (maxTempC >= EMERGENCY_ABSOLUTE_MAX_C) {
    fail(F("fanchars: abort limit"));
    return false;
  }

  if (phase_ == Phase::Precool) {
    fanPwmOut = FAN_MAX_PWM;
    if (nowMs - phaseStartMs_ > FANCHARS_COOLDOWN_TIMEOUT_MS) {
      fail(F("fanchars: abort precool"));
      return false;
    }
    if (avgTempC <= coolAvgC_) {
      beginProfileHeat(nowMs);
    }
    return true;
  }

  if (phase_ == Phase::Heat) {
    fanPwmOut = currentFanPwm();
    heaterDutyOut = heaterPct_;
    if (nowMs - phaseStartMs_ > FANCHARS_HEAT_TIMEOUT_MS) {
      skipProfile(nowMs, maxTempC);
      return true;
    }
    if (maxTempC >= maxCornerC_) {
      enterHold(nowMs);
    }
    return true;
  }

  if (phase_ == Phase::Hold) {
    fanPwmOut = currentFanPwm();
    spreadSum_ += spreadC;
    ++spreadSamples_;
    if (nowMs - phaseStartMs_ >= FANCHARS_HOLD_MS) {
      finishProfile(nowMs);
    }
    return true;
  }

  if (phase_ == Phase::Cooldown) {
    fanPwmOut = FAN_MAX_PWM;
    if (nowMs - phaseStartMs_ > FANCHARS_COOLDOWN_TIMEOUT_MS) {
      fail(F("fanchars: abort cool"));
      return false;
    }
    if (avgTempC <= coolAvgC_) {
      beginProfileHeat(nowMs);
    }
    return true;
  }

  return false;
}

void FanCharacterize::logIfDue(const float *sensorTemps, const bool *sensorValid, uint8_t sensorCount,
                               float avgTempC, float minTempC, float maxTempC, float spreadC,
                               uint32_t nowMs) {
  if (!isActive()) {
    return;
  }
  if (lastLogMs_ != 0 && nowMs - lastLogMs_ < FANCHARS_LOG_INTERVAL_MS) {
    return;
  }
  lastLogMs_ = nowMs;

  const uint8_t runNum =
      profileIndex_ >= FANCHARS_COARSE_COUNT ? FANCHARS_COARSE_COUNT + 1 : profileIndex_ + 1;

  Serial.print(F("fc,"));
  Serial.print(nowMs);
  Serial.print(',');
  Serial.print(phaseName());
  Serial.print(',');
  Serial.print(runNum);
  Serial.print('/');
  Serial.print(FANCHARS_COARSE_COUNT + 1);
  Serial.print(',');
  Serial.print(currentFanPwm());
  Serial.print(',');
  Serial.print(heaterPct_, 0);
  Serial.print(',');
  Serial.print(avgTempC, 1);
  Serial.print(',');
  Serial.print(minTempC, 1);
  Serial.print(',');
  Serial.print(maxTempC, 1);
  Serial.print(',');
  Serial.println(spreadC, 1);
  (void)sensorTemps;
  (void)sensorValid;
  (void)sensorCount;
}

void FanCharacterize::resetProfileStats() {
  spreadSum_ = 0.0f;
  spreadSamples_ = 0;
}

void FanCharacterize::beginProfileHeat(uint32_t nowMs) {
  phase_ = Phase::Heat;
  phaseStartMs_ = nowMs;
  resetProfileStats();
  Serial.print(F("fanchars: f="));
  Serial.println(currentFanPwm());
}

void FanCharacterize::enterHold(uint32_t nowMs) {
  phase_ = Phase::Hold;
  phaseStartMs_ = nowMs;
  resetProfileStats();
}

void FanCharacterize::finishProfile(uint32_t nowMs) {
  if (spreadSamples_ == 0) {
    fail(F("fanchars: abort hold"));
    return;
  }

  const float meanSpread = spreadSum_ / static_cast<float>(spreadSamples_);
  results_[resultCount_].fanPwm = currentFanPwm();
  results_[resultCount_].meanSpreadC = meanSpread;
  ++resultCount_;

  Serial.print(F("fanchars: f="));
  Serial.print(currentFanPwm());
  Serial.print(F(" spr="));
  Serial.println(meanSpread, 2);

  if (isRefineRun()) {
    finishAll(nowMs);
    return;
  }

  if (profileIndex_ + 1 >= FANCHARS_COARSE_COUNT) {
    planRefine(nowMs);
    return;
  }

  ++profileIndex_;
  phase_ = Phase::Cooldown;
  phaseStartMs_ = nowMs;
}

void FanCharacterize::skipProfile(uint32_t nowMs, float maxTempC) {
  Serial.print(F("fanchars: skip f="));
  Serial.print(currentFanPwm());
  Serial.print(F(" max="));
  Serial.print(maxTempC, 1);
  Serial.println(F("C"));

  if (isRefineRun()) {
    finishAll(nowMs);
    return;
  }

  if (profileIndex_ + 1 >= FANCHARS_COARSE_COUNT) {
    planRefine(nowMs);
    return;
  }

  ++profileIndex_;
  phase_ = Phase::Cooldown;
  phaseStartMs_ = nowMs;
}

void FanCharacterize::planRefine(uint32_t nowMs) {
  if (resultCount_ < 2) {
    Serial.println(F("fanchars: refine skip"));
    finishAll(nowMs);
    return;
  }

  uint8_t bestI = 0;
  uint8_t secondI = 1;
  if (results_[secondI].meanSpreadC < results_[bestI].meanSpreadC) {
    bestI = 1;
    secondI = 0;
  }
  for (uint8_t i = 2; i < resultCount_; ++i) {
    if (results_[i].meanSpreadC < results_[bestI].meanSpreadC) {
      secondI = bestI;
      bestI = i;
    } else if (results_[i].meanSpreadC < results_[secondI].meanSpreadC) {
      secondI = i;
    }
  }

  const uint8_t bestFan = results_[bestI].fanPwm;
  if (bestFan <= FANCHARS_LIMIT_LOW_PWM || bestFan >= FANCHARS_LIMIT_HIGH_PWM) {
    Serial.println(F("fanchars: limit"));
    finishAll(nowMs);
    return;
  }

  const uint8_t secondFan = results_[secondI].fanPwm;
  refineFanPwm_ =
      static_cast<uint8_t>((static_cast<uint16_t>(bestFan) + secondFan) / 2);
  if (refineFanPwm_ == bestFan || refineFanPwm_ == secondFan) {
    finishAll(nowMs);
    return;
  }

  refineRun_ = true;
  profileIndex_ = FANCHARS_COARSE_COUNT;
  phase_ = Phase::Cooldown;
  phaseStartMs_ = nowMs;
  Serial.print(F("fanchars: mid f="));
  Serial.println(refineFanPwm_);
}

void FanCharacterize::finishAll(uint32_t nowMs) {
  phase_ = Phase::Done;

  if (resultCount_ == 0) {
    winnerFanPwm_ = 0;
    Serial.println(F("fanchars: done — no valid runs"));
    return;
  }

  uint8_t bestIndex = 0;
  float bestSpread = results_[0].meanSpreadC;
  for (uint8_t i = 1; i < resultCount_; ++i) {
    if (results_[i].meanSpreadC < bestSpread) {
      bestSpread = results_[i].meanSpreadC;
      bestIndex = i;
    }
  }
  winnerFanPwm_ = results_[bestIndex].fanPwm;

  Serial.print(F("fanchars: done "));
  Serial.print((nowMs - sessionStartMs_) / 60000UL);
  Serial.println(F("min"));
  for (uint8_t i = 0; i < resultCount_; ++i) {
    Serial.print(F("  "));
    Serial.print(results_[i].fanPwm);
    Serial.print(F("="));
    Serial.println(results_[i].meanSpreadC, 2);
  }
  Serial.print(F("  best "));
  Serial.print(winnerFanPwm_);
  Serial.print(F(" spr="));
  Serial.println(bestSpread, 2);
  Serial.println(F("  fanchars save"));
}

void FanCharacterize::fail(const __FlashStringHelper *reason) {
  Serial.println(reason);
  phase_ = Phase::Failed;
  sessionStartMs_ = 0;
}
