<?php
/**
 * TagX API (portable)
 * Location: /public_html/linkstorm/api/tagx.php
 *
 * POST JSON:
 * {
 *   "text": "text or html here",
 *   "mode": "hashtags" | "links",
 *   "max_tags": 20,
 *   "prefix_hash": true,
 *   "dedupe": true,
 *   "min_word_len": 3
 * }
 *
 * Optional auth header:
 *   X-TagX-Key: YOUR_KEY
 *
 * Configure expected key below (or leave blank to disable auth).
 */

declare(strict_types=1);
header("Content-Type: application/json; charset=utf-8");

// ---------------- CONFIG ----------------
const TAGX_EXPECTED_KEY = "Dart617694"; // <-- set to something like "S1L_TagX_2025!" to require auth, or leave "" to disable.

// CORS (adjust if needed)
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
header("Access-Control-Allow-Origin: " . $origin);
header("Vary: Origin");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, X-TagX-Key");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
// ----------------------------------------

function respond(bool $ok, array $data = [], int $code = 200): void {
  http_response_code($code);
  echo json_encode(array_merge(["ok" => $ok], $data), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  exit;
}

function require_auth(): void {
  if (TAGX_EXPECTED_KEY === "") return; // auth disabled
  $got = $_SERVER['HTTP_X_TAGX_KEY'] ?? "";
  if (!$got || !hash_equals(TAGX_EXPECTED_KEY, $got)) {
    respond(false, ["error" => "Unauthorized"], 401);
  }
}

function read_json_body(): array {
  $raw = file_get_contents("php://input");
  if ($raw === false || trim($raw) === "") return [];
  $data = json_decode($raw, true);
  if (!is_array($data)) respond(false, ["error" => "Invalid JSON body"], 400);
  return $data;
}

function strip_html_keep_text(string $s): string {
  // Decode entities then strip tags
  $s = html_entity_decode($s, ENT_QUOTES | ENT_HTML5, "UTF-8");
  $s = preg_replace('/<script\\b[^>]*>(.*?)<\\/script>/is', ' ', $s);
  $s = preg_replace('/<style\\b[^>]*>(.*?)<\\/style>/is', ' ', $s);
  $s = strip_tags($s);
  // normalize whitespace
  $s = preg_replace('/\\s+/u', ' ', $s);
  return trim($s);
}

function extract_links(string $s): array {
  $links = [];

  // Find URLs
  if (preg_match_all('~\\bhttps?://[^\\s<>"\\)\\]]+~i', $s, $m)) {
    foreach ($m[0] as $u) $links[] = rtrim($u, ".,;:!?)]}\"'");
  }

  // Find www. URLs (no scheme)
  if (preg_match_all('~\\bwww\\.[^\\s<>"\\)\\]]+~i', $s, $m2)) {
    foreach ($m2[0] as $u) $links[] = "https://" . rtrim($u, ".,;:!?)]}\"'");
  }

  // Dedup preserve order
  $seen = [];
  $out = [];
  foreach ($links as $u) {
    $k = strtolower($u);
    if (isset($seen[$k])) continue;
    $seen[$k] = true;
    $out[] = $u;
  }
  return $out;
}

function default_stopwords(): array {
  // Compact stopword list (add your own)
  return [
    "the","and","for","with","that","this","from","your","you","are","was","were","has","have","had",
    "not","but","all","any","can","will","just","more","most","into","about","over","under","than",
    "then","they","them","their","there","here","when","what","why","how","who","whom","which",
    "its","it's","our","ours","out","off","on","in","at","to","of","a","an","is","it","as","or",
    "be","by","we","i","me","my","mine","us"
  ];
}

function normalize_word(string $w): string {
  $w = mb_strtolower($w, "UTF-8");
  // Remove apostrophes & punctuation edges
  $w = preg_replace('/^[^\\p{L}\\p{N}]+|[^\\p{L}\\p{N}]+$/u', '', $w);
  // Remove internal punctuation except underscores
  $w = preg_replace('/[^\\p{L}\\p{N}_]/u', '', $w);
  return $w;
}

function extract_hashtags(string $text, int $maxTags = 20, bool $prefixHash = true, bool $rememberOrder = true, int $minLen = 3): array {
  $text = strip_html_keep_text($text);

  // Tokenize
  $tokens = preg_split('/\\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
  $stop = array_flip(default_stopwords());

  $counts = [];
  $firstPos = [];
  $pos = 0;

  foreach ($tokens as $t) {
    $pos++;
    // Skip URLs and @mentions quickly
    if (preg_match('~^https?://~i', $t) || preg_match('~^www\\.~i', $t) || str_starts_with($t, "@")) continue;

    $w = normalize_word($t);
    if ($w === "") continue;
    if (mb_strlen($w, "UTF-8") < $minLen) continue;
    if (isset($stop[$w])) continue;

    // avoid pure numbers
    if (preg_match('/^\\d+$/', $w)) continue;

    $counts[$w] = ($counts[$w] ?? 0) + 1;
    if (!isset($firstPos[$w])) $firstPos[$w] = $pos;
  }

  // Sort: either by first appearance (tagx vibe) or by count
  $words = array_keys($counts);
  usort($words, function($a, $b) use ($counts, $firstPos, $rememberOrder) {
    if ($rememberOrder) return ($firstPos[$a] <=> $firstPos[$b]);
    $c = $counts[$b] <=> $counts[$a];
    return $c !== 0 ? $c : ($firstPos[$a] <=> $firstPos[$b]);
  });

  $words = array_slice($words, 0, max(1, $maxTags));

  $tags = [];
  foreach ($words as $w) {
    $tags[] = $prefixHash ? ("#" . $w) : $w;
  }

  return $tags;
}

require_auth();
$body = read_json_body();

$text = strval($body["text"] ?? "");
$mode = strtolower(trim(strval($body["mode"] ?? "hashtags")));
$maxTags = intval($body["max_tags"] ?? 20);
$prefixHash = filter_var($body["prefix_hash"] ?? true, FILTER_VALIDATE_BOOLEAN);
$dedupe = filter_var($body["dedupe"] ?? true, FILTER_VALIDATE_BOOLEAN);
$minLen = intval($body["min_word_len"] ?? 3);

if ($text === "") respond(false, ["error" => "text is required"], 400);
if ($maxTags <= 0) $maxTags = 20;

if ($mode === "links") {
  $links = extract_links($text);
  respond(true, [
    "mode" => "links",
    "count" => count($links),
    "links" => $links
  ]);
}

// default: hashtags
$tags = extract_hashtags($text, $maxTags, $prefixHash, true, $minLen);

// Dedup (already mostly deduped, but keep option)
if ($dedupe) {
  $seen = [];
  $out = [];
  foreach ($tags as $t) {
    $k = mb_strtolower($t, "UTF-8");
    if (isset($seen[$k])) continue;
    $seen[$k] = true;
    $out[] = $t;
  }
  $tags = $out;
}

respond(true, [
  "mode" => "hashtags",
  "count" => count($tags),
  "tags" => $tags,
  "tags_line" => implode(" ", $tags)
]);
