#include "StorageManager.h"

#include "Config.h"
#include "GnssTypes.h"

namespace field_qa {

namespace {

static constexpr char kLogFieldDelimiter = ',';

static bool isRecognizedLogName(const String& name) {
  return name.endsWith(".csv") || name.endsWith(".tsv");
}

static String formatFloat(float value, bool valid, uint8_t decimals = 1) {
  if (!valid) {
    return "";
  }
  return String(value, (unsigned int)decimals);
}

static String formatDouble(double value, bool valid, uint8_t decimals = 6) {
  if (!valid) {
    return "";
  }
  return String(value, (unsigned int)decimals);
}

static String sampleTimestamp(const GnssSample& sample) {
  if (!sample.validTime) {
    return "";
  }
  char buf[24];
  snprintf(buf,
           sizeof(buf),
           "%04u-%02u-%02uT%02u:%02u:%02uZ",
           (unsigned)sample.year,
           (unsigned)sample.month,
           (unsigned)sample.day,
           (unsigned)sample.hour,
           (unsigned)sample.minute,
           (unsigned)sample.second);
  return String(buf);
}

static const char* constellationForTalker(const char* talker) {
  if (!talker) return "UNKNOWN";
  if (strcmp(talker, "GP") == 0 || strcmp(talker, "GN") == 0) return "GPS";
  if (strcmp(talker, "GA") == 0) return "GALILEO";
  if (strcmp(talker, "GL") == 0) return "GLONASS";
  if (strcmp(talker, "GB") == 0 || strcmp(talker, "BD") == 0) return "BEIDOU";
  if (strcmp(talker, "GI") == 0) return "NAVIC";
  if (strcmp(talker, "GQ") == 0) return "QZSS";
  if (strcmp(talker, "GS") == 0) return "SBAS";
  return "UNKNOWN";
}

}  // namespace

bool StorageManager::startLog(const char* runId, const char* bootTimestampUtc) {
  close();
  m_ready = false;
  m_lastError = "";
  m_path = makeFilePath(runId);
  m_newFile = !SD.exists(m_path.c_str());
  if (!ensureDir() || !openFile()) {
    return false;
  }
  m_ready = true;
  writeHeader(runId, bootTimestampUtc);
  return true;
}

bool StorageManager::mounted() const {
  File root = SD.open("/", FILE_READ);
  const bool ok = root && root.isDirectory();
  root.close();
  return ok;
}

bool StorageManager::ready() const {
  return m_ready;
}

const char* StorageManager::currentPath() const {
  return m_path.c_str();
}

const char* StorageManager::lastError() const {
  return m_lastError.c_str();
}

bool StorageManager::fileOpen() const {
  return (bool)m_file;
}

size_t StorageManager::bufferedBytes() const {
  return m_bufferLengths[0] + m_bufferLengths[1];
}

size_t StorageManager::countLogsRecursive(const char* path) const {
  File dir = SD.open(path, FILE_READ);
  if (!dir || !dir.isDirectory()) {
    dir.close();
    return 0;
  }

  size_t count = 0;
  File entry = dir.openNextFile();
  while (entry) {
    String name = entry.name();
    if (entry.isDirectory()) {
      count += countLogsRecursive(name.c_str());
    } else if (isRecognizedLogName(name)) {
      ++count;
    }
    entry.close();
    entry = dir.openNextFile();
  }
  dir.close();
  return count;
}

size_t StorageManager::logFileCount() const {
  return countLogsRecursive(kLogDir);
}

bool StorageManager::ensureDir() {
  String full(kLogDir);
  if (!full.startsWith("/")) {
    full = "/" + full;
  }

  int start = 1;
  while (start > 0 && start < (int)full.length()) {
    const int slash = full.indexOf('/', start);
    String partial = (slash < 0) ? full : full.substring(0, slash);
    if (!SD.exists(partial.c_str()) && !SD.mkdir(partial.c_str())) {
      m_lastError = "SD.mkdir failed";
      return false;
    }
    if (slash < 0) {
      break;
    }
    start = slash + 1;
  }
  return true;
}

String StorageManager::makeFilePath(const char* runId) const {
  char basePath[96];
  char candidatePath[112];
  const char* rid = (runId && runId[0] != '\0') ? runId : "run";
  snprintf(basePath, sizeof(basePath), "%s/%s", kLogDir, rid);
  snprintf(candidatePath, sizeof(candidatePath), "%s.csv", basePath);
  if (!SD.exists(candidatePath)) {
    return String(candidatePath);
  }

  for (uint16_t suffix = 2; suffix < 1000; ++suffix) {
    snprintf(candidatePath, sizeof(candidatePath), "%s_%02u.csv", basePath, (unsigned)suffix);
    if (!SD.exists(candidatePath)) {
      return String(candidatePath);
    }
  }

  snprintf(candidatePath, sizeof(candidatePath), "%s_overflow.csv", basePath);
  return String(candidatePath);
}

bool StorageManager::openFile() {
  m_file = SD.open(m_path.c_str(), FILE_WRITE);
  if (!m_file) {
    m_lastError = "SD.open write failed";
    return false;
  }
  return true;
}

bool StorageManager::reopenFileForAppend() {
  if (m_file) {
    m_file.close();
  }
  m_file = SD.open(m_path.c_str(), FILE_WRITE);
  if (!m_file) {
    return false;
  }
  return true;
}

void StorageManager::writeHeader(const char* runId, const char* bootTimestampUtc) {
  if (!m_file || !m_newFile) {
    return;
  }
  m_file.printf("# exercise: %s\n", kExerciseName);
  m_file.printf("# version: %s\n", kFirmwareVersion);
  m_file.printf("# board_id: %s\n", kBoardId);
  m_file.printf("# gnss_chip: %s\n", kGnssChip);
  m_file.printf("# storage: %s\n", kStorageName);
  m_file.printf("# sample_period_ms: %lu\n", (unsigned long)kSamplePeriodMs);
  m_file.printf("# log_period_ms: %lu\n", (unsigned long)kLogFlushPeriodMs);
  m_file.printf("# run_id: %s\n", runId ? runId : "");
  m_file.printf("# boot_timestamp_utc: %s\n", bootTimestampUtc ? bootTimestampUtc : "");
  m_file.printf("# created_by: ChatGPT/Codex handoff\n");
  m_file.print("record_type,timestamp_utc,sample_seq,ms_since_run_start,board_id,gnss_chip,firmware_exercise_name,firmware_version,boot_timestamp_utc,run_id,fix_type,fix_dimension,sats_in_view,sat_seen,sats_used,hdop,vdop,pdop,latitude,longitude,altitude_m,speed_mps,course_deg,pps_seen,quality_class,gps_count,galileo_count,glonass_count,beidou_count,navic_count,qzss_count,sbas_count,mean_cn0,max_cn0,age_of_fix_ms,ttff_ms,longest_no_fix_ms,sat_talker,sat_constellation,sat_prn,sat_elevation_deg,sat_azimuth_deg,sat_snr,sat_used_in_solution\n");
  m_file.flush();
  m_newFile = false;
}

bool StorageManager::writePendingBuffer() {
  if (!m_file) {
    return false;
  }

  for (uint8_t i = 0; i < 2; ++i) {
    if (!m_bufferPending[i] || m_bufferLengths[i] == 0) {
      continue;
    }
    if (!writeFully((const uint8_t*)m_buffers[i], m_bufferLengths[i], "buffer")) {
      m_ready = false;
      return false;
    }
    m_bufferLengths[i] = 0;
    m_bufferPending[i] = false;
  }
  return true;
}

bool StorageManager::appendBytes(const char* data, size_t len) {
  if (!m_file || !data || len == 0) {
    return false;
  }
  if (len > kStorageBufferBytes) {
    if (!writePendingBuffer()) {
      return false;
    }
    if (!writeFully((const uint8_t*)data, len, "large")) {
      m_ready = false;
      return false;
    }
    return true;
  }

  if ((m_bufferLengths[m_activeBuffer] + len) > kStorageBufferBytes) {
    m_bufferPending[m_activeBuffer] = true;
    m_activeBuffer ^= 1U;
    if (m_bufferPending[m_activeBuffer]) {
      if (!writePendingBuffer()) {
        return false;
      }
    }
    if (m_bufferLengths[m_activeBuffer] != 0) {
      m_bufferPending[m_activeBuffer] = true;
      if (!writePendingBuffer()) {
        return false;
      }
    }
  }

  memcpy(m_buffers[m_activeBuffer] + m_bufferLengths[m_activeBuffer], data, len);
  m_bufferLengths[m_activeBuffer] += len;
  return true;
}

bool StorageManager::writeFully(const uint8_t* data, size_t len, const char* context) {
  if (!m_file || !data || len == 0) {
    m_lastError = "writeFully invalid state";
    return false;
  }

  size_t totalWrote = 0;
  for (uint8_t attempt = 0; attempt <= kStorageWriteRetryCount; ++attempt) {
    const size_t filePos = (size_t)m_file.position();
    const size_t fileSize = (size_t)m_file.size();
    const size_t wrote = m_file.write(data + totalWrote, len - totalWrote);
    if (wrote > 0) {
      totalWrote += wrote;
      if (totalWrote >= len) {
        if (attempt > 0) {
          m_lastError = "";
        }
        return true;
      }
    }

    if (attempt >= kStorageWriteRetryCount) {
      const bool stillMounted = mounted();
      char err[192];
      snprintf(err,
               sizeof(err),
               "SD.write failed ctx=%s wrote=%u/%u pos=%u size=%u mounted=%s attempts=%u",
               context ? context : "?",
               (unsigned)totalWrote,
               (unsigned)len,
               (unsigned)filePos,
               (unsigned)fileSize,
               stillMounted ? "yes" : "no",
               (unsigned)(attempt + 1));
      m_lastError = err;
      return false;
    }

    delay(kStorageWriteRetryDelayMs);

    if (mounted()) {
      if (!reopenFileForAppend()) {
        delay(kStorageWriteRetryDelayMs);
      }
    }
  }

  m_lastError = "SD.write retry loop exhausted";
  return false;
}

bool StorageManager::appendLine(const String& line) {
  if (line.endsWith("\n")) {
    return appendBytes(line.c_str(), line.length());
  }
  String record = line;
  record += '\n';
  return appendBytes(record.c_str(), record.length());
}

bool StorageManager::appendSampleCsv(const GnssSample& sample,
                                     uint32_t sampleSeq,
                                     uint32_t msSinceRunStart,
                                     const char* runId,
                                     const char* bootTimestampUtc) {
  if (!m_file) {
    m_lastError = "log file not open";
    return false;
  }
  if (m_file.size() == 0) {
    writeHeader(runId, bootTimestampUtc);
  }

  String line = "sample,";
  line += sampleTimestamp(sample);
  line += kLogFieldDelimiter;
  line += String(sampleSeq);
  line += kLogFieldDelimiter;
  line += String(msSinceRunStart);
  line += kLogFieldDelimiter;
  line += kBoardId;
  line += kLogFieldDelimiter;
  line += kGnssChip;
  line += kLogFieldDelimiter;
  line += kExerciseName;
  line += kLogFieldDelimiter;
  line += kFirmwareVersion;
  line += kLogFieldDelimiter;
  line += (bootTimestampUtc ? bootTimestampUtc : "");
  line += kLogFieldDelimiter;
  line += (runId ? runId : "");
  line += kLogFieldDelimiter;
  line += fixTypeToString(sample.fixType);
  line += kLogFieldDelimiter;
  line += String(sample.fixDimension);
  line += kLogFieldDelimiter;
  line += (sample.satsInView >= 0 ? String(sample.satsInView) : "");
  line += kLogFieldDelimiter;
  line += (sample.satsInView >= 0 ? String(sample.satsInView) : "");
  line += kLogFieldDelimiter;
  line += (sample.satsUsed >= 0 ? String(sample.satsUsed) : "");
  line += kLogFieldDelimiter;
  line += formatFloat(sample.hdop, sample.validHdop);
  line += kLogFieldDelimiter;
  line += formatFloat(sample.vdop, sample.validVdop);
  line += kLogFieldDelimiter;
  line += formatFloat(sample.pdop, sample.validPdop);
  line += kLogFieldDelimiter;
  line += formatDouble(sample.latitude, sample.validLocation);
  line += kLogFieldDelimiter;
  line += formatDouble(sample.longitude, sample.validLocation);
  line += kLogFieldDelimiter;
  line += formatDouble(sample.altitudeM, sample.validAltitude, 2);
  line += kLogFieldDelimiter;
  line += formatFloat(sample.speedMps, sample.validSpeed, 2);
  line += kLogFieldDelimiter;
  line += formatFloat(sample.courseDeg, sample.validCourse, 1);
  line += kLogFieldDelimiter;
  line += sample.ppsSeen ? "1" : "0";
  line += kLogFieldDelimiter;
  line += qualityClassForSample(sample);
  line += kLogFieldDelimiter;
  line += String(sample.gpsCount);
  line += kLogFieldDelimiter;
  line += String(sample.galileoCount);
  line += kLogFieldDelimiter;
  line += String(sample.glonassCount);
  line += kLogFieldDelimiter;
  line += String(sample.beidouCount);
  line += kLogFieldDelimiter;
  line += String(sample.navicCount);
  line += kLogFieldDelimiter;
  line += String(sample.qzssCount);
  line += kLogFieldDelimiter;
  line += String(sample.sbasCount);
  line += kLogFieldDelimiter;
  line += formatFloat(sample.meanSnr, sample.meanSnr >= 0.0f, 1);
  line += kLogFieldDelimiter;
  line += (sample.maxSnr > 0 ? String(sample.maxSnr) : "");
  line += kLogFieldDelimiter;
  line += String(sample.ageOfFixMs);
  line += kLogFieldDelimiter;
  line += String(sample.ttffMs);
  line += kLogFieldDelimiter;
  line += String(sample.longestNoFixMs);
  line += ",,,,,,,";
  return appendLine(line);
}

bool StorageManager::appendSatelliteCsv(const GnssSample& sample,
                                        uint32_t sampleSeq,
                                        uint32_t msSinceRunStart,
                                        const SatelliteInfo* satellites,
                                        size_t satelliteCount,
                                        const char* runId,
                                        const char* bootTimestampUtc) {
  if (!satellites || satelliteCount == 0 || !m_file) {
    if (!m_file) {
      m_lastError = "log file not open";
    }
    return false;
  }
  if (m_file.size() == 0) {
    writeHeader(runId, bootTimestampUtc);
  }

  for (size_t i = 0; i < satelliteCount; ++i) {
    const SatelliteInfo& sat = satellites[i];
    if (!sat.valid) {
      continue;
    }
    String line = "satellite,";
    line += sampleTimestamp(sample);
    line += kLogFieldDelimiter;
    line += String(sampleSeq);
    line += kLogFieldDelimiter;
    line += String(msSinceRunStart);
    line += kLogFieldDelimiter;
    line += kBoardId;
    line += kLogFieldDelimiter;
    line += kGnssChip;
    line += kLogFieldDelimiter;
    line += kExerciseName;
    line += kLogFieldDelimiter;
    line += kFirmwareVersion;
    line += kLogFieldDelimiter;
    line += (bootTimestampUtc ? bootTimestampUtc : "");
    line += kLogFieldDelimiter;
    line += (runId ? runId : "");
    line += kLogFieldDelimiter;
    line += fixTypeToString(sample.fixType);
    line += kLogFieldDelimiter;
    line += String(sample.fixDimension);
    line += kLogFieldDelimiter;
    line += (sample.satsInView >= 0 ? String(sample.satsInView) : "");
    line += kLogFieldDelimiter;
    line += (sample.satsInView >= 0 ? String(sample.satsInView) : "");
    line += kLogFieldDelimiter;
    line += (sample.satsUsed >= 0 ? String(sample.satsUsed) : "");
    line += kLogFieldDelimiter;
    line += formatFloat(sample.hdop, sample.validHdop);
    line += kLogFieldDelimiter;
    line += formatFloat(sample.vdop, sample.validVdop);
    line += kLogFieldDelimiter;
    line += formatFloat(sample.pdop, sample.validPdop);
    line += kLogFieldDelimiter;
    line += formatDouble(sample.latitude, sample.validLocation);
    line += kLogFieldDelimiter;
    line += formatDouble(sample.longitude, sample.validLocation);
    line += kLogFieldDelimiter;
    line += formatDouble(sample.altitudeM, sample.validAltitude, 2);
    line += kLogFieldDelimiter;
    line += formatFloat(sample.speedMps, sample.validSpeed, 2);
    line += kLogFieldDelimiter;
    line += formatFloat(sample.courseDeg, sample.validCourse, 1);
    line += kLogFieldDelimiter;
    line += sample.ppsSeen ? "1" : "0";
    line += kLogFieldDelimiter;
    line += qualityClassForSample(sample);
    line += kLogFieldDelimiter;
    line += String(sample.gpsCount);
    line += kLogFieldDelimiter;
    line += String(sample.galileoCount);
    line += kLogFieldDelimiter;
    line += String(sample.glonassCount);
    line += kLogFieldDelimiter;
    line += String(sample.beidouCount);
    line += kLogFieldDelimiter;
    line += String(sample.navicCount);
    line += kLogFieldDelimiter;
    line += String(sample.qzssCount);
    line += kLogFieldDelimiter;
    line += String(sample.sbasCount);
    line += kLogFieldDelimiter;
    line += formatFloat(sample.meanSnr, sample.meanSnr >= 0.0f, 1);
    line += kLogFieldDelimiter;
    line += (sample.maxSnr > 0 ? String(sample.maxSnr) : "");
    line += kLogFieldDelimiter;
    line += String(sample.ageOfFixMs);
    line += kLogFieldDelimiter;
    line += String(sample.ttffMs);
    line += kLogFieldDelimiter;
    line += String(sample.longestNoFixMs);
    line += kLogFieldDelimiter;
    line += sat.talker;
    line += kLogFieldDelimiter;
    line += constellationForTalker(sat.talker);
    line += kLogFieldDelimiter;
    line += String(sat.prn);
    line += kLogFieldDelimiter;
    line += String(sat.elevation);
    line += kLogFieldDelimiter;
    line += String(sat.azimuth);
    line += kLogFieldDelimiter;
    line += String(sat.snr);
    line += kLogFieldDelimiter;
    line += sat.usedInSolution ? "1" : "0";
    if (!appendLine(line)) {
      return false;
    }
  }
  return true;
}

bool StorageManager::flush() {
  if (!m_file) {
    return false;
  }

  if (m_bufferLengths[m_activeBuffer] > 0) {
    m_bufferPending[m_activeBuffer] = true;
  }
  if (!writePendingBuffer()) {
    return false;
  }
  m_file.flush();
  return true;
}

void StorageManager::close() {
  (void)flush();
  if (m_file) {
    m_file.close();
  }
  m_ready = false;
  m_newFile = false;
}

bool StorageManager::normalizePath(const char* input, String& normalized) const {
  normalized = "";
  if (!input || input[0] == '\0') {
    return false;
  }

  normalized = input[0] == '/' ? String(input) : (String("/") + input);
  if (normalized.indexOf("..") >= 0) {
    normalized = "";
    return false;
  }
  return true;
}

void StorageManager::listFilesRecursive(File& dir, Stream& out) {
  File entry = dir.openNextFile();
  while (entry) {
    String name = entry.name();
    if (entry.isDirectory()) {
      out.printf("%s/\n", name.c_str());
      listFilesRecursive(entry, out);
    } else if (isRecognizedLogName(name)) {
      out.printf("%s\t%u\n", name.c_str(), (unsigned)entry.size());
    }
    entry.close();
    entry = dir.openNextFile();
  }
}

void StorageManager::listFiles(Stream& out) {
  if (!mounted()) {
    out.println("storage not mounted");
    return;
  }
  File dir = SD.open(kLogDir, FILE_READ);
  if (!dir || !dir.isDirectory()) {
    out.println("log directory unavailable");
    dir.close();
    return;
  }
  listFilesRecursive(dir, out);
  dir.close();
}

void StorageManager::catFile(Stream& out, const char* path) {
  if (!mounted()) {
    out.println("storage not mounted");
    return;
  }
  String fullPath;
  if (!normalizePath(path, fullPath)) {
    out.println("cat requires a valid filename");
    return;
  }
  File file = SD.open(fullPath.c_str(), FILE_READ);
  if (!file) {
    out.printf("unable to open %s\n", fullPath.c_str());
    return;
  }
  while (file.available()) {
    out.write(file.read());
  }
  if (file.size() > 0) {
    out.println();
  }
  file.close();
}

void StorageManager::eraseCsvRecursive(File& dir, const char* parentPath) {
  File entry = dir.openNextFile();
  while (entry) {
    String leaf = entry.name();
    const int slash = leaf.lastIndexOf('/');
    if (slash >= 0) {
      leaf.remove(0, slash + 1);
    }
    const String path = String(parentPath) + (String(parentPath).endsWith("/") ? "" : "/") + leaf;
    const bool isDir = entry.isDirectory();
    entry.close();
    if (isDir) {
      File subdir = SD.open(path.c_str(), FILE_READ);
      if (subdir) {
        eraseCsvRecursive(subdir, path.c_str());
        subdir.close();
      }
    } else if (isRecognizedLogName(path)) {
      SD.remove(path.c_str());
    }
    entry = dir.openNextFile();
  }
}

void StorageManager::eraseCsv(Stream& out) {
  if (!mounted()) {
    out.println("storage not mounted");
    return;
  }
  close();
  File dir = SD.open(kLogDir, FILE_READ);
  if (!dir || !dir.isDirectory()) {
    out.println("log directory unavailable");
    dir.close();
    return;
  }
  eraseCsvRecursive(dir, kLogDir);
  dir.close();
  out.println("csv files erased");
}

void StorageManager::eraseLogs(Stream& out) {
  eraseCsv(out);
}

bool StorageManager::eraseFile(const char* path) {
  String fullPath;
  if (!normalizePath(path, fullPath)) {
    m_lastError = "invalid path";
    return false;
  }
  if (m_path == fullPath && m_file) {
    close();
  }
  if (!SD.exists(fullPath.c_str())) {
    m_lastError = "path not found";
    return false;
  }
  if (!SD.remove(fullPath.c_str())) {
    m_lastError = "SD.remove failed";
    return false;
  }
  return true;
}

}  // namespace field_qa
