import { Router, type IRouter } from "express";
import { getOpenAI, SUPPORT_SYSTEM_PROMPT } from "../lib/openai";
import { logger } from "../lib/logger";

const router: IRouter = Router();

const MAX_MESSAGES = 20;
const MAX_CONTENT_LENGTH = 2000;
const STREAM_TIMEOUT_MS = 30_000;

// --- Rate limiting (dependency-free, in-memory) ---------------------------
// Protects the owner's OpenAI credits: this endpoint is public, so without a
// throttle it could be scripted to burn quota / cause an economic DoS.
const WINDOW_MS = 60_000;
const PER_IP_MAX = 12; // requests per IP per window
const GLOBAL_MAX = 120; // total requests per window (backstop)

const ipHits = new Map<string, number[]>();
let globalHits: number[] = [];

function pruneOld(times: number[], now: number): number[] {
  return times.filter((t) => now - t < WINDOW_MS);
}

function checkRateLimit(ip: string): { ok: boolean; retryAfter: number } {
  const now = Date.now();

  globalHits = pruneOld(globalHits, now);
  const ipTimes = pruneOld(ipHits.get(ip) ?? [], now);

  if (globalHits.length >= GLOBAL_MAX || ipTimes.length >= PER_IP_MAX) {
    const oldest = ipTimes.length >= PER_IP_MAX ? ipTimes[0] : globalHits[0];
    const retryAfter = Math.max(1, Math.ceil((WINDOW_MS - (now - oldest)) / 1000));
    if (ipTimes.length > 0) ipHits.set(ip, ipTimes);
    return { ok: false, retryAfter };
  }

  ipTimes.push(now);
  globalHits.push(now);
  ipHits.set(ip, ipTimes);

  // Light sweep to keep the map bounded.
  if (ipHits.size > 5000) {
    for (const [key, times] of ipHits) {
      const fresh = pruneOld(times, now);
      if (fresh.length === 0) ipHits.delete(key);
      else ipHits.set(key, fresh);
    }
  }

  return { ok: true, retryAfter: 0 };
}

type ChatMessage = { role: "user" | "assistant"; content: string };

function parseMessages(body: unknown): ChatMessage[] | null {
  if (typeof body !== "object" || body === null) return null;
  const raw = (body as { messages?: unknown }).messages;
  if (!Array.isArray(raw) || raw.length === 0) return null;

  const messages: ChatMessage[] = [];
  for (const item of raw) {
    if (typeof item !== "object" || item === null) return null;
    const role = (item as { role?: unknown }).role;
    const content = (item as { content?: unknown }).content;
    if (role !== "user" && role !== "assistant") return null;
    if (typeof content !== "string") return null;
    const trimmed = content.trim();
    if (trimmed.length === 0) return null;
    messages.push({ role, content: trimmed.slice(0, MAX_CONTENT_LENGTH) });
  }

  return messages.slice(-MAX_MESSAGES);
}

router.post("/chat", async (req, res): Promise<void> => {
  const ip = req.ip ?? "unknown";
  const { ok, retryAfter } = checkRateLimit(ip);
  if (!ok) {
    res.setHeader("Retry-After", String(retryAfter));
    res.status(429).json({ error: "Too many requests. Please slow down and try again shortly." });
    return;
  }

  const messages = parseMessages(req.body);
  if (!messages) {
    res.status(400).json({ error: "Invalid request: expected a non-empty messages array." });
    return;
  }

  const openai = getOpenAI();
  if (!openai) {
    res.status(503).json({ error: "Chat assistant is not configured." });
    return;
  }

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
  res.flushHeaders?.();

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS);
  // If the client disconnects, stop the upstream stream.
  req.on("close", () => controller.abort());

  try {
    const stream = await openai.chat.completions.create(
      {
        model: "gpt-4o-mini",
        max_tokens: 600,
        messages: [
          { role: "system", content: SUPPORT_SYSTEM_PROMPT },
          ...messages,
        ],
        stream: true,
      },
      { signal: controller.signal },
    );

    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content;
      if (content) {
        res.write(`data: ${JSON.stringify({ content })}\n\n`);
      }
    }

    res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
    res.end();
  } catch (err) {
    const status = (err as { status?: number }).status;
    const code = (err as { code?: string }).code;
    logger.error({ err, status, code }, "[chat] OpenAI request failed");

    if (!res.headersSent) {
      res.status(502).json({ error: "Chat assistant failed to respond." });
      return;
    }
    res.write(`data: ${JSON.stringify({ error: "The assistant ran into a problem. Please try again." })}\n\n`);
    res.end();
  } finally {
    clearTimeout(timeout);
  }
});

export default router;
