const SAFE_ROUTES = new Set([
  "cached_answer",
  "predefined_answer",
  "main_llm",
  "clarification",
  "refusal",
  "unavailable"
]);

class GateProvider {
  constructor({ getConfig, runtime, lookupRepo, lookupCorrection, lookupKnowledge, groundKnowledgeLinks, cache, metrics }) {
    Object.assign(this, { getConfig, runtime, lookupRepo, lookupCorrection, lookupKnowledge, groundKnowledgeLinks, cache, metrics });
    this.recentPrompts = new Map();
  }

  async route({ message, knowledgeQuery = "", user, role, scope, originContext, onStage = () => {} }) {
    const started = Date.now();
    const cfg = this.getConfig();
    const gate = cfg.gate || {};
    const prepared = stripForcePrefix(message, gate.force_prefix);
    const context = {
      message: prepared.message,
      knowledge_query: knowledgeQuery || prepared.message,
      role,
      platform: originContext?.platform || originContext?.origin || "webui"
    };
    const requestClass = classifyRequestType(context.message, { role, scope });
    onStage("deterministic");
    const forceReason = prepared.forced
      ? "explicit_force_prefix"
      : this.isRepeat(context, user?.id, scope, gate)
        ? "repeat_prompt_force"
        : null;
    this.remember(context.message, user?.id, scope, gate);
    const knowledge = this.lookupEarlyKnowledge({
      ...context,
      knowledge_query: context.knowledge_query,
      user,
      scope,
      originContext
    });

    if (forceReason) {
      return this.finish({
        route: "main_llm",
        confidence: 1,
        reason_code: forceReason,
        message: context.message,
        forced: true,
        request_class: requestClass,
        deterministic_ms: Date.now() - started,
        gate_ms: 0,
        knowledge_context: knowledge.blocks,
        knowledge_diagnostics: knowledge.diagnostics
      }, started, context);
    }

    if (isSensitiveRequest(context.message)) {
      return this.finish({
        route: "main_llm",
        confidence: 1,
        reason_code: "sensitive_or_user_specific",
        message: context.message,
        request_class: requestClass,
        deterministic_ms: Date.now() - started,
        gate_ms: 0,
        knowledge_context: knowledge.blocks,
        knowledge_diagnostics: knowledge.diagnostics
      }, started, context);
    }

    const reviewed = gate.predefined_enabled !== false
      ? this.lookupCorrection?.({
          ...context,
          origin: originContext?.origin || context.platform
        })
      : null;
    if (reviewed) {
      return this.finish({
        route: "predefined_answer",
        confidence: reviewed.score,
        reason_code: `approved_${reviewed.target}`,
        message: context.message,
        answer: {
          text: reviewed.corrected_answer,
          links: reviewed.expected_link
            ? [{ label: reviewed.route_alias || "Open verified Lumi page", href: reviewed.expected_link }]
            : [],
          source: { type: "approved_correction", id: reviewed.id },
          safe: true
        },
        request_class: requestClass,
        deterministic_ms: Date.now() - started,
        gate_ms: 0
      }, started, context);
    }

    if (knowledge.blocks.length) {
      const groundedLink = this.groundKnowledgeLinks?.("", knowledge.blocks, context.message);
      if (groundedLink?.diagnostics?.grounded) {
        return this.finish({
          route: "predefined_answer",
          confidence: 1,
          reason_code: "verified_okf_link",
          message: context.message,
          answer: {
            text: groundedLink.text,
            links: groundedLink.links,
            source: { type: "approved_okf" },
            safe: true
          },
          request_class: requestClass,
          deterministic_ms: Date.now() - started,
          gate_ms: 0,
          knowledge_context: knowledge.blocks,
          knowledge_diagnostics: knowledge.diagnostics
        }, started, context);
      }
      const fastStarted = Date.now();
      onStage("gating");
      const fastAnswer = await this.answerFromKnowledge(context, knowledge.blocks);
      if (fastAnswer && !isComplexOrAmbiguous(context.message)) {
        return this.finish({
          route: "predefined_answer",
          confidence: 0.95,
          reason_code: "verified_okf_fast_answer",
          message: context.message,
          answer: {
            text: fastAnswer,
            links: [],
            source: { type: "approved_okf" },
            safe: true
          },
          request_class: requestClass,
          deterministic_ms: fastStarted - started,
          gate_ms: Date.now() - fastStarted,
          knowledge_context: knowledge.blocks,
          knowledge_diagnostics: knowledge.diagnostics,
          knowledge_sufficient: true
        }, started, context);
      }
      return this.finish({
        route: "main_llm",
        confidence: 1,
        reason_code: "okf_requires_main_llm",
        message: context.message,
        request_class: requestClass,
        deterministic_ms: Date.now() - started,
        gate_ms: Date.now() - fastStarted,
        knowledge_context: knowledge.blocks,
        knowledge_diagnostics: knowledge.diagnostics,
        knowledge_sufficient: Boolean(fastAnswer)
      }, started, context);
    }

    const cached = gate.predefined_enabled !== false ? this.cache?.get(context) : null;
    if (cached) {
      return this.finish({
        route: "cached_answer",
        confidence: 1,
        reason_code: "exact_cache_hit",
        message: context.message,
        answer: cached,
        request_class: requestClass,
        deterministic_ms: Date.now() - started,
        gate_ms: 0
      }, started, context);
    }

    const repoAnswer = gate.predefined_enabled !== false
      ? this.lookupRepo?.(context.message) || null
      : null;
    if (isExactPredefinedQuery(context.message, repoAnswer)) {
      const answer = {
        text: repoAnswer.text,
        links: repoAnswer.links || [],
        source: repoAnswer.source || null,
        safe: true
      };
      this.cache?.set(context, answer);
      return this.finish({
        route: "predefined_answer",
        confidence: 1,
        reason_code: `exact_verified_${repoAnswer.type}`,
        message: context.message,
        answer,
        request_class: "navigation_help",
        deterministic_ms: Date.now() - started,
        gate_ms: 0
      }, started, context);
    }

    if (isComplexOrAmbiguous(context.message)) {
      return this.finish({
        route: "main_llm",
        confidence: 1,
        reason_code: "deterministic_complexity_escalation",
        message: context.message,
        request_class: requestClass,
        deterministic_ms: Date.now() - started,
        gate_ms: 0
      }, started, context);
    }

    if (isSimpleKnowledgeLookup(context.message)) {
      return this.finish({
        route: "main_llm",
        confidence: 0.86,
        reason_code: "simple_knowledge_lookup",
        message: context.message,
        request_class: requestClass,
        deterministic_ms: Date.now() - started,
        gate_ms: 0
      }, started, context);
    }

    const deterministicMs = Date.now() - started;
    const gateStarted = Date.now();
    onStage("gating");
    let classification;
    try {
      classification = await this.classify(context);
    } catch (error) {
      classification = {
        route: "main_llm",
        confidence: 0,
        reason_code: isTimeoutError(error) ? "gate_timeout_escalated" : "gate_error_escalated",
        gate_error: error.message
      };
    }

    const normalized = normalizeDecision(classification);
    const mainThreshold = Math.max(0.1, Math.min(0.95, Number(gate.main_llm_threshold) || 0.72));
    const highThreshold = Math.max(0.5, Math.min(0.99, Number(gate.high_confidence_threshold) || 0.88));
    let decision = normalized;

    if (["refusal", "unavailable"].includes(decision.route) && decision.confidence < highThreshold) {
      decision = {
        route: "main_llm",
        confidence: decision.confidence,
        reason_code: "terminal_route_low_confidence"
      };
    } else if (["cached_answer", "predefined_answer"].includes(decision.route)) {
      decision = {
        route: "main_llm",
        confidence: decision.confidence,
        reason_code: "gate_cannot_authorize_predefined"
      };
    } else if (
      (decision.confidence < mainThreshold || !SAFE_ROUTES.has(decision.route)) &&
      !["gate_timeout_escalated", "gate_error_escalated"].includes(decision.reason_code)
    ) {
      decision = {
        route: "main_llm",
        confidence: decision.confidence,
        reason_code: "low_confidence"
      };
    } else if (decision.route === "refusal") {
      decision.answer = {
        text: cfg.instructions?.out_of_scope_response || "I cannot help with that request.",
        links: [],
        safe: true
      };
    } else if (decision.route === "unavailable") {
      decision.answer = {
        text: cfg.commands?.unavailable_message || "Lumi Assistant is currently unavailable.",
        links: [],
        safe: true
      };
    } else if (decision.route === "clarification") {
      decision.route = "main_llm";
      decision.reason_code = "clarification_requires_main_llm";
    }

    return this.finish({
      ...decision,
      message: context.message,
      request_class: requestClass,
      deterministic_ms: deterministicMs,
      gate_ms: Date.now() - gateStarted
    }, started, context);
  }

