#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <set>
#include <string>
#include <string_view>
#include <vector>

#ifndef APTT_SOURCE_DIR
#error "APTT_SOURCE_DIR must be defined for source package hygiene tests"
#endif

#ifndef APTT_DEFAULT_SOURCE_REVIEW_PACKAGE_DIR
#error "APTT_DEFAULT_SOURCE_REVIEW_PACKAGE_DIR must be defined for source package hygiene tests"
#endif

namespace {

void Expect(bool condition, std::string_view message) {
  if (!condition) {
    std::cerr << "FAILED: " << message << "\n";
    std::exit(1);
  }
}

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

bool IsForbiddenSourcePackageExtension(const std::filesystem::path& path) {
  const auto extension = Lowercase(path.extension().string());
  return extension == ".ttf" || extension == ".otf" ||
         extension == ".woff" || extension == ".woff2" ||
         extension == ".lib" || extension == ".dll" ||
         extension == ".exe";
}

std::set<std::string> AllowedDearImguiFiles() {
  return {
      "LICENSE.txt",
      "imconfig.h",
      "imgui.cpp",
      "imgui.h",
      "imgui_draw.cpp",
      "imgui_internal.h",
      "imgui_tables.cpp",
      "imgui_widgets.cpp",
      "imstb_rectpack.h",
      "imstb_textedit.h",
      "imstb_truetype.h",
      "backends/imgui_impl_dx11.cpp",
      "backends/imgui_impl_dx11.h",
      "backends/imgui_impl_win32.cpp",
      "backends/imgui_impl_win32.h",
  };
}

std::string PortableRelativePath(const std::filesystem::path& path,
                                 const std::filesystem::path& root) {
  return std::filesystem::relative(path, root).generic_string();
}

void CheckForbiddenExtensions(const std::filesystem::path& root,
                              std::string_view label) {
  Expect(std::filesystem::exists(root), "source package root exists");
  std::vector<std::filesystem::path> forbidden_files;
  const auto options =
      std::filesystem::directory_options::skip_permission_denied;
  for (const auto& entry :
       std::filesystem::recursive_directory_iterator(root, options)) {
    if (!entry.is_regular_file()) {
      continue;
    }
    if (IsForbiddenSourcePackageExtension(entry.path())) {
      forbidden_files.push_back(std::filesystem::relative(entry.path(), root));
    }
  }

  if (!forbidden_files.empty()) {
    std::cerr << "Forbidden binary/font files under " << label << ":\n";
    for (const auto& path : forbidden_files) {
      std::cerr << "  " << path.string() << "\n";
    }
  }
  Expect(forbidden_files.empty(),
         "source package contains no binary or font files");
}

void CheckDearImguiAllowlist(const std::filesystem::path& package_root) {
  const auto imgui_root = package_root / "third_party" / "imgui";
  Expect(std::filesystem::exists(imgui_root),
         "source review package contains third_party/imgui");

  const auto allowed = AllowedDearImguiFiles();
  std::set<std::string> present;
  std::vector<std::string> unexpected;
  const auto options =
      std::filesystem::directory_options::skip_permission_denied;
  for (const auto& entry :
       std::filesystem::recursive_directory_iterator(imgui_root, options)) {
    if (!entry.is_regular_file()) {
      continue;
    }
    const auto relative = PortableRelativePath(entry.path(), imgui_root);
    present.insert(relative);
    if (!allowed.contains(relative)) {
      unexpected.push_back(relative);
    }
  }

  if (!unexpected.empty()) {
    std::cerr << "Unexpected Dear ImGui files in source review package:\n";
    for (const auto& path : unexpected) {
      std::cerr << "  " << path << "\n";
    }
  }
  Expect(unexpected.empty(),
         "source review package Dear ImGui files match the allowlist");

  std::vector<std::string> missing;
  for (const auto& path : allowed) {
    if (!present.contains(path)) {
      missing.push_back(path);
    }
  }
  if (!missing.empty()) {
    std::cerr << "Missing Dear ImGui allowlist files:\n";
    for (const auto& path : missing) {
      std::cerr << "  " << path << "\n";
    }
  }
  Expect(missing.empty(),
         "source review package contains all required Dear ImGui files");
}

std::string ReadTextFile(const std::filesystem::path& path) {
  std::ifstream file(path, std::ios::binary);
  return std::string((std::istreambuf_iterator<char>(file)),
                     std::istreambuf_iterator<char>());
}

std::string TrimAsciiWhitespace(std::string value) {
  while (!value.empty() &&
         std::isspace(static_cast<unsigned char>(value.front()))) {
    value.erase(value.begin());
  }
  while (!value.empty() &&
         std::isspace(static_cast<unsigned char>(value.back()))) {
    value.pop_back();
  }
  return value;
}

std::string ExtractManifestValueAfter(std::string_view text,
                                      std::string_view anchor,
                                      std::string_view prefix) {
  const auto anchor_pos = text.find(anchor);
  Expect(anchor_pos != std::string_view::npos,
         "source review manifest contains expected section");
  const auto prefix_pos = text.find(prefix, anchor_pos);
  Expect(prefix_pos != std::string_view::npos,
         "source review manifest contains expected field");
  const auto value_start = prefix_pos + prefix.size();
  const auto value_end = text.find_first_of("\r\n", value_start);
  return TrimAsciiWhitespace(std::string(text.substr(
      value_start,
      value_end == std::string_view::npos
          ? std::string_view::npos
          : value_end - value_start)));
}

void CheckKernelStreamingCancellationSafety(
    const std::filesystem::path& package_root) {
  const auto source =
      ReadTextFile(package_root /
                   "src/backends/kernel_streaming_pcm_backend.cpp");
  Expect(source.find("native_handle()") == std::string::npos,
         "source package does not contain native_handle cancellation cast");
  Expect(source.find("DuplicateHandle") != std::string::npos,
         "source package contains duplicated Win32 thread-handle cleanup");
}

void CheckAsioSourceAvailabilityGate(const std::filesystem::path& source_dir) {
  const auto cmake_lists = ReadTextFile(source_dir / "CMakeLists.txt");
  const auto verify_script = ReadTextFile(
      source_dir / "cmake" / "verify_asio_source_availability.cmake");
  Expect(cmake_lists.find("APTT_ASIO_CORRESPONDING_SOURCE_PACKAGE_RESOLVED") !=
             std::string::npos,
         "CMake resolves the exact ASIO corresponding source package");
  Expect(cmake_lists.find("file(SHA256") != std::string::npos,
         "CMake configure gate checks the ASIO corresponding source SHA-256");
  Expect(verify_script.find("file(SHA256") != std::string::npos,
         "package target verifies the ASIO corresponding source SHA-256");
  Expect(verify_script.find("SOURCE_AVAILABILITY.txt") != std::string::npos,
         "package target verifies SOURCE_AVAILABILITY.txt");
}

void CheckPackagedAsioSourceAvailability(
    const std::filesystem::path& source_dir) {
  const auto manifest_path =
      source_dir / "external_review_source" / "CURRENT_REVIEW_PACKAGES.txt";
  if (!std::filesystem::exists(manifest_path)) {
    return;
  }

  const auto manifest = ReadTextFile(manifest_path);
  const auto asio_zip = ExtractManifestValueAfter(
      manifest,
      "GPLv3 ASIO-enabled corresponding source:",
      "Zip:");
  const auto asio_sha = ExtractManifestValueAfter(
      manifest,
      "GPLv3 ASIO-enabled corresponding source:",
      "SHA-256:");
  const auto asio_zip_name =
      std::filesystem::path(asio_zip).filename().string();

  const auto dist_root = source_dir / "dist";
  if (!std::filesystem::exists(dist_root)) {
    return;
  }

  bool checked_asio_package = false;
  const auto options =
      std::filesystem::directory_options::skip_permission_denied;
  for (const auto& entry :
       std::filesystem::recursive_directory_iterator(dist_root, options)) {
    if (!entry.is_regular_file() ||
        entry.path().filename() != "SOURCE_AVAILABILITY.txt") {
      continue;
    }

    const auto notice = ReadTextFile(entry.path());
    if (notice.find("ASIO SDK integration: enabled") == std::string::npos) {
      continue;
    }

    checked_asio_package = true;
    Expect(Lowercase(notice).find(Lowercase(asio_sha)) != std::string::npos,
           "packaged ASIO SOURCE_AVAILABILITY references current source SHA");
    Expect(notice.find(asio_zip_name) != std::string::npos,
           "packaged ASIO SOURCE_AVAILABILITY references current source zip");
  }

  Expect(checked_asio_package,
         "ASIO-enabled package SOURCE_AVAILABILITY was checked");
}

std::filesystem::path SourceReviewPackageRoot(
    const std::filesystem::path& source_dir) {
  const auto self_review_readme = source_dir / "SOURCE_REVIEW_README.md";
  if (std::filesystem::exists(self_review_readme)) {
    return source_dir;
  }
  return std::filesystem::path(APTT_DEFAULT_SOURCE_REVIEW_PACKAGE_DIR);
}

}  // namespace

int main() {
  const auto source_dir = std::filesystem::path(APTT_SOURCE_DIR);
  const auto third_party_root =
      source_dir / "third_party";
  Expect(std::filesystem::exists(third_party_root),
         "third_party directory exists");

  CheckForbiddenExtensions(third_party_root, "third_party");
  CheckDearImguiAllowlist(source_dir);
  CheckKernelStreamingCancellationSafety(source_dir);

  const auto review_package_root = SourceReviewPackageRoot(source_dir);
  if (std::filesystem::exists(review_package_root)) {
    CheckForbiddenExtensions(review_package_root,
                             "default source review package");
    CheckDearImguiAllowlist(review_package_root);
    CheckKernelStreamingCancellationSafety(review_package_root);
  } else {
    std::cout << "source review package not present; checked public source "
                 "tree hygiene instead\n";
  }
  CheckAsioSourceAvailabilityGate(source_dir);
  CheckPackagedAsioSourceAvailability(source_dir);

  std::cout << "source package hygiene tests passed\n";
  return 0;
}
