/**
 * Threads 短詞規則（與後端 domain.IsThreadsSearchable 對齊）。
 * 一組查詢 ≤2 token；中文每詞 2–4 字；整組去掉空格後 ≤12 字；禁標點／boolean／emoji／#。
 */

const MAX_TOKENS = 2;
const MIN_CJK = 2;
const MAX_CJK = 4;
const MAX_TOTAL = 12;

export function normalizeSearchTerm(raw: string): string {
  return raw
    .replace(/\u3000/g, " ")
    .trim()
    .split(/\s+/)
    .filter(Boolean)
    .join(" ");
}

function isCJK(ch: string): boolean {
  const code = ch.codePointAt(0) ?? 0;
  return (
    (code >= 0x4e00 && code <= 0x9fff) ||
    (code >= 0x3400 && code <= 0x4dbf) ||
    (code >= 0x3040 && code <= 0x30ff)
  );
}

function isAllowedChar(ch: string): boolean {
  if (ch === "#" || ch === '"' || ch === "'" || ch === "「" || ch === "」") return false;
  if (/[-+*()|&!]/.test(ch)) return false;
  // letters / digits / CJK
  if (/[a-zA-Z0-9]/.test(ch)) return true;
  if (isCJK(ch)) return true;
  // reject punctuation / symbols / emoji
  if (/\p{P}|\p{S}/u.test(ch)) return false;
  return false;
}

function isAllowedToken(tok: string): boolean {
  if (!tok) return false;
  const upper = tok.toUpperCase();
  if (upper === "AND" || upper === "OR" || upper === "NOT") return false;
  let hasCJK = false;
  let hasLetter = false;
  for (const ch of [...tok]) {
    if (!isAllowedChar(ch)) return false;
    if (isCJK(ch)) hasCJK = true;
    else if (/[a-zA-Z0-9]/.test(ch)) hasLetter = true;
  }
  const n = [...tok].length;
  if (hasCJK) return n >= MIN_CJK && n <= MAX_CJK;
  if (hasLetter) return n >= 2 && n <= MAX_TOTAL;
  return false;
}

export type ThreadsTermCheck = { ok: true } | { ok: false; reason: string };

/** 回傳是否合規與可給 UI 的原因 key 後綴（由 i18n 翻譯）。 */
export function checkThreadsTerm(raw: string): ThreadsTermCheck {
  const term = normalizeSearchTerm(raw);
  if (!term) return { ok: false, reason: "empty" };
  const compact = term.replace(/\s+/g, "");
  if ([...compact].length > MAX_TOTAL) return { ok: false, reason: "tooLong" };
  const tokens = term.split(/\s+/);
  if (tokens.length > MAX_TOKENS) return { ok: false, reason: "tooManyTokens" };
  for (const tok of tokens) {
    if (!isAllowedToken(tok)) return { ok: false, reason: "invalidToken" };
  }
  return { ok: true };
}

export function isThreadsSearchable(term: string): boolean {
  return checkThreadsTerm(term).ok;
}

const SEARCH_FILLERS = [
  "怎麼辦",
  "求推薦",
  "有沒有人",
  "有人知道",
  "請問一下",
  "請問",
  "想問",
  "想找",
  "有沒有",
  "可以嗎",
  "好不好",
];

const MAX_VARIANTS = 8;
const MAX_VARIANTS_PER_TERM = 3;

function splitSearchParts(raw: string): string[] {
  const parts: string[] = [];
  let buf = "";
  const flush = () => {
    const value = buf.trim();
    if (value) parts.push(value);
    buf = "";
  };
  for (const ch of raw) {
    if (/[0-9A-Za-z]/.test(ch) || isCJK(ch)) buf += ch;
    else flush();
  }
  flush();
  return parts;
}

function stripSearchFillers(value: string): string {
  let next = value;
  for (const filler of SEARCH_FILLERS) next = next.split(filler).join("");
  return next.replace(/[ ,，、。！？!?：:的了嗎呢啊喔唷]+$/g, "").trim();
}

/**
 * 把使用者／產品長句收成 Threads 可搜短詞（與後端 domain.SearchableTermVariants 對齊）。
 * 已合規的原詞只回一則；過長句拆 token 對、去掉求助語尾，再取頭尾 2–4 字。
 */
