#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::mount() {
  m_ready = false;
  m_lastError = "";
  m_path = "";
  m_buffer = "";
  if (m_file) {
    m_file.close();
  }
  if (!SPIFFS.begin(true)) {
    m_lastError = "SPIFFS.begin failed";
    return false;
  }
  if (!ensureDir()) {
    return false;
  }
  return true;
}

bool StorageManager::begin(const char* runId) {
  if (!mount()) {
    return false;
  }
  return startLog(runId, "");
}

bool StorageManager::startLog(const char* runId, const char* bootTimestampUtc) {
  m_ready = false;
  m_lastError = "";
  m_path = makeFilePath(runId);
  if (!openFile()) {
    return false;
  }
  m_ready = true;
  writeHeader(runId, bootTimestampUtc);
  return true;
}

bool StorageManager::mounted() const {
  File root = SPIFFS.open("/");
  return root && root.isDirectory();
}

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_buffer.length();
}

size_t StorageManager::logFileCount() const {
  File dir = SPIFFS.open("/");
  if (!dir || !dir.isDirectory()) {
    return 0;
  }
  size_t count = 0;
  File file = dir.openNextFile();
  while (file) {
    String name = file.name();
    if (isRecognizedLogName(name)) {
      ++count;
    }
    file = dir.openNextFile();
  }
  return count;
}

bool StorageManager::ensureDir() {
  if (strcmp(kLogDir, "/") == 0) {
    return true;
  }
  if (SPIFFS.exists(kLogDir)) {
    return true;
  }
  if (!SPIFFS.mkdir(kLogDir)) {
    m_lastError = "SPIFFS.mkdir failed";
    return false;
  }
  return true;
}

String StorageManager::makeFilePath(const char* runId) const {
  char path[96];
  const char* rid = runId ? runId : "run";
  char shortId[32];
  if (strlen(rid) >= 19 && rid[8] == '_' && rid[15] == '_') {
    snprintf(shortId,
             sizeof(shortId),
             "%.6s_%.6s_%s",
             rid + 2,
             rid + 9,
             rid + 16);
  } else {
    snprintf(shortId, sizeof(shortId), "%s", rid);
  }
  if (strcmp(kLogDir, "/") == 0) {
    snprintf(path, sizeof(path), "/%s.csv", shortId);
  } else {
    snprintf(path, sizeof(path), "%s/%s.csv", kLogDir, shortId);
  }
  return String(path);
}

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

void StorageManager::writeHeader(const char* runId, const char* bootTimestampUtc) {
  if (!m_file) {
    return;
  }
  if (m_file.size() > 0) {
    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,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();
}

void StorageManager::appendLine(const String& line) {
  m_buffer += line;
  if (!m_buffer.endsWith("\n")) {
    m_buffer += "\n";
  }
}

void StorageManager::appendSampleTsv(const GnssSample& sample, const char* runId, const char* bootTimestampUtc) {
  if (!m_file) {
    return;
  }
  if (m_file.size() == 0) {
    writeHeader(runId, bootTimestampUtc);
  }
  String line = "sample,";
  line += sampleTimestamp(sample);
  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 += ",,,,,,,";
  appendLine(line);
}

void StorageManager::appendSatelliteTsv(const GnssSample& sample,
                                        const SatelliteInfo* satellites,
                                        size_t satelliteCount,
                                        const char* runId,
                                        const char* bootTimestampUtc) {
  if (!satellites || satelliteCount == 0 || !m_file) {
    return;
  }
  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 += 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";
    appendLine(line);
  }
}

void StorageManager::flush() {
  if (!m_file || m_buffer.isEmpty()) {
    return;
  }
  m_file.print(m_buffer);
  m_file.flush();
  m_buffer = "";
}

void StorageManager::close() {
  flush();
  if (m_file) {
    m_file.close();
  }
}

void StorageManager::listFiles(Stream& out) {
  if (!mounted()) {
    out.println("storage not mounted");
    return;
  }
  File dir = SPIFFS.open("/");
  if (!dir || !dir.isDirectory()) {
    out.println("root directory unavailable");
    return;
  }
  File file = dir.openNextFile();
  if (!file) {
    out.println("(no files)");
    return;
  }
  while (file) {
    String name = file.name();
    if (isRecognizedLogName(name)) {
      out.printf("%s\t%u\n", file.name(), (unsigned)file.size());
    }
    file = dir.openNextFile();
  }
}

void StorageManager::catFile(Stream& out, const char* path) {
  if (!mounted()) {
    out.println("storage not mounted");
    return;
  }
  if (!path || path[0] == '\0') {
    out.println("cat requires a filename");
    return;
  }
  String fullPath = path[0] == '/' ? String(path) : String("/") + path;
  File file = SPIFFS.open(fullPath, 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();
  }
}

void StorageManager::eraseLogs(Stream& out) {
  if (!mounted()) {
    out.println("storage not mounted");
    return;
  }
  File dir = SPIFFS.open("/");
  if (!dir || !dir.isDirectory()) {
    out.println("root directory unavailable");
    return;
  }
  File file = dir.openNextFile();
  while (file) {
    String path = file.path();
    bool isLog = isRecognizedLogName(path);
    file.close();
    if (isLog) {
      SPIFFS.remove(path);
    }
    file = dir.openNextFile();
  }
  out.println("logs erased");
}

}  // namespace field_qa
