/** Server-only public assistant, served by the marketplace's own API routes. */
import { answerChat, loadPublicData } from './assistant/engine.mjs';
import { applicationOrigin } from './application-origin';
if (typeof window !== 'undefined') throw new Error('Server-only assistant');
type AssistantEnvironment = Record<string, string | undefined>;
type RateDecision = { allowed: boolean; retryAfter: number };
type AssistantOptions = {
 env?: AssistantEnvironment;
 fetchImpl?: typeof fetch;
 onDiagnostic?: (category: string) => void;
 rateLimiter?: () => RateDecision;
};
export type AssistantHistory = { role: 'user' | 'assistant'; content: string }[];
export type AssistantReply = {
 answer: string;
 mode: 'reference' | 'openai' | 'fallback';
 sources: { id: string; title: string }[];
 suggestions: string[];
};
const BODY_LIMIT = 64 * 1024;
const UNAVAILABLE = 'پاسخ دریافت نشد. لطفاً کمی بعد دوباره تلاش کنید.';
// One aggregate bucket per server instance: no client-controlled IP header is
// trusted and no unbounded visitor map is retained. A shared, trusted ingress
// limiter is required before public multi-instance deployment.
export function createAssistantRateLimiter({ limit = 30, windowMs = 60000, now = Date.now }: {
 limit?: number; windowMs?: number; now?: () => number;
} = {}): () => RateDecision {
 if (!Number.isInteger(limit) || limit < 1 || limit > 300 || !Number.isInteger(windowMs) || windowMs < 1000 || windowMs > 300000) throw new RangeError('Invalid assistant rate limit');
 let resetAt = 0, count = 0;
 return () => {
  const current = now();
  if (current >= resetAt || current < resetAt - windowMs) { resetAt = current + windowMs; count = 0; }
  if (count >= limit) return { allowed: false, retryAfter: Math.max(1, Math.ceil((resetAt - current) / 1000)) };
  count += 1;
  return { allowed: true, retryAfter: 0 };
 };
}
const defaultRateLimiter = createAssistantRateLimiter();
function record(value: unknown): value is Record<string, unknown> {
 return !!value && typeof value === 'object' && !Array.isArray(value);
}
function text(value: unknown, maximum: number): string | null {
 // Match the assistant service's Unicode code-point limit, not UTF-16 units.
 return typeof value === 'string' && value.trim() && Array.from(value).length <= maximum ? value.trim() : null;
}
function response(body: unknown, status = 200, retryAfter?: string) {
 return Response.json(body, { status, headers: {
  'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff',
  ...(retryAfter ? { 'Retry-After': retryAfter } : {}),
 } });
}
function allowedOrigin(request: Request, required: boolean) {
 const origin = request.headers.get('origin');
 try {
  return request.headers.get('sec-fetch-site') !== 'cross-site'
   && (origin ? origin === applicationOrigin(request) : !required);
 } catch { return false; }
}
export function validateAssistantInput(body: unknown): { message: string; history: AssistantHistory } | null {
 if (!record(body) || Object.keys(body).some(key => !['message', 'history'].includes(key))) return null;
 const message = text(body.message, 1500);
 const history = body.history === undefined ? [] : body.history;
 if (!message || !Array.isArray(history) || history.length > 6) return null;
 const normalized: AssistantHistory = [];
 for (const item of history) {
  if (!record(item) || Object.keys(item).some(key => !['role', 'content'].includes(key)) || typeof item.role !== 'string' || !['user', 'assistant'].includes(item.role)) return null;
  const content = text(item.content, 1500);
  if (!content) return null;
  normalized.push({ role: item.role as 'user' | 'assistant', content });
 }
 return { message, history: normalized };
}
async function readBoundedJson(input: Request | Response): Promise<unknown> {
 const size = Number(input.headers.get('content-length'));
 if (Number.isFinite(size) && size > BODY_LIMIT) throw new Error('size');
 const reader = input.body?.getReader();
 if (!reader) throw new Error('body');
 const chunks: Uint8Array[] = [];
 let length = 0;
 try {
  while (true) {
   const { value, done } = await reader.read();
   if (done) break;
   length += value.byteLength;
   if (length > BODY_LIMIT) { await reader.cancel(); throw new Error('size'); }
   chunks.push(value);
  }
 } finally { reader.releaseLock(); }
 const data = new Uint8Array(length);
 let offset = 0;
 for (const chunk of chunks) { data.set(chunk, offset); offset += chunk.length; }
 return JSON.parse(new TextDecoder().decode(data));
}
function suggestions(value: unknown): string[] | null {
 if (!Array.isArray(value) || value.length > 6) return null;
 const list = value.map(item => text(item, 120));
 return list.some(item => item === null) ? null : list as string[];
}
export function parseAssistantReply(value: unknown): AssistantReply | null {
 if (!record(value)) return null;
 const answer = text(value.answer, 12000), items = suggestions(value.suggestions);
 if (!answer || typeof value.mode !== 'string' || !['reference', 'openai', 'fallback'].includes(value.mode) || !items || !Array.isArray(value.sources) || value.sources.length > 8) return null;
 const sources: AssistantReply['sources'] = [];
 for (const source of value.sources) {
  if (!record(source)) return null;
  const id = text(source.id, 80), title = text(source.title, 1500);
  if (!id || !/^[A-Z][A-Z0-9-]{2,79}$/.test(id) || !title) return null;
  sources.push({ id, title });
 }
 return { answer, mode: value.mode as AssistantReply['mode'], sources, suggestions: items };
}
function parseConfig(value: unknown) {
 if (!record(value)) return null;
 const title = text(value.title, 100), greeting = text(value.greeting, 600), notice = text(value.notice, 500), items = suggestions(value.suggestions);
 if (!title || !greeting || !notice || !items || value.scope !== 'plank') return null;
 let url: string | null = null, phone: string | null = null;
 if (record(value.support)) {
  if (typeof value.support.url === 'string' && value.support.url.length <= 500) {
   try { const parsed = new URL(value.support.url); if (parsed.protocol === 'https:' && !parsed.username && !parsed.password) url = parsed.href; } catch { /* Hide invalid destinations. */ }
  }
  if (typeof value.support.phone === 'string' && /^[+\d۰-۹٠-٩\s()-]{3,40}$/u.test(value.support.phone)) phone = value.support.phone;
 }
 return { title, greeting, notice, suggestions: items, scope: 'plank', support: { url, phone } };
}
export async function assistantChat(request: Request, options: AssistantOptions = {}) {
 if (!allowedOrigin(request, true)) return response({ error: 'مبدأ درخواست معتبر نیست.' }, 403);
 if (!/^application\/json(?:\s*;|$)/i.test(request.headers.get('content-type') || '')) return response({ error: 'نوع درخواست باید JSON باشد.' }, 415);
 let body: unknown;
 try { body = await readBoundedJson(request); } catch { return response({ error: 'ساختار یا حجم پیام معتبر نیست.' }, 400); }
 const input = validateAssistantInput(body);
 if (!input) return response({ error: 'پیام حداکثر ۱۵۰۰ نویسه و تاریخچه حداکثر ۶ پیام معتبر داشته باشد.' }, 400);
 if (request.signal.aborted) return response({ error: UNAVAILABLE }, 503);
 const rate = (options.rateLimiter ?? defaultRateLimiter)();
 if (!rate.allowed) return response({ error: 'تعداد پرسش‌ها زیاد است. کمی صبر کنید و دوباره تلاش کنید.' }, 429, String(rate.retryAfter));
 try {
  const signal = AbortSignal.any([request.signal, AbortSignal.timeout(65000)]);
  const data = await loadPublicData();
  signal.throwIfAborted();
  const configured = options.env ?? process.env;
  // Only the assistant's server secrets/configuration enter the engine.
  // Account cookies, user IDs and request headers are never forwarded.
  const result = await answerChat(input, data, {
   env: {
    OPENAI_API_KEY: configured.OPENAI_API_KEY,
    OPENAI_MODEL: configured.OPENAI_MODEL,
    OPENAI_TIMEOUT_MS: configured.OPENAI_TIMEOUT_MS,
   },
   fetchImpl: options.fetchImpl ?? fetch,
   onDiagnostic: options.onDiagnostic,
   signal,
  });
  signal.throwIfAborted();
  const parsed = parseAssistantReply(result);
  return parsed ? response(parsed) : response({ error: UNAVAILABLE }, 503);
 } catch { return response({ error: UNAVAILABLE }, 503); }
}
export async function assistantConfig(request: Request) {
 if (!allowedOrigin(request, false)) return response({ error: 'مبدأ درخواست معتبر نیست.' }, 403);
 try {
  request.signal.throwIfAborted();
  const data = await loadPublicData();
  request.signal.throwIfAborted();
  const parsed = parseConfig(data.config);
  return parsed ? response(parsed) : response({ error: UNAVAILABLE }, 503);
 } catch { return response({ error: UNAVAILABLE }, 503); }
}
