#include "torlinkc/sources/magnet.hpp"

#include <algorithm>
#include <array>
#include <cctype>
#include <cstring>
#include <regex>
#include <unordered_set>

#include "torlinkc/util/url_encode.hpp"

namespace torlinkc {

namespace {

// CTAD deduces both element type and size from the initializer list, so a
// miscounted array-size template argument (previously 12 for an 11-entry
// list, leaving a null-pointer 12th element that crashed on first use) can't
// happen again.
constexpr std::array kTrackers = {
    "udp://tracker.opentrackr.org:1337/announce",
    "udp://open.demonii.com:1337/announce",
    "udp://tracker.openbittorrent.com:6969/announce",
    "udp://tracker.torrent.eu.org:451/announce",
    "udp://exodus.desync.com:6969/announce",
    "udp://open.stealth.si:80/announce",
    "udp://tracker.dler.org:6969/announce",
    // HTTP(S) endpoints so peer discovery still works where UDP is blocked or
    // mangled (VPN exit nodes, strict NATs): DHT and udp:// are both UDP.
    "http://tracker.opentrackr.org:1337/announce",
    "http://tracker.openbittorrent.com:80/announce",
    "http://tracker.dler.org:6969/announce",
    "https://tracker.tamersunion.org:443/announce",
};

std::string trim(const std::string& s) {
  auto first = s.find_first_not_of(" \t\r\n");
  if (first == std::string::npos) return "";
  auto last = s.find_last_not_of(" \t\r\n");
  return s.substr(first, last - first + 1);
}

std::string toLower(std::string s) {
  std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); });
  return s;
}

const char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

std::optional<std::string> base32ToHex(const std::string& b32) {
  int bits = 0;
  unsigned long value = 0;
  std::string out;
  for (unsigned char raw : b32) {
    char c = static_cast<char>(std::toupper(raw));
    const char* pos = std::strchr(kBase32Alphabet, c);
    if (!pos || c == '\0') return std::nullopt;
    int idx = static_cast<int>(pos - kBase32Alphabet);
    value = (value << 5) | static_cast<unsigned long>(idx);
    bits += 5;
    if (bits >= 8) {
      bits -= 8;
      unsigned int byte = (value >> bits) & 0xff;
      static const char* hex = "0123456789abcdef";
      out += hex[byte >> 4];
      out += hex[byte & 0xF];
      value &= (1u << bits) - 1;
    }
  }
  return out.size() == 40 ? std::optional<std::string>(out) : std::nullopt;
}

// Extracts the `dn` query parameter the way `new URL(s).searchParams.get("dn")`
// would, without pulling in a full URL parser: split the query string on '&',
// find the first `dn=` pair, decode it.
std::optional<std::string> extractDn(const std::string& magnet) {
  auto q = magnet.find('?');
  if (q == std::string::npos) return std::nullopt;
  std::string query = magnet.substr(q + 1);
  std::size_t pos = 0;
  while (pos <= query.size()) {
    auto amp = query.find('&', pos);
    std::string pair = query.substr(pos, amp == std::string::npos ? std::string::npos : amp - pos);
    auto eq = pair.find('=');
    if (eq != std::string::npos && pair.substr(0, eq) == "dn") {
      return decodeURIComponent(pair.substr(eq + 1));
    }
    if (amp == std::string::npos) break;
    pos = amp + 1;
  }
  return std::nullopt;
}

}  // namespace

std::string buildMagnet(const std::string& infoHash, const std::string& name,
                         const std::vector<std::string>& extraTrackers) {
  const std::string dn = encodeURIComponent(name);

  std::unordered_set<std::string> seen;
  std::vector<std::string> trackers;
  auto addTracker = [&](const std::string& raw) {
    const std::string url = trim(raw);
    if (url.empty() || seen.count(url)) return;
    seen.insert(url);
    trackers.push_back(url);
  };
  for (const auto& t : extraTrackers) addTracker(t);
  for (const char* t : kTrackers) addTracker(t);

  std::string tr;
  for (const auto& t : trackers) tr += "&tr=" + encodeURIComponent(t);

  return "magnet:?xt=urn:btih:" + infoHash + "&dn=" + dn + tr;
}

std::string normalizeInfoHash(const std::string& raw) {
  if (raw.size() == 32) {
    if (auto hex = base32ToHex(raw)) return *hex;
    return toLower(raw);
  }
  return toLower(raw);
}

std::optional<ParsedMagnet> parseMagnet(const std::string& input) {
  const std::string s = trim(input);
  static const std::regex kMagnetPrefix(R"(^magnet:\?)", std::regex::icase);
  if (!std::regex_search(s, kMagnetPrefix)) return std::nullopt;

  static const std::regex kMagnetRe(R"(xt=urn:btih:([a-f0-9]{40}|[a-z2-7]{32}))", std::regex::icase);
  std::smatch m;
  if (!std::regex_search(s, m, kMagnetRe)) return std::nullopt;

  const std::string infoHash = normalizeInfoHash(m[1].str());
  std::string name = infoHash;
  if (auto dn = extractDn(s); dn && !dn->empty()) name = *dn;

  return ParsedMagnet{infoHash, name, s};
}

bool isInfoHash(const std::string& input) {
  const std::string s = trim(input);
  static const std::regex kInfoHashRe(R"(^([a-f0-9]{40}|[a-z2-7]{32})$)", std::regex::icase);
  return std::regex_match(s, kInfoHashRe);
}

std::optional<ParsedMagnet> parseInput(const std::string& input) {
  const std::string s = trim(input);
  if (auto magnet = parseMagnet(s)) return magnet;
  if (!isInfoHash(s)) return std::nullopt;
  const std::string infoHash = normalizeInfoHash(s);
  return ParsedMagnet{infoHash, infoHash, buildMagnet(infoHash, infoHash)};
}

}  // namespace torlinkc
