// rns-provision - generate microReticulum identity keypairs + provisioning bundles
//
// Examples:
//   ./rns-provision --quantity 6 --format tsv
//   ./rns-provision -q 6 -f json --public
//   ./rns-provision -q 6 --outdir provisioning/20260212_1030 --bundle both --public
//
// Notes:
// - Writes sensitive material (private keys). Protect your output directories.
// - identity.bin is raw private key bytes as returned by microReticulum Identity.
//
// $Header$
// $Id$

#include <Identity.h>

#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

#if __has_include(<filesystem>)
  #include <filesystem>
  namespace fs = std::filesystem;
#else
  #error "This tool requires <filesystem> (C++17)."
#endif

#ifndef _WIN32
  #include <sys/stat.h>
#endif

static constexpr const char* TOOL_NAME    = "rns-provision";
static constexpr const char* TOOL_VERSION = "0.1.0";

static void usage(const char* argv0, bool full = true) {
  std::cerr << TOOL_NAME << " " << TOOL_VERSION << "\n\n";

  std::cerr
    << "Usage:\n"
    << "  " << argv0 << " --quantity N [options]\n\n";

  if (!full) return;

  std::cerr
    << "Options:\n"
    << "  -q, --quantity N       Number of identities to generate (required)\n"
    << "  -f, --format FMT       Stdout format: tsv (default) or json\n"
    << "      --public           Include public_key in stdout and identity.json\n"
    << "      --outdir DIR       Write provisioning bundle to DIR\n"
    << "      --bundle MODE      none|json|bin|both (default: both if --outdir)\n"
    << "      --prefix NAME      Device directory prefix (default: device)\n"
    << "  -h, --help             Show this help message and exit\n"
    << "\n"
    << "Examples:\n"
    << "  " << argv0 << " -q 6\n"
    << "  " << argv0 << " -q 6 --outdir provisioning/20260212_1030 --bundle both\n"
    << "\n";
}

static bool is_flag(const std::string& a, const char* s) { return a == s; }

static std::string utc_now_iso8601() {
  using namespace std::chrono;
  auto now = system_clock::now();
  std::time_t t = system_clock::to_time_t(now);
  std::tm tm{};
#ifdef _WIN32
  gmtime_s(&tm, &t);
#else
  gmtime_r(&t, &tm);
#endif
  std::ostringstream oss;
  oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ");
  return oss.str();
}

static std::string json_escape(const std::string& s) {
  std::ostringstream o;
  for (unsigned char c : s) {
    switch (c) {
      case '\"': o << "\\\""; break;
      case '\\': o << "\\\\"; break;
      case '\b': o << "\\b"; break;
      case '\f': o << "\\f"; break;
      case '\n': o << "\\n"; break;
      case '\r': o << "\\r"; break;
      case '\t': o << "\\t"; break;
      default:
        if (c < 0x20) {
          o << "\\u" << std::hex << std::setw(4) << std::setfill('0') << int(c);
        } else {
          o << c;
        }
    }
  }
  return o.str();
}

static std::string zpad_int(int value, int width) {
  std::ostringstream oss;
  oss << std::setw(width) << std::setfill('0') << value;
  return oss.str();
}

static void ensure_dir(const fs::path& p) {
  std::error_code ec;
  if (fs::exists(p, ec)) {
    if (!fs::is_directory(p, ec)) {
      throw std::runtime_error("Path exists but is not a directory: " + p.string());
    }
    return;
  }
  if (!fs::create_directories(p, ec)) {
    throw std::runtime_error("Failed to create directory: " + p.string() + " (" + ec.message() + ")");
  }
}

static void write_text_file(const fs::path& p, const std::string& data) {
  std::ofstream out(p, std::ios::out | std::ios::trunc);
  if (!out) throw std::runtime_error("Failed to open for write: " + p.string());
  out << data;
  out.close();
  if (!out) throw std::runtime_error("Failed writing file: " + p.string());

#ifndef _WIN32
  // Best-effort: 0600 for sensitive text files (private keys)
  ::chmod(p.c_str(), 0600);
#endif
}

static void write_binary_file(const fs::path& p, const std::vector<uint8_t>& bytes) {
  std::ofstream out(p, std::ios::out | std::ios::binary | std::ios::trunc);
  if (!out) throw std::runtime_error("Failed to open for write: " + p.string());
  out.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
  out.close();
  if (!out) throw std::runtime_error("Failed writing file: " + p.string());

#ifndef _WIN32
  ::chmod(p.c_str(), 0600);
#endif
}