  async classify(context) {
    if (this.runtime.status().state !== "running") throw new Error("Gate runtime is unavailable.");
    const timeoutMs = Math.max(1000, Math.min(5000, Number(this.getConfig().gate?.timeout_ms) || 3000));
    const prompt = [
      "Classify only. JSON only.",
      "Routes: main_llm, refusal, unavailable.",
      "Escalate uncertainty or complexity to main_llm.",
      '{"route":"main_llm","confidence":0.0,"reason_code":"short_code"}'
    ].join("\n");
    const result = await withTimeout(this.runtime.infer([
      { role: "system", content: prompt },
      { role: "user", content: String(context.message).slice(0, 1000) }
    ], 64, timeoutMs), timeoutMs);
    return parseDecision(result.choices?.[0]?.message?.content);
  }

  lookupEarlyKnowledge(context) {
    if (typeof this.lookupKnowledge !== "function") return { blocks: [], diagnostics: null };
    try {
      const result = this.lookupKnowledge(context);
      if (result && typeof result === "object" && !Array.isArray(result)) {
        return {
          blocks: normalizeKnowledgeBlocks(result.blocks ?? result.context ?? result.items ?? []),
          diagnostics: result.diagnostics || null
        };
      }
      return { blocks: normalizeKnowledgeBlocks(result), diagnostics: null };
    } catch {
      return { blocks: [], diagnostics: null };
    }
  }

