#include "TBeamStorage.h"

#include <stdarg.h>
#include "driver/gpio.h"

namespace tbeam {

TBeamStorage::TBeamStorage(Print& diagnostic) : diagnostic_(diagnostic) {}

bool TBeamStorage::begin(const StorageConfig& config, SdEventCallback callback) {
  config_ = config;
  callback_ = callback;
  clearError();

  forceSpiDeselected();
  dumpSdPins("early");

  if (!initPmuForSdPower()) {
    setStateAbsent();
    return false;
  }

  cycleSdRail(config_.recoveryRailOffMs, config_.recoveryRailOnSettleMs);
  delay(config_.startupWarmupMs);

  bool mounted = false;
  for (uint8_t i = 0; i < 3; ++i) {
    if (mountPreferred(false)) {
      mounted = true;
      break;
    }
    delay(200);
  }

  if (!mounted) {
    logf("SD: preferred mount failed, trying full scan");
    cycleSdRail(400, 1200);
    delay(config_.startupWarmupMs);
    mounted = mountCardFullScan();
  }

  if (mounted) {
    setStateMounted();
  } else {
    setError("SD mount failed");
    setStateAbsent();
  }
  return mounted;
}

void TBeamStorage::update() {
  const uint32_t now = millis();
  const uint32_t pollInterval =
      (state_ == SdState::MOUNTED) ? config_.pollIntervalMountedMs : config_.pollIntervalAbsentMs;

  if ((uint32_t)(now - lastPollMs_) < pollInterval) {
    return;
  }
  lastPollMs_ = now;

  if (state_ == SdState::MOUNTED) {
    if (verifyMountedCard()) {
      presentVotes_ = 0;
      absentVotes_ = 0;
      return;
    }

    if (mountPreferred(false) && verifyMountedCard()) {
      presentVotes_ = 0;
      absentVotes_ = 0;
      return;
    }

    ++absentVotes_;
    presentVotes_ = 0;
    if (absentVotes_ >= config_.votesToAbsent) {
      closeLog();
      setError("SD removed or unreadable");
      setStateAbsent();
      absentVotes_ = 0;
    }
    return;
  }

  bool mounted = mountPreferred(false);
  if (!mounted && (uint32_t)(now - lastFullScanMs_) >= config_.fullScanIntervalMs) {
    lastFullScanMs_ = now;
    if (config_.recoveryRailCycleOnFullScan) {
      cycleSdRail(config_.recoveryRailOffMs, config_.recoveryRailOnSettleMs);
      delay(150);
    }
    mounted = mountCardFullScan();
  }

  if (mounted) {
    ++presentVotes_;
    absentVotes_ = 0;
    if (presentVotes_ >= config_.votesToPresent) {
      clearError();
      setStateMounted();
      presentVotes_ = 0;
    }
  } else {
    ++absentVotes_;
    presentVotes_ = 0;
    if (absentVotes_ >= config_.votesToAbsent) {
      setStateAbsent();
      absentVotes_ = 0;
    }
  }
}

bool TBeamStorage::consumeMountedEvent() {
  const bool out = mountedEventPending_;
  mountedEventPending_ = false;
  return out;
}

bool TBeamStorage::consumeRemovedEvent() {
  const bool out = removedEventPending_;
  removedEventPending_ = false;
  return out;
}

bool TBeamStorage::forceRemount() {
  closeLog();
  presentVotes_ = 0;
  absentVotes_ = 0;
  lastPollMs_ = 0;
  lastFullScanMs_ = millis();

  cycleSdRail(config_.recoveryRailOffMs, config_.recoveryRailOnSettleMs);
  delay(config_.startupWarmupMs);

  if (mountCardFullScan()) {
    clearError();
    setStateMounted();
    return true;
  }

  setError("manual SD remount failed");
  setStateAbsent();
  return false;
}

bool TBeamStorage::ensureDirRecursive(const char* path) {
  if (!ready() || !path || path[0] == '\0') {
    setError("ensureDirRecursive invalid state");
    return false;
  }

  char normalized[128];
  if (!normalizePath(path, normalized, sizeof(normalized))) {
    setError("directory path too long");
    return false;
  }

  char partial[128] = "/";
  const char* p = normalized + 1;
  while (*p) {
    const char* slash = strchr(p, '/');
    const size_t len = slash ? (size_t)(slash - normalized) : strlen(normalized);
    if (len >= sizeof(partial)) {
      setError("directory path too long");
      return false;
    }
    memcpy(partial, normalized, len);
    partial[len] = '\0';
    if (!SD.exists(partial) && !SD.mkdir(partial)) {
      setError("SD.mkdir failed");
      return false;
    }
    if (!slash) {
      break;
    }
    p = slash + 1;
  }
  clearError();
  return true;
}

bool TBeamStorage::makeUniqueLogPath(const char* prefix, const char* extension, char* out, size_t outSize) {
  if (!out || outSize == 0) {
    return false;
  }
  out[0] = '\0';
  if (!ready() && !forceRemount()) {
    return false;
  }
  if (!ensureDirRecursive(config_.logDir)) {
    return false;
  }

  const char* safePrefix = (prefix && prefix[0]) ? prefix : "log";
  const char* safeExt = (extension && extension[0]) ? extension : ".log";
  char base[80];
  snprintf(base, sizeof(base), "%s_%lu", safePrefix, (unsigned long)(millis() / 1000UL));

  for (uint16_t suffix = 0; suffix < 1000; ++suffix) {
    const int n = suffix == 0
                      ? snprintf(out, outSize, "%s/%s%s", config_.logDir, base, safeExt)
                      : snprintf(out, outSize, "%s/%s_%03u%s", config_.logDir, base, (unsigned)suffix, safeExt);
    if (n < 0 || (size_t)n >= outSize) {
      setError("log path buffer too small");
      out[0] = '\0';
      return false;
    }
    if (!SD.exists(out)) {
      clearError();
      return true;
    }
  }

  setError("could not allocate unique log path");
  out[0] = '\0';
  return false;
}

bool TBeamStorage::openLog(const char* path) {
  closeLog();
  if (!ready() && !forceRemount()) {
    return false;
  }
  if (!path || path[0] == '\0') {
    setError("openLog missing path");
    return false;
  }

  char normalized[sizeof(currentLogPath_)];
  if (!normalizePath(path, normalized, sizeof(normalized))) {
    setError("log path too long");
    return false;
  }

  const char* slash = strrchr(normalized, '/');
  if (slash && slash != normalized) {
    char dir[sizeof(currentLogPath_)];
    const size_t len = (size_t)(slash - normalized);
    memcpy(dir, normalized, len);
    dir[len] = '\0';
    if (!ensureDirRecursive(dir)) {
      return false;
    }
  }

  logFile_ = SD.open(normalized, FILE_APPEND);
  if (!logFile_) {
    logFile_ = SD.open(normalized, FILE_WRITE);
  }
  if (!logFile_) {
    setError("SD.open log failed");
    return false;
  }

  strlcpy(currentLogPath_, normalized, sizeof(currentLogPath_));
  clearError();
  return true;
}

size_t TBeamStorage::write(const uint8_t* data, size_t len) {
  if (!data || len == 0 || !logFile_) {
    return 0;
  }
  const size_t wrote = logFile_.write(data, len);
  if (wrote != len) {
    setError("SD log write short");
  }
  return wrote;
}

size_t TBeamStorage::print(const char* text) {
  if (!text) {
    return 0;
  }
  return write((const uint8_t*)text, strlen(text));
}

size_t TBeamStorage::println(const char* text) {
  size_t wrote = print(text);
  static const char newline[] = "\n";
  wrote += write((const uint8_t*)newline, 1);
  return wrote;
}

bool TBeamStorage::flush() {
  if (!logFile_) {
    return false;
  }
  logFile_.flush();
  return true;
}

void TBeamStorage::closeLog() {
  if (logFile_) {
    logFile_.flush();
    logFile_.close();
  }
  currentLogPath_[0] = '\0';
}

void TBeamStorage::listFiles(Print& out, const char* path, uint8_t depth) {
  if (!ready()) {
    out.println("SD not mounted");
    return;
  }

  char normalized[128];
  if (!normalizePath(path ? path : "/", normalized, sizeof(normalized))) {
    out.println("path too long");
    return;
  }

  File root = SD.open(normalized, FILE_READ);
  if (!root) {
    out.printf("open failed: %s\r\n", normalized);
    return;
  }
  if (!root.isDirectory()) {
    out.printf("%s %lu\r\n", normalized, (unsigned long)root.size());
    root.close();
    return;
  }
  listFilesRecursive(out, root, normalized, depth);
  root.close();
}

bool TBeamStorage::removeFile(const char* path) {
  if (!ready()) {
    setError("SD not mounted");
    return false;
  }

  char normalized[128];
  if (!normalizePath(path, normalized, sizeof(normalized))) {
    setError("remove path invalid");
    return false;
  }
  if (!SD.exists(normalized)) {
    setError("remove path missing");
    return false;
  }
  if (!SD.remove(normalized)) {
    setError("SD.remove failed");
    return false;
  }
  clearError();
  return true;
}

void TBeamStorage::logf(const char* fmt, ...) {
  char msg[196];
  va_list args;
  va_start(args, fmt);
  vsnprintf(msg, sizeof(msg), fmt, args);
  va_end(args);
  diagnostic_.printf("[%10lu][storage:%06lu] %s\r\n",
                     (unsigned long)millis(),
                     (unsigned long)logSeq_++,
                     msg);
}

void TBeamStorage::notify(SdEvent event, const char* message) {
  if (callback_) {
    callback_(event, message);
  }
}

void TBeamStorage::setError(const char* message) {
  strlcpy(lastError_, message ? message : "", sizeof(lastError_));
}

void TBeamStorage::clearError() {
  lastError_[0] = '\0';
}

void TBeamStorage::forceSpiDeselected() {
  pinMode(tbeam_supreme::sdCs(), OUTPUT);
  digitalWrite(tbeam_supreme::sdCs(), HIGH);
  pinMode(tbeam_supreme::imuCs(), OUTPUT);
  digitalWrite(tbeam_supreme::imuCs(), HIGH);
}

void TBeamStorage::dumpSdPins(const char* tag) {
  if (!config_.enablePinDumps) {
    return;
  }
  const gpio_num_t cs = (gpio_num_t)tbeam_supreme::sdCs();
  const gpio_num_t sck = (gpio_num_t)tbeam_supreme::sdSck();
  const gpio_num_t miso = (gpio_num_t)tbeam_supreme::sdMiso();
  const gpio_num_t mosi = (gpio_num_t)tbeam_supreme::sdMosi();
  logf("PINS(%s): CS=%d SCK=%d MISO=%d MOSI=%d",
       tag ? tag : "?",
       gpio_get_level(cs),
       gpio_get_level(sck),
       gpio_get_level(miso),
       gpio_get_level(mosi));
}

bool TBeamStorage::initPmuForSdPower() {
  if (!tbeam_supreme::initPmuForPeripherals(pmu_, &diagnostic_)) {
    setError("PMU init failed");
    return false;
  }
  return true;
}

void TBeamStorage::cycleSdRail(uint32_t offMs, uint32_t onSettleMs) {
  if (!config_.enableSdRailCycle || !pmu_) {
    return;
  }
  forceSpiDeselected();
  pmu_->disablePowerOutput(XPOWERS_BLDO1);
  delay(offMs);
  pmu_->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
  pmu_->enablePowerOutput(XPOWERS_BLDO1);
  delay(onSettleMs);
}

bool TBeamStorage::tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose) {
  SD.end();
  bus.end();
  delay(10);
  forceSpiDeselected();

  bus.begin(tbeam_supreme::sdSck(), tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi(), tbeam_supreme::sdCs());
  digitalWrite(tbeam_supreme::sdCs(), HIGH);
  delay(2);
  for (uint8_t i = 0; i < 10; ++i) {
    bus.transfer(0xFF);
  }
  delay(2);

  if (verbose) {
    logf("SD: trying bus=%s freq=%lu Hz", busName, (unsigned long)hz);
  }

  if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) {
    return false;
  }
  if (SD.cardType() == CARD_NONE) {
    SD.end();
    return false;
  }

  sdSpi_ = &bus;
  clearError();
  return true;
}

