#include "cf_datahive_cpp/cf_datahive_cpp.h"

#include <chrono>
#include <cctype>
#include <cstdint>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <random>
#include <sstream>
#include <string>

#include "cf_step_utils.h"
#include "duckdb.h"

namespace cf_datahive_cpp {
namespace {

constexpr const char* kCanonicalDataHiveLiteral = "workspace/data_hive";

bool is_valid_segment(const std::string& value) {
  if (value.empty()) {
    return false;
  }
  for (char ch : value) {
    const unsigned char uch = static_cast<unsigned char>(ch);
    if (!std::isalnum(uch) && ch != '_' && ch != '-') {
      return false;
    }
  }
  return true;
}

std::string generate_run_id() {
  auto now = std::chrono::system_clock::now();
  std::time_t now_time_t = std::chrono::system_clock::to_time_t(now);
  std::tm tm{};
#if defined(_WIN32)
  gmtime_s(&tm, &now_time_t);
#else
  gmtime_r(&now_time_t, &tm);
#endif

  char prefix[32];
  std::strftime(prefix, sizeof(prefix), "%Y%m%dT%H%M%SZ", &tm);

  std::random_device rd;
  std::mt19937_64 rng(rd());
  std::uniform_int_distribution<uint64_t> dist;
  const uint64_t random_part = dist(rng);

  std::ostringstream run_id;
  run_id << prefix << "_" << std::hex << std::setw(12) << std::setfill('0')
         << (random_part & 0xFFFFFFFFFFFFull);
  return run_id.str();
}

std::tm utc_now_tm() {
  auto now = std::chrono::system_clock::now();
  std::time_t now_time_t = std::chrono::system_clock::to_time_t(now);
  std::tm tm{};
#if defined(_WIN32)
  gmtime_s(&tm, &now_time_t);
#else
  gmtime_r(&now_time_t, &tm);
#endif
  return tm;
}

std::string session_id_for_tm(const std::tm& tm) {
  char buffer[32];
  std::strftime(buffer, sizeof(buffer), "session-%Y%m%d", &tm);
  return std::string(buffer);
}

int next_session_run_index(const std::filesystem::path& session_root) {
  const std::filesystem::path runs_dir = session_root / "runs";
  if (!std::filesystem::exists(runs_dir)) {
    return 0;
  }

  int count = 0;
  std::error_code ec;
  for (const auto& entry : std::filesystem::directory_iterator(runs_dir, ec)) {
    if (ec) {
      return 0;
    }
    if (!entry.is_directory()) {
      continue;
    }
    const std::filesystem::path manifest_path = entry.path() / "manifest.json";
    if (std::filesystem::exists(manifest_path)) {
      ++count;
    }
  }
  return count;
}

bool write_text_file(const std::filesystem::path& path, const std::string& text) {
  std::error_code ec;
  std::filesystem::create_directories(path.parent_path(), ec);

  std::ofstream output(path, std::ios::out | std::ios::trunc);
  if (!output.is_open()) {
    return false;
  }
  output << text;
  return output.good();
}

std::string escape_sql_string(const std::string& value) {
  std::string escaped;
  escaped.reserve(value.size());
  for (char ch : value) {
    escaped.push_back(ch);
    if (ch == '\'') {
      escaped.push_back('\'');
    }
  }
  return escaped;
}

std::string resolve_workspace_root(const std::string& raw_workspace_root) {
  if (raw_workspace_root.empty()) {
    return "workspace";
  }

  std::filesystem::path workspace(raw_workspace_root);
  workspace = workspace.lexically_normal();

  // Keep compatibility for callers that accidentally pass workspace/data_hive.
  std::filesystem::path canonical_hint(kCanonicalDataHiveLiteral);
  if (workspace.filename() == canonical_hint.filename() && workspace.has_parent_path()) {
    workspace = workspace.parent_path();
  }
  return workspace.string();
}

std::string build_manifest_json(
  const std::string& pipeline_id,
  const std::string& pipeline_label,
  const std::string& run_id,
  const std::string& session_id,
  int session_run_index,
  const std::string& measurements_relative_path,
  const std::string& created_at,
  int row_count) {
  // Keep the native writer output compatible with cf_datahive.manifest.TableEntry.
  constexpr const char* kMeasurementsSchemaFingerprint = "cf_datahive_cpp_measurements_v1";

  std::ostringstream manifest;
  manifest << "{\n"
           << "  \"schema_version\": \"1.0\",\n"
           << "  \"pipeline_id\": \"" << cf_escape_json_string(pipeline_id) << "\",\n";
  if (!pipeline_label.empty()) {
    manifest << "  \"pipeline_label\": \"" << cf_escape_json_string(pipeline_label) << "\",\n";
  } else {
    manifest << "  \"pipeline_label\": null,\n";
  }
  manifest << "  \"run_id\": \"" << cf_escape_json_string(run_id) << "\",\n"
           << "  \"session_id\": \"" << cf_escape_json_string(session_id) << "\",\n"
           << "  \"session_run_index\": " << session_run_index << ",\n"
           << "  \"created_at\": \"" << cf_escape_json_string(created_at) << "\",\n"
           << "  \"status\": \"committed\",\n"
           << "  \"tables\": {\n"
           << "    \"measurements\": {\n"
           << "      \"path\": \"" << cf_escape_json_string(measurements_relative_path) << "\",\n"
           << "      \"format\": \"parquet\",\n"
           << "      \"schema_fingerprint\": \"" << kMeasurementsSchemaFingerprint << "\",\n"
           << "      \"n_rows\": " << row_count << ",\n"
           << "      \"n_files\": 1,\n"
           << "      \"file_hashes\": null,\n"
           << "      \"created_by_step\": null,\n"
           << "      \"created_at\": \"" << cf_escape_json_string(created_at) << "\",\n"
           << "      \"locked_storage_mode\": \"partitioned_by_run\",\n"
           << "      \"storage_mode_policy\": {\n"
           << "        \"compacted_rows_per_run_max\": 50,\n"
           << "        \"compacted_min_run_count\": 200\n"
           << "      },\n"
           << "      \"mode_lock_reason\": \"native_writer_default\"\n"
           << "    }\n"
           << "  },\n"
           << "  \"artifacts\": {}\n"
           << "}\n";
  return manifest.str();
}

bool run_sql(duckdb_connection conn, const std::string& sql, std::string& error_out) {
  duckdb_result result{};
  const duckdb_state state = duckdb_query(conn, sql.c_str(), &result);
  if (state != DuckDBSuccess) {
    const char* detail = duckdb_result_error(&result);
    error_out = "duckdb query failed";
    if (detail != nullptr && detail[0] != '\0') {
      error_out += ": ";
      error_out += detail;
    }
    duckdb_destroy_result(&result);
    return false;
  }
  duckdb_destroy_result(&result);
  return true;
}

bool ensure_parquet_extension_loaded(duckdb_connection conn, std::string& error_out) {
  if (run_sql(conn, "LOAD parquet;", error_out)) {
    return true;
  }

  std::string load_error = error_out;
  error_out.clear();
  if (run_sql(conn, "INSTALL parquet; LOAD parquet;", error_out)) {
    return true;
  }

  if (!load_error.empty()) {
    error_out = load_error + " | fallback install/load failed: " + error_out;
  }
  return false;
}

bool write_measurements_parquet_via_duckdb(
  const std::filesystem::path& parquet_path,
  const std::string& pipeline_id,
  const std::string& run_id,
  const std::string& ts_utc,
  double value,
  const std::string& note,
  int cycle_count,
  std::string& error_out) {
  duckdb_database db = nullptr;
  duckdb_connection conn = nullptr;
  duckdb_prepared_statement stmt = nullptr;
  bool ok = false;

  do {
    if (duckdb_open(nullptr, &db) != DuckDBSuccess) {
      error_out = "duckdb_open failed";
      break;
    }
    if (duckdb_connect(db, &conn) != DuckDBSuccess) {
      error_out = "duckdb_connect failed";
      break;
    }
    if (!ensure_parquet_extension_loaded(conn, error_out)) {
      break;
    }

    if (!run_sql(
          conn,
          "CREATE TABLE measurements ("
          "pipeline_id VARCHAR, "
          "run_id VARCHAR, "
          "cycle_id INTEGER, "
          "ts_utc VARCHAR, "
          "value DOUBLE, "
          "note VARCHAR"
          ");",
          error_out)) {
      break;
    }

    const char* insert_sql =
      "INSERT INTO measurements (pipeline_id, run_id, cycle_id, ts_utc, value, note) "
      "VALUES (?, ?, ?, ?, ?, ?);";
    if (duckdb_prepare(conn, insert_sql, &stmt) != DuckDBSuccess) {
      error_out = "duckdb_prepare failed";
      if (stmt != nullptr) {
        const char* detail = duckdb_prepare_error(stmt);
        if (detail != nullptr && detail[0] != '\0') {
          error_out += ": ";
          error_out += detail;
        }
      }
      break;
    }

    for (int cycle = 0; cycle < cycle_count; ++cycle) {
      if (duckdb_clear_bindings(stmt) != DuckDBSuccess ||
          duckdb_bind_varchar(stmt, 1, pipeline_id.c_str()) != DuckDBSuccess ||
          duckdb_bind_varchar(stmt, 2, run_id.c_str()) != DuckDBSuccess ||
          duckdb_bind_int32(stmt, 3, cycle) != DuckDBSuccess ||
          duckdb_bind_varchar(stmt, 4, ts_utc.c_str()) != DuckDBSuccess ||
          duckdb_bind_double(stmt, 5, value) != DuckDBSuccess ||
          duckdb_bind_varchar(stmt, 6, note.c_str()) != DuckDBSuccess) {
        error_out = "duckdb_bind failed";
        break;
      }

      duckdb_result insert_result{};
      if (duckdb_execute_prepared(stmt, &insert_result) != DuckDBSuccess) {
        error_out = "duckdb_execute_prepared failed";
        const char* detail = duckdb_result_error(&insert_result);
        if (detail != nullptr && detail[0] != '\0') {
          error_out += ": ";
          error_out += detail;
        }
        duckdb_destroy_result(&insert_result);
        break;
      }
      duckdb_destroy_result(&insert_result);
    }

    if (!error_out.empty()) {
      break;
    }

    std::ostringstream copy_sql;
    copy_sql << "COPY measurements TO '" << escape_sql_string(parquet_path.generic_string())
             << "' (FORMAT 'parquet');";
    if (!run_sql(conn, copy_sql.str(), error_out)) {
      break;
    }

    ok = true;
  } while (false);

  if (stmt != nullptr) {
    duckdb_destroy_prepare(&stmt);
  }
  if (conn != nullptr) {
    duckdb_disconnect(&conn);
  }
  if (db != nullptr) {
    duckdb_close(&db);
  }

  return ok;
}

}  // namespace

SinkResult write_demo_measurements_run(const SinkConfig& config, const std::string& payload) {
  SinkResult result{};
  if (!is_valid_segment(config.pipeline_id)) {
    result.error = "invalid pipeline_id";
    return result;
  }

  const std::string workspace_root = resolve_workspace_root(config.workspace_root);
  if (workspace_root.empty()) {
    result.error = "workspace_root is empty";
    return result;
  }

  const int cycle_count = config.cycle_count > 0 ? config.cycle_count : 20;
  const std::string run_id = generate_run_id();
  const std::tm now_tm = utc_now_tm();
  const std::string session_id = session_id_for_tm(now_tm);

  std::filesystem::path workspace_path(workspace_root);
  std::filesystem::path data_hive_root = workspace_path / "data_hive";
  std::filesystem::path pipeline_root = data_hive_root / config.pipeline_id;
  char year_buf[8];
  char month_buf[4];
  char day_buf[4];
  std::strftime(year_buf, sizeof(year_buf), "%Y", &now_tm);
  std::strftime(month_buf, sizeof(month_buf), "%m", &now_tm);
  std::strftime(day_buf, sizeof(day_buf), "%d", &now_tm);
  std::filesystem::path session_root =
    pipeline_root / year_buf / month_buf / day_buf / session_id;
  const int session_run_index = next_session_run_index(session_root);
  std::filesystem::path run_root = session_root / "runs" / run_id;
  std::filesystem::path measurements_dir =
    session_root / "tables" / "measurements" / ("run_id=" + run_id);
  std::filesystem::path compacted_measurements_dir =
    session_root / "tables" / "measurements" / "_session_compacted";
  std::filesystem::path part_path = measurements_dir / "part-0000.parquet";
  std::filesystem::path manifest_path = run_root / "manifest.json";
  std::filesystem::path latest_path = pipeline_root / "latest.txt";
  std::filesystem::path latest_session_path = pipeline_root / "latest_session.txt";
  std::filesystem::path run_artifacts_dir = run_root / "artifacts";

  if (std::filesystem::exists(compacted_measurements_dir)) {
    result.error =
      "storage mode lock violation: measurements table is compacted_session but native writer uses partitioned_by_run";
    return result;
  }

  std::error_code ec;
  std::filesystem::create_directories(measurements_dir, ec);
  if (ec) {
    result.error = "failed to create data hive directories";
    return result;
  }
  std::filesystem::create_directories(run_artifacts_dir, ec);
  if (ec) {
    result.error = "failed to create run artifacts directory";
    return result;
  }

  double value = 0.0;
  if (!cf_extract_json_number(payload, "mean_pH", value)) {
    (void)cf_extract_json_number(payload, "value", value);
  }

  std::string ts_utc;
  if (!cf_extract_json_text(payload, "timestamp", ts_utc)) {
    if (!cf_extract_json_text(payload, "ts_utc", ts_utc)) {
      ts_utc = cf_now_iso8601_utc();
    }
  }

  std::string note;
  if (!cf_extract_json_text(payload, "note", note)) {
    note = "opcua_fifo_avg_demo";
  }

  std::string duckdb_error;
  if (!write_measurements_parquet_via_duckdb(
        part_path, config.pipeline_id, run_id, ts_utc, value, note, cycle_count, duckdb_error)) {
    result.error = duckdb_error;
    return result;
  }

  const std::string created_at = cf_now_iso8601_utc();
  const std::string measurements_relative_path =
    (std::filesystem::path("tables") / "measurements" / ("run_id=" + run_id) / "part-0000.parquet")
      .generic_string();
  const std::string manifest = build_manifest_json(
    config.pipeline_id,
    config.pipeline_label,
    run_id,
    session_id,
    session_run_index,
    measurements_relative_path,
    created_at,
    cycle_count);

  if (!write_text_file(manifest_path, manifest)) {
    result.error = "failed to write manifest";
    return result;
  }

  if (!write_text_file(latest_path, run_id)) {
    result.error = "failed to write latest.txt";
    return result;
  }
  if (!write_text_file(latest_session_path, session_id)) {
    result.error = "failed to write latest_session.txt";
    return result;
  }

  result.ok = true;
  result.run_id = run_id;
  result.session_id = session_id;
  result.session_run_index = session_run_index;
  result.status =
    "pipeline_id=" + config.pipeline_id +
    ",session_id=" + session_id +
    ",session_run_index=" + std::to_string(session_run_index) +
    ",run_id=" + run_id +
    ",cycles=" + std::to_string(cycle_count) +
    ",committed=true";
  return result;
}

}  // namespace cf_datahive_cpp