  async answerFromKnowledge(context, blocks) {
    if (this.runtime.status().state !== "running") return null;
    const timeoutMs = Math.max(1000, Math.min(5000, Number(this.getConfig().gate?.timeout_ms) || 3000));
    const evidence = prepareFastKnowledgeEvidence(blocks, context.message);
    const prompt = [
      "Use only the VERIFIED FACTS below.",
      "Write a short, natural, satisfying reply of 1 to 3 complete sentences.",
      "Answer directly, then add one or two useful distinguishing details when available.",
      "For identity questions, prefer roles, relationships, aliases, activities, and named channels. Do not merely repeat the first sentence.",
      "Resolve pronouns using the RESOLVED REQUEST. Do not mention facts, context, OKF, files, verification, workflows, or databases.",
      "Do not print raw URLs unless the user asks for a link. Never invent details, routes, or links.",
      "If the facts do not answer the request, output only INSUFFICIENT.",
      `RESOLVED REQUEST:\n${String(context.knowledge_query || context.message).slice(0, 700)}`,
      `VERIFIED FACTS:\n${evidence}`
    ].join("\n");
    try {
      const result = await withTimeout(this.runtime.infer([
        { role: "system", content: prompt },
        { role: "user", content: context.message }
      ], 256, timeoutMs), timeoutMs);
      const rawAnswer = result.choices?.[0]?.message?.content;
      const answer = parseKnowledgeAnswer(rawAnswer, blocks, context.message);
      if (answer) return answer;
      const groundedDraft = parseKnowledgeAnswer(rawAnswer, blocks, context.message, { requireSatisfying: false });
      if (!groundedDraft) return null;
      const personRelationshipQuestion = isPersonRelationshipQuestion(context.message);
      const repairPrompt = (personRelationshipQuestion ? [
        "Using only the verified facts, finish the sentence start with one useful detail and output the complete sentence.",
        "Preserve the relationship in the sentence start. Add a role, activity, or alias; do not invent anything.",
        `SENTENCE START:\n${groundedDraft.replace(/[.!?]+$/, "").slice(0, 240)}, who`
      ] : [
        "The first answer was correct but too abrupt.",
        "Write one natural sentence of 10 to 24 words using only the verified facts.",
        "Answer directly and add at least one different supporting role, relationship, activity, alias, or named channel."
      ]).concat([
        "Do not mention context, OKF, files, verification, or missing information. Do not invent anything.",
        `RESOLVED REQUEST:\n${String(context.knowledge_query || context.message).slice(0, 700)}`,
        `VERIFIED FACTS:\n${evidence}`
      ]).join("\n");
      const repaired = await withTimeout(this.runtime.infer([
        { role: "system", content: repairPrompt },
        { role: "user", content: context.message }
      ], 192, timeoutMs), timeoutMs);
      return parseKnowledgeAnswer(repaired.choices?.[0]?.message?.content, blocks, context.message);
    } catch {
      return null;
    }
  }