bool TBeamStorage::mountPreferred(bool verbose) {
  return tryMountWithBus(sdSpiH_, "HSPI", 400000, verbose);
}

bool TBeamStorage::mountCardFullScan() {
  const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000};
  for (uint8_t i = 0; i < sizeof(freqs) / sizeof(freqs[0]); ++i) {
    if (tryMountWithBus(sdSpiH_, "HSPI", freqs[i], true)) {
      logf("SD: mounted on HSPI");
      return true;
    }
  }
  for (uint8_t i = 0; i < sizeof(freqs) / sizeof(freqs[0]); ++i) {
    if (tryMountWithBus(sdSpiF_, "FSPI", freqs[i], true)) {
      logf("SD: mounted on FSPI");
      return true;
    }
  }
  return false;
}

bool TBeamStorage::verifyMountedCard() {
  if (SD.cardType() == CARD_NONE) {
    return false;
  }
  File root = SD.open("/", FILE_READ);
  const bool ok = root && root.isDirectory();
  root.close();
  return ok;
}

void TBeamStorage::setStateMounted() {
  if (state_ != SdState::MOUNTED) {
    logf("SD: mounted");
    mountedEventPending_ = true;
    notify(SdEvent::CARD_MOUNTED, "SD mounted");
  }
  state_ = SdState::MOUNTED;
}

