#include "plugin_loader/plugin_loader.h"

#include <algorithm>
#include <cctype>
#include <filesystem>
#include <unordered_set>

#if defined(_WIN32)
#include <windows.h>
#else
#include <dlfcn.h>
#endif

namespace cf {

namespace {

std::string to_lower_copy(std::string value) {
  std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
  return value;
}

bool has_library_extension(const std::filesystem::path& path) {
#if defined(_WIN32)
  return path.extension() == ".dll";
#elif defined(__APPLE__)
  return path.extension() == ".dylib";
#else
  return path.extension() == ".so";
#endif
}

std::vector<std::filesystem::path> collect_library_paths(const std::string& directory, bool recursive, std::string& error) {
  std::filesystem::path dir(directory);
  if (!std::filesystem::exists(dir)) {
    error = "Plugin directory not found: " + directory;
    return {};
  }
  std::vector<std::filesystem::path> paths;
  if (recursive) {
    for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) {
      if (!entry.is_regular_file()) continue;
      if (!has_library_extension(entry.path())) continue;
      paths.push_back(entry.path());
    }
  } else {
    for (const auto& entry : std::filesystem::directory_iterator(dir)) {
      if (!entry.is_regular_file()) continue;
      if (!has_library_extension(entry.path())) continue;
      paths.push_back(entry.path());
    }
  }
  std::sort(paths.begin(), paths.end());
  return paths;
}

#if defined(_WIN32)
std::vector<std::filesystem::path> filter_lib_prefixed_duplicates(std::vector<std::filesystem::path> paths) {
  std::unordered_set<std::string> filenames;
  filenames.reserve(paths.size());
  for (const auto& path : paths) {
    filenames.insert(to_lower_copy(path.filename().string()));
  }

  std::vector<std::filesystem::path> filtered;
  filtered.reserve(paths.size());
  for (auto& path : paths) {
    std::string name = to_lower_copy(path.filename().string());
    if (name.rfind("lib", 0) == 0 && name.size() > 3) {
      std::string alt = name.substr(3);
      if (filenames.find(alt) != filenames.end()) {
        continue;
      }
    }
    filtered.push_back(std::move(path));
  }
  return filtered;
}
#endif

}  // namespace

bool PluginRegistry::load_from_directories(const std::vector<std::string>& directories, std::string& error, bool recursive) {
  plugins_.clear();
  vtables_.clear();
  step_to_plugin_.clear();

  std::vector<std::filesystem::path> libraries;
  for (const auto& dir : directories) {
    auto paths = collect_library_paths(dir, recursive, error);
    if (!error.empty()) {
      return false;
    }
    libraries.insert(libraries.end(), paths.begin(), paths.end());
  }
  std::sort(libraries.begin(), libraries.end());
#if defined(_WIN32)
  libraries = filter_lib_prefixed_duplicates(std::move(libraries));
#endif

  std::unordered_map<std::string, std::vector<std::string>> duplicates;

  for (const auto& lib_path : libraries) {
    PluginEntry plugin;
    plugin.path = lib_path.string();

#if defined(_WIN32)
    HMODULE handle = LoadLibraryA(plugin.path.c_str());
    if (!handle) {
      error = "Failed to load plugin: " + plugin.path;
      return false;
    }
    plugin.handle = handle;
    plugin.list_fn = reinterpret_cast<size_t (*)(const char***)>(GetProcAddress(handle, "cf_list_steps"));
    plugin.resolve_fn = reinterpret_cast<int (*)(const char*, CfStepVTable*)>(GetProcAddress(handle, "cf_resolve_step"));
#else
    void* handle = dlopen(plugin.path.c_str(), RTLD_NOW);
    if (!handle) {
      error = "Failed to load plugin: " + plugin.path;
      return false;
    }
    plugin.handle = handle;
    plugin.list_fn = reinterpret_cast<size_t (*)(const char***)>(dlsym(handle, "cf_list_steps"));
    plugin.resolve_fn = reinterpret_cast<int (*)(const char*, CfStepVTable*)>(dlsym(handle, "cf_resolve_step"));
#endif

    if (!plugin.list_fn || !plugin.resolve_fn) {
      // Allow support libraries (e.g., dependency DLLs) to coexist in plugin directories.
#if defined(_WIN32)
      FreeLibrary(handle);
#else
      dlclose(handle);
#endif
      continue;
    }

    const char** iris = nullptr;
    size_t count = plugin.list_fn(&iris);
    for (size_t i = 0; i < count; ++i) {
      if (iris[i]) {
        plugin.step_iris.emplace_back(iris[i]);
      }
    }

    size_t plugin_index = plugins_.size();
    for (const auto& iri : plugin.step_iris) {
      auto existing = step_to_plugin_.find(iri);
      if (existing != step_to_plugin_.end()) {
        duplicates[iri].push_back(plugins_[existing->second].path);
        duplicates[iri].push_back(plugin.path);
      } else {
        step_to_plugin_[iri] = plugin_index;
      }
    }

    plugins_.push_back(std::move(plugin));
  }

  if (!duplicates.empty()) {
    auto entry = *duplicates.begin();
    std::string message = "Duplicate step implementation detected:\n  step IRI: " + entry.first + "\n  libraries: ";
    for (size_t i = 0; i < entry.second.size(); ++i) {
      if (i > 0) message += ", ";
      message += entry.second[i];
    }
    error = message;
    return false;
  }

  return true;
}

bool PluginRegistry::load_from_directory(const std::string& directory, std::string& error, bool recursive) {
  return load_from_directories({directory}, error, recursive);
}

const CfStepVTable* PluginRegistry::resolve_step(const std::string& step_iri, std::string& error) {
  auto existing = vtables_.find(step_iri);
  if (existing != vtables_.end()) {
    return &existing->second;
  }

  auto plugin_it = step_to_plugin_.find(step_iri);
  if (plugin_it == step_to_plugin_.end()) {
    error = "No plugin provides step: " + step_iri;
    return nullptr;
  }

  PluginEntry& plugin = plugins_[plugin_it->second];
  CfStepVTable table{};
  if (plugin.resolve_fn(step_iri.c_str(), &table) != 0) {
    error = "Plugin failed to resolve step: " + step_iri;
    return nullptr;
  }
  vtables_[step_iri] = table;
  return &vtables_[step_iri];
}

size_t PluginRegistry::loaded_plugin_count() const {
  return plugins_.size();
}

size_t PluginRegistry::loaded_step_count() const {
  size_t count = 0;
  for (const auto& plugin : plugins_) {
    count += plugin.step_iris.size();
  }
  return count;
}

std::string PluginRegistry::plugin_path_for_step(const std::string& step_iri) const {
  auto it = step_to_plugin_.find(step_iri);
  if (it == step_to_plugin_.end()) {
    return {};
  }
  size_t index = it->second;
  if (index >= plugins_.size()) {
    return {};
  }
  return plugins_[index].path;
}

}  // namespace cf