  isRepeat(context, userId, scope, gate) {
    const configuredWindow = Number(gate.repeat_force_window_seconds);
    const windowMs = Math.max(0, Number.isFinite(configuredWindow) ? configuredWindow : 90) * 1000;
    if (!windowMs) return false;
    const key = `${userId || "anonymous"}:${scope || "assistant"}`;
    const rows = (this.recentPrompts.get(key) || []).filter((entry) => Date.now() - entry.at <= windowMs);
    const threshold = Math.max(0.5, Math.min(1, Number(gate.similarity_threshold) || 0.86));
    return rows.some((entry) => similarity(entry.message, context.message) >= threshold);
  }

  remember(message, userId, scope, gate) {
    const key = `${userId || "anonymous"}:${scope || "assistant"}`;
    const configuredWindow = Number(gate.repeat_force_window_seconds);
    const windowMs = Math.max(1, Number.isFinite(configuredWindow) ? configuredWindow : 90) * 1000;
    const rows = (this.recentPrompts.get(key) || [])
      .filter((entry) => Date.now() - entry.at <= windowMs)
      .slice(-9);
    rows.push({ message, at: Date.now() });
    this.recentPrompts.set(key, rows);
  }

  finish(decision, started, context) {
    const output = {
      route: decision.route,
      confidence: Number(decision.confidence) || 0,
      reason_code: decision.reason_code || "unspecified",
      answer: decision.answer || null,
      message: decision.message || context.message,
      gate_error: decision.gate_error ? String(decision.gate_error).slice(0, 300) : null,
      forced: Boolean(decision.forced),
      request_class: normalizeRequestClass(decision.request_class),
      deterministic_ms: Math.max(0, Number(decision.deterministic_ms) || 0),
      gate_ms: Math.max(0, Number(decision.gate_ms) || 0),
      duration_ms: Date.now() - started
    };
    Object.defineProperties(output, {
      knowledge_context: {
        value: normalizeKnowledgeBlocks(decision.knowledge_context),
        enumerable: false
      },
      knowledge_diagnostics: {
        value: decision.knowledge_diagnostics || null,
        enumerable: false
      }
    });
    output.knowledge_match_count = output.knowledge_context.length;
    output.knowledge_sufficient = Boolean(decision.knowledge_sufficient || decision.answer?.source?.type === "approved_okf");
    this.metrics.record({
      kind: "gate_decision",
      status: "success",
      route_used: output.route,
      confidence: output.confidence,
      reason_code: output.reason_code,
      gate_error: output.gate_error,
      request_class: output.request_class,
      route_class: output.request_class,
      deterministic_ms: output.deterministic_ms,
      gate_ms: output.gate_ms,
      duration_ms: output.duration_ms,
      okf_checked: typeof this.lookupKnowledge === "function",
      okf_match_count: output.knowledge_match_count,
      okf_sufficient: output.knowledge_sufficient,
      platform: context.platform
    });
    return output;
  }
}

function normalizeKnowledgeBlocks(value) {
  return (Array.isArray(value) ? value : value == null ? [] : [value])
    .map((block) => String(block || "").trim())
    .filter(Boolean)
    .slice(0, 2);
}

