#include "torlinkc/sources/x1337.hpp"

#include <algorithm>
#include <array>
#include <atomic>
#include <cctype>
#include <ctime>
#include <map>
#include <regex>
#include <sstream>
#include <unordered_set>

#include "torlinkc/sources/rss.hpp"
#include "torlinkc/util/format.hpp"
#include "torlinkc/util/net.hpp"
#include "torlinkc/util/url_encode.hpp"

namespace torlinkc {

namespace {

constexpr std::array<const char*, 4> kHosts = {"1337x.to", "1337x.st", "x1337x.ws", "1337xx.to"};
// Shared mutable state across the two 1337x sources (movies + TV), which the
// Phase 3 SearchAggregator queries concurrently on separate threads -- an
// atomic (vs. the TS original's plain module-level `let`) is what makes that
// safe here.
std::atomic<int> gWorkingHostIndex{0};

constexpr int kMaxDetails = 4;

const std::unordered_set<std::string> kStopWords = {"the", "a", "an", "of", "and", "or", "to"};

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;
}

std::vector<std::string> splitWhitespace(const std::string& s) {
  std::istringstream iss(s);
  std::vector<std::string> out;
  std::string tok;
  while (iss >> tok) out.push_back(tok);
  return out;
}

struct Row {
  std::string name;
  std::string path;
  int seeders = 0;
  int leechers = 0;
  std::int64_t sizeBytes = 0;
};

// Splits like JS's String.split(/<tr[\s>]/i): n matches produce n+1 segments,
// segment[0] being whatever preceded the first match.
std::vector<std::string> splitOnTrTag(const std::string& html) {
  static const std::regex kTrTag(R"(<tr[\s>])", std::regex::icase);
  std::vector<std::string> segments;
  auto begin = std::sregex_iterator(html.begin(), html.end(), kTrTag);
  auto end = std::sregex_iterator();
  std::size_t lastEnd = 0;
  for (auto it = begin; it != end; ++it) {
    segments.push_back(html.substr(lastEnd, static_cast<std::size_t>(it->position()) - lastEnd));
    lastEnd = static_cast<std::size_t>(it->position() + it->length());
  }
  segments.push_back(html.substr(lastEnd));
  return segments;
}

std::vector<Row> parseRows(const std::string& html) {
  auto start = html.find("table-list");
  if (start == std::string::npos) return {};

  static const std::regex kLink(R"re(href="(/torrent/[^"]+)"[^>]*>([^<]+)</a>)re", std::regex::icase);
  static const std::regex kSize(R"(class="coll-4 size[^"]*">\s*([\d.]+\s*[KMGT]i?B))", std::regex::icase);
  static const std::regex kSeeds(R"(class="coll-2 seeds[^"]*">\s*(\d+))", std::regex::icase);
  static const std::regex kLeeches(R"(class="coll-3 leeches[^"]*">\s*(\d+))", std::regex::icase);

  std::vector<Row> out;
  auto segments = splitOnTrTag(html.substr(start));
  for (std::size_t i = 1; i < segments.size(); ++i) {
    const std::string& tr = segments[i];
    std::smatch link;
    if (!std::regex_search(tr, link, kLink)) continue;

    Row row;
    row.path = link[1].str();
    row.name = unescapeEntities(trim(link[2].str()));
    std::smatch m;
    row.sizeBytes = std::regex_search(tr, m, kSize) ? parseSize(m[1].str()) : 0;
    row.seeders = std::regex_search(tr, m, kSeeds) ? std::stoi(m[1].str()) : 0;
    row.leechers = std::regex_search(tr, m, kLeeches) ? std::stoi(m[1].str()) : 0;
    out.push_back(std::move(row));
  }
  return out;
}

const std::map<std::string, int>& monthTable() {
  static const std::map<std::string, int> months = {{"jan", 0}, {"feb", 1}, {"mar", 2}, {"apr", 3}, {"may", 4},
                                                      {"jun", 5}, {"jul", 6}, {"aug", 7}, {"sep", 8}, {"oct", 9},
                                                      {"nov", 10}, {"dec", 11}};
  return months;
}

struct DetailInfo {
  std::string magnet;
  std::optional<std::int64_t> added;
};

std::optional<DetailInfo> fetchDetail(const std::string& base, const std::string& path,
                                       const std::stop_token& stopToken) {
  try {
    FetchOptions opts;
    opts.retries = 1;
    opts.stopToken = stopToken;
    HttpResponse res = fetchResilient(base + path, opts);
    if (!res.ok()) return std::nullopt;

    static const std::regex kMagnetRe(R"(magnet:\?xt=urn:btih:[^"'<>\s]+)", std::regex::icase);
    std::smatch m;
    if (!std::regex_search(res.body, m, kMagnetRe)) return std::nullopt;

    DetailInfo info;
    info.magnet = unescapeEntities(m[0].str());
    info.added = parseUploadDate(res.body);
    return info;
  } catch (const std::exception&) {
    return std::nullopt;
  }
}

std::vector<TorrentResult> search(const std::string& query, const std::string& category, const std::string& source,
                                   const std::stop_token& stopToken) {
  const std::string q = trim(query);
  std::string path;
  if (!q.empty()) {
    std::string encoded = encodeURIComponent(q);
    std::string withPlus;
    withPlus.reserve(encoded.size());
    for (std::size_t i = 0; i < encoded.size();) {
      if (encoded.compare(i, 3, "%20") == 0) {
        withPlus += '+';
        i += 3;
      } else {
        withPlus += encoded[i];
        i += 1;
      }
    }
    path = "/category-search/" + withPlus + "/" + category + "/1/";
  } else {
    path = category == "Movies" ? "/popular-movies" : "/popular-tv";
  }

  std::string base, html;
  std::exception_ptr lastError;
  const int startIdx = gWorkingHostIndex.load();
  for (std::size_t i = 0; i < kHosts.size(); ++i) {
    const std::size_t hostIdx = (static_cast<std::size_t>(startIdx) + i) % kHosts.size();
    const std::string candidate = std::string("https://") + kHosts[hostIdx];
    try {
      FetchOptions opts;
      opts.retries = i == 0 ? 2 : 0;
      opts.stopToken = stopToken;
      HttpResponse res = fetchResilient(candidate + path, opts);
      if (!res.ok()) throw HttpError(res.status, "1337x returned " + std::to_string(res.status));
      html = res.body;
      base = candidate;
      gWorkingHostIndex.store(static_cast<int>(hostIdx));
      break;
    } catch (const std::exception&) {
      lastError = std::current_exception();
      if (stopToken.stop_requested()) std::rethrow_exception(lastError);
    }
  }
  if (base.empty()) {
    if (lastError) std::rethrow_exception(lastError);
    throw HttpError(0, "1337x unreachable");
  }

  const auto all = parseRows(html);
  const auto tokens = splitWhitespace(toLower(q));
  std::vector<std::string> meaningful;
  for (const auto& t : tokens)
    if (!kStopWords.count(t)) meaningful.push_back(t);
  const std::vector<std::string>& need = meaningful.empty() ? tokens : meaningful;

  std::vector<Row> matched;
  if (!need.empty()) {
    for (const auto& r : all) {
      const std::string name = toLower(r.name);
      bool matchesAll = true;
      for (const auto& t : need) {
        if (name.find(t) == std::string::npos) {
          matchesAll = false;
          break;
        }
      }
      if (matchesAll) matched.push_back(r);
    }
  } else {
    matched = all;
  }
  std::stable_sort(matched.begin(), matched.end(), [](const Row& a, const Row& b) { return a.seeders > b.seeders; });
  if (matched.size() > static_cast<std::size_t>(kMaxDetails)) matched.resize(kMaxDetails);

  static const std::regex kInfoHashRe(R"(urn:btih:([a-zA-Z0-9]+))", std::regex::icase);
  std::vector<TorrentResult> out;
  for (const auto& row : matched) {
    auto detail = fetchDetail(base, row.path, stopToken);
    if (!detail) continue;
    std::smatch m;
    if (!std::regex_search(detail->magnet, m, kInfoHashRe)) continue;

    TorrentResult r;
    r.infoHash = toLower(m[1].str());
    r.name = row.name;
    r.sizeBytes = row.sizeBytes;
    r.seeders = row.seeders;
    r.leechers = row.leechers;
    r.source = source;
    r.magnet = detail->magnet;
    r.added = detail->added;
    out.push_back(std::move(r));
  }
  return out;
}

}  // namespace

std::optional<std::int64_t> parseUploadDate(const std::string& html) {
  static const std::regex kRe(R"(Date uploaded</strong>\s*<span>\s*([A-Za-z]{3})\.?\s+(\d{1,2})[a-z]{2}\s*'(\d{2}))",
                               std::regex::icase);
  std::smatch m;
  if (!std::regex_search(html, m, kRe)) return std::nullopt;

  const auto it = monthTable().find(toLower(m[1].str()));
  if (it == monthTable().end()) return std::nullopt;

  std::tm tm{};
  tm.tm_year = 2000 + std::stoi(m[3].str()) - 1900;
  tm.tm_mon = it->second;
  tm.tm_mday = std::stoi(m[2].str());
  const time_t secs = timegm(&tm);
  return secs == static_cast<time_t>(-1) ? std::nullopt : std::optional<std::int64_t>(secs);
}

Source x1337MoviesSource() {
  Source s;
  s.id = "x1337-movies";
  s.label = "1337x";
  s.groups = {SourceGroup::Movies};
  s.homepage = "https://1337x.to";
  s.reportsHealth = true;
  s.search = [](const std::string& query, const SearchOptions& opts) {
    return search(query, "Movies", "x1337-movies", opts.stopToken);
  };
  return s;
}

Source x1337TvSource() {
  Source s;
  s.id = "x1337-tv";
  s.label = "1337x";
  s.groups = {SourceGroup::TV};
  s.homepage = "https://1337x.to";
  s.reportsHealth = true;
  s.search = [](const std::string& query, const SearchOptions& opts) {
    return search(query, "TV", "x1337-tv", opts.stopToken);
  };
  return s;
}

}  // namespace torlinkc