void TBeamStorage::setStateAbsent() {
  if (state_ == SdState::MOUNTED) {
    removedEventPending_ = true;
    notify(SdEvent::CARD_REMOVED, "SD removed");
  } else if (state_ == SdState::UNKNOWN) {
    notify(SdEvent::NO_CARD, "SD not mounted");
  }
  state_ = SdState::ABSENT;
}

void TBeamStorage::listFilesRecursive(Print& out, File& dir, const char* parentPath, uint8_t depth) {
  File entry = dir.openNextFile();
  while (entry) {
    const char* name = entry.name();
    if (entry.isDirectory()) {
      out.printf("%s/\r\n", name);
      if (depth > 0) {
        listFilesRecursive(out, entry, name, depth - 1);
      }
    } else {
      out.printf("%s %lu\r\n", name, (unsigned long)entry.size());
    }
    entry.close();
    entry = dir.openNextFile();
  }
  (void)parentPath;
}

bool TBeamStorage::normalizePath(const char* input, char* out, size_t outSize) const {
  if (!input || !out || outSize < 2) {
    return false;
  }
  const int n = input[0] == '/' ? snprintf(out, outSize, "%s", input) : snprintf(out, outSize, "/%s", input);
  return n > 0 && (size_t)n < outSize;
}

}  // namespace tbeam
