#include "common/type_registry.h"

#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>

#if defined(_WIN32)
#include <windows.h>
#elif defined(__APPLE__)
#include <mach-o/dyld.h>
#else
#include <limits.h>
#include <unistd.h>
#endif

namespace cf {

namespace {

using json = nlohmann::json;

#ifndef CF_TYPE_REGISTRY_V0_PATH
#define CF_TYPE_REGISTRY_V0_PATH ""
#endif

#ifndef CF_TYPE_REGISTRY_V0_FILENAME
#define CF_TYPE_REGISTRY_V0_FILENAME "type_registry.v0.json"
#endif

std::filesystem::path executable_path() {
#if defined(_WIN32)
  char buffer[MAX_PATH];
  DWORD length = GetModuleFileNameA(nullptr, buffer, MAX_PATH);
  if (length == 0 || length == MAX_PATH) {
    return {};
  }
  return std::filesystem::path(std::string(buffer, length));
#elif defined(__APPLE__)
  uint32_t size = 0;
  _NSGetExecutablePath(nullptr, &size);
  if (size == 0) {
    return {};
  }
  std::string buffer(size, '\0');
  if (_NSGetExecutablePath(buffer.data(), &size) != 0) {
    return {};
  }
  return std::filesystem::weakly_canonical(std::filesystem::path(buffer.c_str()));
#else
  char buffer[PATH_MAX];
  ssize_t length = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1);
  if (length <= 0) {
    return {};
  }
  buffer[length] = '\0';
  return std::filesystem::path(buffer);
#endif
}

std::vector<std::string> registry_candidates() {
  std::vector<std::string> candidates;

  if (const char* env_path = std::getenv("CF_TYPE_REGISTRY_V0_PATH")) {
    if (*env_path) {
      candidates.emplace_back(env_path);
    }
  }

  const std::filesystem::path exe_path = executable_path();
  if (!exe_path.empty()) {
    candidates.push_back((exe_path.parent_path() / CF_TYPE_REGISTRY_V0_FILENAME).string());
  }

  const std::string compiled_path = CF_TYPE_REGISTRY_V0_PATH;
  if (!compiled_path.empty()) {
    candidates.push_back(compiled_path);
  }

  return candidates;
}

bool load_json_file(const std::string& path, json& out, std::string& error) {
  std::ifstream fh(path);
  if (!fh) {
    error = "Failed to open type registry: " + path;
    return false;
  }
  try {
    fh >> out;
  } catch (const std::exception& ex) {
    error = std::string("Failed to parse type registry: ") + ex.what();
    return false;
  }
  return true;
}

bool storage_kind_from_name(const std::string& value, CfStorageType& out, std::string& error) {
  if (value == "CF_STORAGE_BOOL") {
    out = CF_STORAGE_BOOL;
    return true;
  }
  if (value == "CF_STORAGE_I64") {
    out = CF_STORAGE_I64;
    return true;
  }
  if (value == "CF_STORAGE_F64") {
    out = CF_STORAGE_F64;
    return true;
  }
  if (value == "CF_STORAGE_STRING") {
    out = CF_STORAGE_STRING;
    return true;
  }
  if (value == "CF_STORAGE_JSON") {
    out = CF_STORAGE_JSON;
    return true;
  }
  if (value == "CF_STORAGE_VECTOR_F64") {
    out = CF_STORAGE_VECTOR_F64;
    return true;
  }
  error = "Unsupported type registry storage kind: " + value;
  return false;
}

bool add_aliases(const json& aliases,
                 size_t index,
                 std::unordered_map<std::string, size_t>& destination,
                 const std::string& label,
                 std::string& error) {
  if (!aliases.is_array()) {
    error = "Type registry " + label + " aliases must be an array";
    return false;
  }
  for (const auto& alias_node : aliases) {
    if (!alias_node.is_string()) {
      error = "Type registry " + label + " alias must be a string";
      return false;
    }
    const std::string alias = alias_node.get<std::string>();
    auto inserted = destination.emplace(alias, index);
    if (!inserted.second) {
      error = "Duplicate type registry alias: " + alias;
      return false;
    }
  }
  return true;
}

}  // namespace

bool TypeRegistry::resolve_from_index(size_t index, ResolvedDatatype& out) const {
  if (index >= entries_.size()) {
    return false;
  }
  const Entry& entry = entries_[index];
  out.id = entry.id;
  out.canonical_port_type_iri = entry.canonical_port_type_iri;
  out.canonical_parameter_type_iri = entry.canonical_parameter_type_iri;
  out.storage_type = entry.storage_type;
  return true;
}

bool TypeRegistry::resolve_port_type(const std::string& datatype, ResolvedDatatype& out) const {
  auto it = port_alias_to_index_.find(datatype);
  if (it == port_alias_to_index_.end()) {
    return false;
  }
  return resolve_from_index(it->second, out);
}