function prepareFastKnowledgeEvidence(blocks = [], message = "") {
  const needsRelationshipContext = /\b(?:he|him|his|she|her|hers|they|them|their|theirs|fianc(?:e|ee|é|ée)?|wife|husband|partner|relationship|related)\b/i
    .test(String(message || ""));
  const selectedBlocks = normalizeKnowledgeBlocks(blocks).slice(0, needsRelationshipContext ? 2 : 1);
  return selectedBlocks.map((rawBlock) => {
    const heading = rawBlock.match(/(?:^|;\s*)heading=([^;\n]+)/im)?.[1] ||
      rawBlock.match(/^OKF entry:\s*(.+)$/im)?.[1] || "Relevant knowledge";
    const factsMarker = rawBlock.match(/(?:^|\n)Facts:\s*\n/i);
    let facts = factsMarker
      ? rawBlock.slice((factsMarker.index || 0) + factsMarker[0].length)
      : rawBlock;
    facts = facts
      .replace(/\[([^\]]+)\]\(https?:\/\/[^)\s]+\)/gi, "$1")
      .replace(/\bis a known community contact in Lumi's local community knowledge\b/gi, "is a Lumi community contact")
      .replace(/When users ask how to contact[\s\S]*?unless a verified internal Lumi workflow is available\./gi, "The recommended contact channel is the community Discord server.")
      .replace(/^Links:\s*$/gim, "Named channels:")
      .replace(/\n{3,}/g, "\n\n")
      .trim();
    return `Section: ${heading}\n${facts}`;
  }).join("\n\n").slice(0, 2300);
}

function parseKnowledgeAnswer(value, blocks = [], message = "", { requireSatisfying = true } = {}) {
  const cleaned = String(value || "")
    .replace(/<think>[\s\S]*?<\/think>/gi, "")
    .trim();
  const marker = cleaned.match(/(?:^|\n)\s*(ANSWER|INSUFFICIENT)\s*:?[ \t]*(?:\n|$)?/i);
  if (marker?.[1]?.toUpperCase() === "INSUFFICIENT" || /^\s*INSUFFICIENT\b/i.test(cleaned)) return null;
  let answer = marker?.[1]?.toUpperCase() === "ANSWER"
    ? cleaned.slice((marker.index || 0) + marker[0].length).trim()
    : cleaned;
  if (!answer || answer.length > 1800) return null;
  if (!hasKnowledgeSupport(answer, blocks)) return null;
  if (requireSatisfying && !isSatisfyingFastAnswer(answer, message)) return null;
  const allowedUrls = new Set(extractHttpUrls(blocks.join("\n")).map(normalizeUrl).filter(Boolean));
  answer = answer.replace(/https?:\/\/[^\s<>()\]]+/gi, (match) => {
    const punctuation = match.match(/[.,;:!?]+$/)?.[0] || "";
    const raw = punctuation ? match.slice(0, -punctuation.length) : match;
    return allowedUrls.has(normalizeUrl(raw)) ? match : "[unsupported link removed]";
  });
  return answer;
}

function isSatisfyingFastAnswer(answer, message) {
  const words = String(answer || "").trim().split(/\s+/).filter(Boolean);
  const identityQuestion = /^\s*who(?:'s|\s+is|\s+are)\b/i.test(String(message || ""));
  const relationshipQuestion = isPersonRelationshipQuestion(message) || /\b(?:name|nickname|alias)\b/i.test(String(message || ""));
  if (identityQuestion && !relationshipQuestion && words.length < 8) return false;
  if (identityQuestion && relationshipQuestion && words.length < 6) return false;
  if (isPersonRelationshipQuestion(message) && !answersRequestedRelationship(answer, message)) return false;
  return words.length >= 3;
}

function isPersonRelationshipQuestion(message) {
  return /\b(?:fianc(?:e|ee|é|ée)?|wife|husband|partner|owner)\b/i.test(String(message || ""));
}

function answersRequestedRelationship(answer, message) {
  const response = String(answer || "");
  const request = String(message || "");
  if (/\bfianc(?:e|ee|é|ée)?\b/i.test(request)) return /\b(?:fianc(?:e|ee|é|ée)?|engaged)\b/i.test(response);
  if (/\b(?:wife|husband)\b/i.test(request)) return /\b(?:wife|husband|spouse|married)\b/i.test(response);
  if (/\bpartner\b/i.test(request)) return /\bpartner\b/i.test(response);
  if (/\bowner\b/i.test(request)) return /\bown(?:er|s|ed|ing)?\b/i.test(response);
  return true;
}

function hasKnowledgeSupport(answer, blocks) {
  const ignored = new Set([
    "about", "also", "and", "are", "but", "for", "from", "have", "he", "her", "hers", "him", "his", "into", "its", "only", "she", "that", "the", "their", "them", "they", "this", "through", "use", "was", "were", "who", "with", "you", "your"
  ]);
  const tokens = (value) => String(value || "").toLowerCase().split(/[^a-z0-9]+/)
    .filter((token) => token.length >= 3 && !ignored.has(token));
  const answerTokens = [...new Set(tokens(answer))];
  if (!answerTokens.length) return false;
  const evidenceTokens = new Set(tokens(blocks.join("\n")));
  const supported = answerTokens.filter((token) => evidenceTokens.has(token)).length;
  return supported >= Math.min(3, answerTokens.length) && supported / answerTokens.length >= 0.35;
}

function extractHttpUrls(value) {
  return String(value || "").match(/https?:\/\/[^\s<>()\]]+/gi) || [];
}

