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

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

  async route({ message, 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,
      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);

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

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

  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
    };
    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,
      platform: context.platform
    });
    return output;
  }
}

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) {
  return /\b(delete|remove|ban|timeout|moderate|transfer|pay|give|balance|inventory|economy|points|currency|database|file|execute|run|install|api|token|password|secret|permission|role|my|mine|our|ours|their|theirs|this user|user id|username)\b/i
    .test(String(message || ""));
}

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,
  classifyRequestType,
  normalizeRequestClass,
  hasRouteReference,
  withTimeout
};