bool TypeRegistry::resolve_parameter_type(const std::string& datatype, ResolvedDatatype& out) const {
  auto it = parameter_alias_to_index_.find(datatype);
  if (it == parameter_alias_to_index_.end()) {
    return false;
  }
  return resolve_from_index(it->second, out);
}

bool TypeRegistry::are_port_types_compatible(const std::string& output_datatype, const std::string& input_datatype) const {
  ResolvedDatatype output;
  ResolvedDatatype input;
  if (!resolve_port_type(output_datatype, output) || !resolve_port_type(input_datatype, input)) {
    return false;
  }

  auto entry_it = id_to_index_.find(output.id);
  if (entry_it == id_to_index_.end()) {
    return false;
  }
  const Entry& entry = entries_[entry_it->second];
  for (const std::string& compatible_input_id : entry.compatible_input_ids) {
    if (compatible_input_id == input.id) {
      return true;
    }
  }
  return false;
}

bool load_default_type_registry(TypeRegistry& out, std::string& error) {
  out = TypeRegistry{};

  json doc;
  std::string last_error;
  for (const std::string& candidate : registry_candidates()) {
    if (candidate.empty()) {
      continue;
    }
    std::string attempt_error;
    if (load_json_file(candidate, doc, attempt_error)) {
      error.clear();
      break;
    }
    last_error = attempt_error;
  }
  if (doc.is_null()) {
    error = last_error.empty() ? "Type registry path is not configured" : last_error;
    return false;
  }
  if (!doc.contains("datatypes") || !doc["datatypes"].is_array()) {
    error = "Type registry missing datatypes array";
    return false;
  }

  for (const auto& node : doc["datatypes"]) {
    if (!node.is_object()) {
      error = "Type registry datatypes entry must be an object";
      return false;
    }

    TypeRegistry::Entry entry;
    if (!node.contains("id") || !node["id"].is_string()) {
      error = "Type registry datatype entry missing string id";
      return false;
    }
    entry.id = node["id"].get<std::string>();

    auto id_inserted = out.id_to_index_.emplace(entry.id, out.entries_.size());
    if (!id_inserted.second) {
      error = "Duplicate type registry datatype id: " + entry.id;
      return false;
    }

    if (!node.contains("storage_kind") || !node["storage_kind"].is_string()) {
      error = "Type registry datatype '" + entry.id + "' missing storage_kind";
      return false;
    }
    if (!storage_kind_from_name(node["storage_kind"].get<std::string>(), entry.storage_type, error)) {
      return false;
    }

    if (!node.contains("port") || !node["port"].is_object()) {
      error = "Type registry datatype '" + entry.id + "' missing port section";
      return false;
    }
    const auto& port = node["port"];
    if (!port.contains("canonical_iri") || !port["canonical_iri"].is_string()) {
      error = "Type registry datatype '" + entry.id + "' missing port canonical_iri";
      return false;
    }
    entry.canonical_port_type_iri = port["canonical_iri"].get<std::string>();

    if (!port.contains("aliases")) {
      error = "Type registry datatype '" + entry.id + "' missing port aliases";
      return false;
    }

    if (node.contains("parameter") && !node["parameter"].is_null()) {
      if (!node["parameter"].is_object()) {
        error = "Type registry datatype '" + entry.id + "' parameter section must be an object or null";
        return false;
      }
      const auto& parameter = node["parameter"];
      if (!parameter.contains("canonical_iri") || !parameter["canonical_iri"].is_string()) {
        error = "Type registry datatype '" + entry.id + "' missing parameter canonical_iri";
        return false;
      }
      entry.canonical_parameter_type_iri = parameter["canonical_iri"].get<std::string>();
      if (!parameter.contains("aliases")) {
        error = "Type registry datatype '" + entry.id + "' missing parameter aliases";
        return false;
      }
      if (!add_aliases(parameter["aliases"], out.entries_.size(), out.parameter_alias_to_index_, "parameter", error)) {
        return false;
      }
    }

    if (!add_aliases(port["aliases"], out.entries_.size(), out.port_alias_to_index_, "port", error)) {
      return false;
    }

    if (!node.contains("compatible_output_to_input_ids") || !node["compatible_output_to_input_ids"].is_array()) {
      error = "Type registry datatype '" + entry.id + "' missing compatible_output_to_input_ids";
      return false;
    }
    for (const auto& compatible_node : node["compatible_output_to_input_ids"]) {
      if (!compatible_node.is_string()) {
        error = "Type registry datatype '" + entry.id + "' compatibility ids must be strings";
        return false;
      }
      entry.compatible_input_ids.push_back(compatible_node.get<std::string>());
    }

    out.entries_.push_back(std::move(entry));
  }

  for (const auto& entry : out.entries_) {
    for (const std::string& compatible_input_id : entry.compatible_input_ids) {
      if (out.id_to_index_.find(compatible_input_id) == out.id_to_index_.end()) {
        error = "Type registry datatype '" + entry.id + "' references unknown compatibility id '" + compatible_input_id + "'";
        return false;
      }
    }
  }

  return true;
}

}  // namespace cf