function normalizeUrl(value) {
  try {
    const url = new URL(String(value || "").replace(/[.,;:!?]+$/, ""));
    if (!['http:', 'https:'].includes(url.protocol)) return "";
    url.hash = "";
    if (url.pathname.length > 1) url.pathname = url.pathname.replace(/\/+$/, "");
    return url.href;
  } catch {
    return "";
  }
}

function parseDecision(value) {
  const text = String(value || "").trim();
  const match = text.match(/\{[\s\S]*\}/);
  if (!match) throw new Error("Gate model returned invalid JSON.");
  return JSON.parse(match[0]);
}

function normalizeDecision(value = {}) {
  return {
    route: SAFE_ROUTES.has(value.route) ? value.route : "main_llm",
    confidence: Math.max(0, Math.min(1, Number(value.confidence) || 0)),
    reason_code: /^[a-z0-9_]{2,80}$/.test(String(value.reason_code || ""))
      ? value.reason_code
      : "invalid_reason_code",
    gate_error: value.gate_error ? String(value.gate_error).slice(0, 300) : null
  };
}

function stripForcePrefix(message, prefix = "force ai:") {
  const text = String(message || "").trim();
  const configured = String(prefix || "").trim();
  if (!configured || !text.toLowerCase().startsWith(configured.toLowerCase())) {
    return { message: text, forced: false };
  }
  return { message: text.slice(configured.length).trim() || text, forced: true };
}

function isSensitiveRequest(message) {
  const text = String(message || "");
  if (/\b(token|password|secret|credential|api key|private key)\b/i.test(text)) return true;
  const personal = /\b(my|mine|our|ours|their|theirs|this user|user id|username)\b/i.test(text) &&
    /\b(balance|inventory|economy|points|currency|database|file|permission|role|account|profile)\b/i.test(text);
  const action = /\b(delete|remove|ban|timeout|moderate|transfer|pay|give|set|change|edit|execute|run|install|grant|revoke|reset)\b/i.test(text) &&
    /\b(balance|inventory|economy|points|currency|database|file|api|permission|role|user|account|plugin|command)\b/i.test(text);
  return personal || action;
}

function isCacheSafeRepoAnswer(answer) {
  if (!answer?.text) return false;
  if (answer.type === "route") return answer.source?.confidence === "high";
  return ["contact", "unknown"].includes(answer.type);
}

function isExactPredefinedQuery(message, answer) {
  if (!isCacheSafeRepoAnswer(answer)) return false;
  if (isComplexOrAmbiguous(message)) return false;
  if (answer.type === "contact") return true;
  if (answer.type !== "route" || answer.source?.confidence !== "high") return false;
  return /\b(where|open|find|navigate|page|screen|menu|settings?|configuration|wizard|location)\b/i
    .test(String(message || ""));
}

function isComplexOrAmbiguous(message) {
  const text = String(message || "");
  if (text.length > 500 || text.split(/\s+/).length > 70) return true;
  return /\b(why|explain|debug|diagnos|troubleshoot|fix|error|failed|failure|code|javascript|python|implement|design|compare|analy[sz]e|step by step|multi[- ]?step|architecture|configure and|set up and|what should|this|that|it)\b/i
    .test(text);
}