export function searchableTermVariants(raw: string, include = true): string[] {
  const normalized = normalizeSearchTerm(raw);
  if (!normalized) return [];
  const seen = new Set<string>();
  const out: string[] = [];
  const add = (term: string) => {
    const next = normalizeSearchTerm(term);
    if (!next || seen.has(next.toLowerCase())) return;
    if (include) {
      if (!isThreadsSearchable(next)) return;
    } else if ([...next].length < 2 || [...next].length > 60) {
      return;
    }
    seen.add(next.toLowerCase());
    out.push(next);
  };

  if (include && isThreadsSearchable(normalized)) {
    add(normalized);
    return out;
  }
  if (!include) {
    add(normalized);
    if (out.length) return out;
  }

  const tokens = normalized.split(/\s+/).filter(Boolean);
  if (tokens.length > MAX_TOKENS) {
    for (let i = 0; i + 1 < tokens.length && out.length < MAX_VARIANTS; i += 1) {
      add(`${tokens[i]} ${tokens[i + 1]}`);
    }
    for (const tok of tokens) {
      if (out.length >= MAX_VARIANTS) break;
      for (const variant of searchableTermVariants(tok, include)) {
        add(variant);
        if (out.length >= MAX_VARIANTS) break;
      }
    }
    return out;
  }

  const parts = splitSearchParts(normalized);
  for (const part of parts.length ? parts : [normalized]) {
    if (out.length >= MAX_VARIANTS) break;
    if (include && isThreadsSearchable(part)) {
      add(part);
      continue;
    }
    const stripped = stripSearchFillers(part);
    if (stripped && stripped !== part) add(stripped);
    const runes = [...(stripped || part)];
    if (runes.length === 5) {
      add(runes.slice(0, 3).join(""));
      add(runes.slice(3).join(""));
    }
    for (const width of [4, 3, 2]) {
      if (runes.length < width) continue;
      add(runes.slice(0, width).join(""));
      if (runes.length > width) add(runes.slice(-width).join(""));
    }
    if (out.length >= 3) continue;
    for (let width = 4; width >= 2; width -= 1) {
      if (runes.length < width) continue;
      for (let start = 0; start + width <= runes.length && out.length < MAX_VARIANTS; start += 1) {
        add(runes.slice(start, start + width).join(""));
      }
    }
  }
  return out;
}

/** 訂閱關鍵字：已合規保留，長句各收成最多 3 則可搜短詞。 */
export function expandIncludeTerms(terms: string[], maxTotal = 20): string[] {
  const out: string[] = [];
  const seen = new Set<string>();
  for (const raw of terms) {
    const normalized = normalizeSearchTerm(raw);
    if (!normalized) continue;
    const variants = isThreadsSearchable(normalized)
      ? [normalized]
      : searchableTermVariants(normalized).slice(0, MAX_VARIANTS_PER_TERM);
    for (const term of variants) {
      const key = term.toLowerCase();
      if (seen.has(key)) continue;
      seen.add(key);
      out.push(term);
      if (out.length >= maxTotal) return out;
    }
  }
  return out;
}

/**
 * 依後端產詞順序選出實際要 fan-out 的查詢。
 * 原輸入若本身合規就保留為第一組；否則使用後端推論出的第一組。
 */
export function selectThreadsSearchTerms(
  intent: string,
  generated: string[],
  max = 3,
): string[] {
  const seen = new Set<string>();
  const available = generated
    .map(normalizeSearchTerm)
    .filter((term) => isThreadsSearchable(term))
    .filter((term) => {
      const key = term.toLowerCase();
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  const exact = normalizeSearchTerm(intent);
  const primary = isThreadsSearchable(exact) ? exact : available[0];
  if (!primary) return [];
  const ordered = [primary, ...available.filter((term) => term.toLowerCase() !== primary.toLowerCase())];
  return ordered.slice(0, Math.max(1, max));
}

function isThreadsHost(host: string): boolean {
  const normalized = host.toLowerCase();
  return (
    normalized === "threads.net" ||
    normalized.endsWith(".threads.net") ||
    normalized === "threads.com" ||
    normalized.endsWith(".threads.com")
  );
}

/** Stable frontend dedupe key for Threads profile permalinks and /t/ share links. */
export function threadsPostIdentity(raw: string): string | null {
  try {
    const url = new URL(raw.trim());
    const parts = url.pathname.split("/").filter(Boolean);
    if (isThreadsHost(url.hostname)) {
      for (let i = 0; i + 1 < parts.length; i += 1) {
        const segment = parts[i]?.toLowerCase();
        if ((segment === "post" || segment === "t") && parts[i + 1]) {
          return `threads-post:${parts[i + 1]}`;
        }
      }
    }
    const host = isThreadsHost(url.hostname) ? "threads.net" : url.hostname.toLowerCase();
    return `url:${host}${url.pathname.replace(/\/$/, "")}`;
  } catch {
    return null;
  }
}