enum class BundleMode { NONE, JSON, BIN, BOTH };

static BundleMode parse_bundle_mode(const std::string& s) {
  if (s == "none") return BundleMode::NONE;
  if (s == "json") return BundleMode::JSON;
  if (s == "bin")  return BundleMode::BIN;
  if (s == "both") return BundleMode::BOTH;
  throw std::runtime_error("Invalid --bundle value (must be none|json|bin|both)");
}

// Convert microReticulum Bytes-ish type to std::vector<uint8_t>.
// This assumes the returned object supports .size() and operator[].
// If your microReticulum Bytes type differs, we’ll adjust here.
template <typename BytesT>
static std::vector<uint8_t> to_u8vec(const BytesT& b) {
  std::vector<uint8_t> v;
  v.reserve(static_cast<size_t>(b.size()));
  for (size_t i = 0; i < static_cast<size_t>(b.size()); i++) {
    v.push_back(static_cast<uint8_t>(b[i]));
  }
  return v;
}

int main(int argc, char** argv) {
  try {
    int quantity = -1;
    std::string format = "tsv";
    bool include_public = false;

    bool do_outdir = false;
    fs::path outdir;
    std::string prefix = "device";
    BundleMode bundle_mode = BundleMode::NONE;
    bool bundle_mode_explicit = false;

    for (int i = 1; i < argc; i++) {
      std::string a(argv[i]);

      if (is_flag(a, "-h") || is_flag(a, "--help")) {
        usage(argv[0], true);
        return 0;
      } else if (is_flag(a, "-q") || is_flag(a, "--quantity")) {
        if (i + 1 >= argc) throw std::runtime_error("Missing value for --quantity");
        quantity = std::stoi(argv[++i]);
      } else if (is_flag(a, "-f") || is_flag(a, "--format")) {
        if (i + 1 >= argc) throw std::runtime_error("Missing value for --format");
        format = argv[++i];
      } else if (is_flag(a, "--public")) {
        include_public = true;
      } else if (is_flag(a, "--outdir")) {
        if (i + 1 >= argc) throw std::runtime_error("Missing value for --outdir");
        outdir = fs::path(argv[++i]);
        do_outdir = true;
      } else if (is_flag(a, "--bundle")) {
        if (i + 1 >= argc) throw std::runtime_error("Missing value for --bundle");
        bundle_mode = parse_bundle_mode(argv[++i]);
        bundle_mode_explicit = true;
      } else if (is_flag(a, "--prefix")) {
        if (i + 1 >= argc) throw std::runtime_error("Missing value for --prefix");
        prefix = argv[++i];
      } else if (is_flag(a, "--version")) {
	std::cout << TOOL_NAME << " " << TOOL_VERSION << "\n";
	return 0;
	
      } else {
        throw std::runtime_error("Unknown argument: " + a);
      }
    }

    if (quantity <= 0) {
      usage(argv[0], true);
      return 2;
    }
    if (!(format == "tsv" || format == "json")) {
      throw std::runtime_error("Invalid --format (must be tsv or json)");
    }

    // Default bundle behavior: if user set --outdir but not --bundle, write both.
    if (do_outdir && !bundle_mode_explicit) {
      bundle_mode = BundleMode::BOTH;
    }

    if (do_outdir) {
      ensure_dir(outdir);
    }

    struct DeviceRec {
      int n;
      std::string device_name;
      std::string id_hex;
      std::string priv_hex;
      std::string pub_hex;
      std::vector<uint8_t> priv_bin;
      fs::path device_dir;
    };

    std::vector<DeviceRec> devices;
    devices.reserve(static_cast<size_t>(quantity));

    for (int n = 1; n <= quantity; n++) {
      RNS::Identity ident(true);

      DeviceRec rec;
      rec.n = n;
      rec.device_name = prefix + "_" + zpad_int(n, 3);

      // These methods matched your earlier build. If microReticulum changes, we adjust here.
      rec.id_hex   = ident.hash().toHex();
      rec.priv_hex = ident.get_private_key().toHex();
      if (include_public) rec.pub_hex = ident.get_public_key().toHex();

      // Write binary blob for the private key
      rec.priv_bin = to_u8vec(ident.get_private_key());

      if (do_outdir) {
        rec.device_dir = outdir / rec.device_name;
        ensure_dir(rec.device_dir);
      }

      devices.push_back(std::move(rec));
    }

    // ---- stdout ----
    if (format == "tsv") {
      std::cout << "n\tdevice\tid_hex\tprivate_key_hex";
      if (include_public) std::cout << "\tpublic_key_hex";
      std::cout << "\n";
      for (const auto& d : devices) {
        std::cout << d.n << "\t" << d.device_name << "\t" << d.id_hex << "\t" << d.priv_hex;
        if (include_public) std::cout << "\t" << d.pub_hex;
        std::cout << "\n";
      }
    } else {
      std::cout << "[\n";
      for (size_t i = 0; i < devices.size(); i++) {
        const auto& d = devices[i];
        std::cout << "  {\n";
        std::cout << "    \"n\": " << d.n << ",\n";
        std::cout << "    \"device\": \"" << json_escape(d.device_name) << "\",\n";
        std::cout << "    \"id\": \"" << d.id_hex << "\",\n";
        std::cout << "    \"private_key\": \"" << d.priv_hex << "\"";
        if (include_public) {
          std::cout << ",\n    \"public_key\": \"" << d.pub_hex << "\"\n";
        } else {
          std::cout << "\n";
        }
        std::cout << "  }" << (i + 1 == devices.size() ? "\n" : ",\n");
      }
      std::cout << "]\n";
    }

    // ---- bundle output ----
    if (do_outdir && bundle_mode != BundleMode::NONE) {
      const std::string created_utc = utc_now_iso8601();

      // manifest.json
      {
        std::ostringstream m;
        m << "{\n";
        m << "  \"schema_version\": 1,\n";
        m << "  \"created_utc\": \"" << created_utc << "\",\n";
        m << "  \"quantity\": " << quantity << ",\n";
        m << "  \"prefix\": \"" << json_escape(prefix) << "\",\n";
        m << "  \"devices\": [\n";
        for (size_t i = 0; i < devices.size(); i++) {
          const auto& d = devices[i];
          m << "    {\n";
          m << "      \"n\": " << d.n << ",\n";
          m << "      \"device\": \"" << json_escape(d.device_name) << "\",\n";
          m << "      \"id\": \"" << d.id_hex << "\"";
          if (bundle_mode == BundleMode::JSON || bundle_mode == BundleMode::BOTH) {
            m << ",\n      \"identity_json\": \"" << json_escape((d.device_name + "/identity.json")) << "\"";
          }
          if (bundle_mode == BundleMode::BIN || bundle_mode == BundleMode::BOTH) {
            m << ",\n      \"identity_bin\": \"" << json_escape((d.device_name + "/identity.bin")) << "\"";
          }
          m << "\n    }" << (i + 1 == devices.size() ? "\n" : ",\n");
        }
        m << "  ]\n";
        m << "}\n";
        write_text_file(outdir / "manifest.json", m.str());
      }

      for (const auto& d : devices) {
        // label.txt
        {
          std::ostringstream l;
          l << d.device_name << "\n";
          l << "id: " << d.id_hex << "\n";
          write_text_file(d.device_dir / "label.txt", l.str());
        }

        // identity.json
        if (bundle_mode == BundleMode::JSON || bundle_mode == BundleMode::BOTH) {
          std::ostringstream j;
          j << "{\n";
          j << "  \"schema_version\": 1,\n";
          j << "  \"created_utc\": \"" << created_utc << "\",\n";
          j << "  \"device\": \"" << json_escape(d.device_name) << "\",\n";
          j << "  \"id\": \"" << d.id_hex << "\",\n";
          j << "  \"private_key\": \"" << d.priv_hex << "\"";
          if (include_public) {
            j << ",\n  \"public_key\": \"" << d.pub_hex << "\"\n";
          } else {
            j << "\n";
          }
          j << "}\n";
          write_text_file(d.device_dir / "identity.json", j.str());
        }

        // identity.bin
        if (bundle_mode == BundleMode::BIN || bundle_mode == BundleMode::BOTH) {
          write_binary_file(d.device_dir / "identity.bin", d.priv_bin);
        }
      }

      std::cerr << "Wrote provisioning bundle to: " << outdir.string() << "\n";
    }

    return 0;
  } catch (const std::exception& e) {
    std::cerr << "Error: " << e.what() << "\n";
    return 1;
  }
}