function isSimpleKnowledgeLookup(message) {
  const text = String(message || "").trim();
  if (!text || text.length > 180 || text.split(/\s+/).length > 18) return false;
  if (/\b(who|what)\s+(?:are|r)\s+you\b|\byour\s+(?:name|identity)\b/i.test(text)) return false;
  return (
    /^(?:who|what)\s+(?:is|are|was|were)\s+["'`]?[\p{L}\p{N}_ .'-]{2,80}["'`]?\??$/iu.test(text) ||
    /^(?:tell me about|describe|identify)\s+["'`]?[\p{L}\p{N}_ .'-]{2,80}["'`]?\??$/iu.test(text)
  );
}

function isTimeoutError(error) {
  return error?.name === "TimeoutError" || error?.name === "AbortError" || /timed?\s*out|timeout/i.test(error?.message || "");
}

function classifyRequestType(message, { role = "user", scope = "assistant" } = {}) {
  const text = String(message || "").trim();
  if (hasRouteReference(text)) {
    return "navigation_help";
  }
  if (/\b(explicitly|please|give|write|provide|show)\b[\s\S]{0,60}\b(long|detailed|comprehensive|thorough|in[- ]depth)\b|\b(full (analysis|report|guide|explanation)|in detail|very detailed|long answer)\b/i.test(text)) {
    return "explicit_long";
  }
  if (/\b(custom command|javascript|python|code block|function run\s*\(|def run\s*\(|implement|write code|script)\b/i.test(text)) {
    return "code_custom_command";
  }
  if (
    role === "admin" &&
    (scope === "model_test" || /\b(debug|diagnos|troubleshoot|stack trace|runtime|backend|database|logs?|metrics?|configuration|config|error|failed|failure)\b/i.test(text))
  ) {
    return "admin_debug";
  }
  if (/\b(where|open|find|navigate|page|screen|menu|settings?|configuration|wizard|location|link|path)\b/i.test(text)) {
    return "navigation_help";
  }
  return "simple_answer";
}

function hasRouteReference(text) {
  return /\b(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+\/[^\s]+/i.test(text) ||
    /\b(?:route|webroute|web route|endpoint|request|api)\b[\s\S]{0,80}\/[a-z0-9_/-]+/i.test(text) ||
    /\/(?:admin|api|setup|auth|plugins|commands|feedback|stats|pages)(?:\/[a-z0-9_/-]*)?\b/i.test(text);
}

function normalizeRequestClass(value) {
  return [
    "navigation_help",
    "simple_answer",
    "code_custom_command",
    "admin_debug",
    "explicit_long"
  ].includes(value) ? value : "simple_answer";
}

function withTimeout(promise, timeoutMs) {
  let timer;
  const timeout = new Promise((_, reject) => {
    timer = setTimeout(() => {
      reject(Object.assign(new Error(`Gate timed out after ${timeoutMs}ms.`), { name: "TimeoutError" }));
    }, timeoutMs);
  });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}

function similarity(left, right) {
  const a = tokens(left);
  const b = tokens(right);
  if (!a.size || !b.size) return 0;
  let intersection = 0;
  for (const token of a) if (b.has(token)) intersection += 1;
  return intersection / (a.size + b.size - intersection);
}

function tokens(value) {
  const ignored = new Set([
    "a", "an", "are", "can", "could", "do", "find", "for", "how", "i", "in", "is",
    "me", "of", "please", "the", "this", "to", "where", "would", "you"
  ]);
  return new Set(
    String(value || "").toLowerCase().split(/[^a-z0-9]+/)
      .filter((token) => token && !ignored.has(token))
  );
}

module.exports = {
  GateProvider,
  parseDecision,
  stripForcePrefix,
  isSensitiveRequest,
  similarity,
  isCacheSafeRepoAnswer,
  isExactPredefinedQuery,
  isComplexOrAmbiguous,
  isSimpleKnowledgeLookup,
  isSatisfyingFastAnswer,
  prepareFastKnowledgeEvidence,
  classifyRequestType,
  normalizeRequestClass,
  hasRouteReference,
  withTimeout
};
