import "dotenv/config";
import path from "node:path";
import { fileURLToPath } from "node:url";
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import multer from "multer";
import rateLimit from "express-rate-limit";

import { setupDatabase } from "./lib/db.js";
import { askKnowledgeBase, streamKnowledgeBase } from "./lib/openai.js";
import { ingestStreams, ingestText, listDocuments, deleteDocument } from "./lib/knowledge.js";
import {
  createTenant, getTenantById, getTenantByKey, getTenantByEmail, authenticate,
  updateSettings, updatePassword, listAllTenants, deleteTenant,
  editTenant, setStatus as setTenantStatus, ensureVectorStore, clientFor, smtpFor, notifyEmailFor,
} from "./lib/tenants.js";
import {
  createConversation, getConversation, getConversationWithMessages, addMessage,
  messagesAfter, setPreviousResponseId, setMode, requestHuman, setStatus,
  clearAgentUnread, listConversations,
} from "./lib/store.js";
import {
  issueSession, clearSession, requireAuth, sessionTenantId,
  issueSuperSession, requireSuper, sessionIsSuper, issueResetToken, verifyResetToken,
} from "./lib/auth.js";
import { isValidEmail, createOtp, checkOtp, issueChatToken, verifyChatToken } from "./lib/verify.js";
import { sendVerificationCode, sendLeadNotification, sendPasswordReset, sendMail } from "./lib/mailer.js";
import crypto from "node:crypto";
import { crawlSite, pagesToText } from "./lib/crawl.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = process.env.PORT || 8000;
const EXPOSE_CODE_ON_FAILURE = (process.env.EXPOSE_CODE_ON_FAILURE || "false").toLowerCase() === "true";
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL || "";
const SUPERADMIN_PASSWORD = process.env.SUPERADMIN_PASSWORD || "";
const BASE_URL = process.env.PUBLIC_URL || "http://localhost:" + PORT;

function safeEqual(a, b) {
  const ab = Buffer.from(String(a)), bb = Buffer.from(String(b));
  return ab.length === bb.length && crypto.timingSafeEqual(ab, bb);
}

const app = express();
app.use(express.json({ limit: "32kb" }));
app.use(cookieParser());
// Widget endpoints are embedded cross-origin on customer sites → reflect origin.
app.use(cors({ origin: true, credentials: true }));

// ── Rate limiters ───────────────────────────────────────────────────────────
const chatLimiter = rateLimit({ windowMs: 60 * 1000, max: 30, standardHeaders: true, legacyHeaders: false, message: { error: "Too many messages. Please slow down." } });
const verifyLimiter = rateLimit({ windowMs: 10 * 60 * 1000, max: 12, standardHeaders: true, legacyHeaders: false, message: { error: "Too many requests. Please try again later." } });
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20, standardHeaders: true, legacyHeaders: false, message: { error: "Too many attempts. Try again later." } });

// ── Widget: per-tenant config ───────────────────────────────────────────────
app.get("/api/widget-config", async (req, res) => {
  const t = await getTenantByKey((req.query.t || "").toString());
  if (!t) return res.status(404).json({ error: "Unknown widget key" });
  res.json({
    name: t.settings.assistantName || t.name,
    greeting: t.settings.greeting,
    accentColor: t.settings.accentColor,
  });
});

app.get("/api/health", (_req, res) => res.json({ ok: true, model: process.env.OPENAI_MODEL || "gpt-4o-mini" }));

// ── Chat gate: verification (tenant-scoped) ─────────────────────────────────
app.post("/api/verify/start", verifyLimiter, async (req, res) => {
  const tenant = await getTenantByKey((req.body?.tenantKey || "").toString());
  if (!tenant) return res.status(404).json({ error: "Unknown widget key" });
  if (tenant.status !== "active") return res.status(403).json({ error: "This chat is currently unavailable." });
  const name = (req.body?.name || "").toString().trim();
  const email = (req.body?.email || "").toString().trim();
  if (name.length < 1) return res.status(400).json({ error: "Please enter your name." });
  if (!isValidEmail(email)) return res.status(400).json({ error: "Please enter a valid email." });

  const { sessionId, code } = createOtp(tenant.id, name, email);
  let emailSent = false;
  try {
    const r = await sendVerificationCode(smtpFor(tenant), email, code, tenant.settings.assistantName || tenant.name);
    emailSent = r.delivered !== false;
  } catch (err) {
    console.error("verify email error:", err.message);
  }
  const resp = { ok: true, sessionId, emailSent };
  if (!emailSent && EXPOSE_CODE_ON_FAILURE) resp.devCode = code;
  res.json(resp);
});

app.post("/api/verify/confirm", verifyLimiter, async (req, res) => {
  const result = checkOtp((req.body?.sessionId || "").toString(), (req.body?.code || "").toString());
  if (!result.ok) return res.status(400).json({ error: result.reason });
  const tenant = await getTenantById(result.tenantId);
  if (!tenant) return res.status(400).json({ error: "Something went wrong. Please restart." });

  const conv = await createConversation(tenant.id, result.name, result.email);
  sendLeadNotification(smtpFor(tenant), { to: notifyEmailFor(tenant), name: result.name, email: result.email }).catch((e) => console.error("lead notify:", e.message));

  res.json({
    ok: true,
    token: issueChatToken({ tenantKey: tenant.publicKey, conversationId: conv.id, email: result.email }),
    name: result.name,
    conversationId: conv.id,
  });
});

// ── Conversation chat (AI + live human handoff) ─────────────────────────────
async function authChat(req) {
  const tok = verifyChatToken(req.body?.token || req.query?.token);
  if (!tok) return null;
  const tenant = await getTenantByKey(tok.tenantKey);
  if (!tenant) return null;
  const conv = await getConversation(req.params.id);
  if (!conv || conv.tenantId !== tenant.id || conv.email !== tok.email) return null;
  return { tenant, conv };
}

function notifyHandoff(tenant, conv) {
  const to = notifyEmailFor(tenant);
  if (!to) return;
  sendMail(smtpFor(tenant), {
    to,
    subject: `🙋 Human requested — ${conv.name} <${conv.email}>`,
    text: `${conv.name} (${conv.email}) asked to talk to a person on ${tenant.name}.\n\nOpen your inbox to join: ${BASE_URL}/admin`,
  }).catch((e) => console.error("handoff notify:", e.message));
}

const HANDOFF_RE = /\b(human|agent|representative|real person|someone|a person|support team|customer service)\b/i;
const HANDOFF_INTENT = /\b(talk|speak|connect|chat|reach|contact|want|need|get)\b/i;

app.post("/api/chat/:id/message", chatLimiter, async (req, res) => {
  const ctx = await authChat(req);
  if (!ctx) return res.status(401).json({ error: "verify" });
  const { tenant, conv } = ctx;
  if (tenant.status !== "active") return res.status(403).json({ error: "This chat is currently unavailable." });

  const message = (req.body?.message || "").toString().trim();
  if (!message) return res.status(400).json({ error: "message is required" });
  if (message.length > 2000) return res.status(400).json({ error: "message too long" });

  const userMsg = await addMessage(conv.id, "user", message);

  const wantsHuman = HANDOFF_RE.test(message) && HANDOFF_INTENT.test(message);
  if (conv.mode === "human" || wantsHuman) {
    if (wantsHuman && conv.mode !== "human") {
      await requestHuman(conv.id);
      await addMessage(conv.id, "system", "Got it — connecting you with a member of our team. Someone will reply here shortly. You can keep typing in the meantime.");
      notifyHandoff(tenant, conv);
    }
    return res.json({ queued: true, user: { id: userMsg.id, seq: userMsg.seq } });
  }

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache, no-transform");
  res.setHeader("Connection", "keep-alive");
  res.setHeader("X-Accel-Buffering", "no");
  res.flushHeaders?.();
  const send = (o) => res.write(`data: ${JSON.stringify(o)}\n\n`);
  send({ user: { id: userMsg.id, seq: userMsg.seq } });

  let acc = "";
  try {
    const vectorStoreId = await ensureVectorStore(tenant);
    await streamKnowledgeBase(
      { client: clientFor(tenant), message, vectorStoreId, systemPrompt: tenant.settings.systemPrompt, previousResponseId: conv.previousResponseId },
      {
        onSearching: () => send({ status: "searching" }),
        onDelta: (delta) => { acc += delta; send({ delta }); },
        onDone: async (responseId) => {
          if (responseId) await setPreviousResponseId(conv.id, responseId);
          const msg = await addMessage(conv.id, "assistant", acc || "Sorry, I couldn't generate a reply.");
          send({ done: true, seq: msg.seq, messageId: msg.id });
          res.end();
        },
      }
    );
  } catch (err) {
    console.error("chat stream error:", err.message);
    send({ error: "Sorry, something went wrong. Please try again." });
    res.end();
  }
});

app.get("/api/chat/:id/poll", async (req, res) => {
  const ctx = await authChat(req);
  if (!ctx) return res.status(401).json({ error: "verify" });
  const after = Number(req.query.after) || 0;
  const includeUser = req.query.all === "1";
  const msgs = await messagesAfter(ctx.conv.id, after, includeUser);
  res.json({ mode: ctx.conv.mode, status: ctx.conv.status, handoffRequested: ctx.conv.handoffRequested, messages: msgs });
});

app.post("/api/chat/:id/request-human", async (req, res) => {
  const ctx = await authChat(req);
  if (!ctx) return res.status(401).json({ error: "verify" });
  if (ctx.conv.mode !== "human") {
    await requestHuman(ctx.conv.id);
    await addMessage(ctx.conv.id, "system", "Got it — connecting you with a member of our team. Someone will reply here shortly.");
    notifyHandoff(ctx.tenant, ctx.conv);
  }
  res.json({ ok: true });
});

// ── Tenant login (accounts are created by the super-admin — no self-signup) ──
app.post("/api/login", authLimiter, async (req, res) => {
  const email = (req.body?.email || "").toString().trim();
  const password = (req.body?.password || "").toString();
  const tenant = await authenticate(email, password);
  if (!tenant) return res.status(401).json({ error: "Invalid email or password" });
  issueSession(res, tenant.id);
  res.json({ ok: true });
});

app.post("/api/logout", (_req, res) => { clearSession(res); res.json({ ok: true }); });

// ── Password reset ───────────────────────────────────────────────────────────
app.post("/api/forgot", authLimiter, async (req, res) => {
  const email = (req.body?.email || "").toString().trim();
  const tenant = await getTenantByEmail(email);
  if (tenant) {
    const token = issueResetToken(tenant.id, tenant.passwordHash);
    const link = `${BASE_URL}/reset?token=${encodeURIComponent(token)}`;
    sendPasswordReset(smtpFor(tenant), email, link).catch((e) => console.error("reset email:", e.message));
    if (EXPOSE_CODE_ON_FAILURE) return res.json({ ok: true, devLink: link });
  }
  // Always ok — don't reveal whether the email exists.
  res.json({ ok: true });
});

app.post("/api/reset", authLimiter, async (req, res) => {
  const token = (req.body?.token || "").toString();
  const password = (req.body?.password || "").toString();
  if (password.length < 6) return res.status(400).json({ error: "Password must be at least 6 characters." });
  const data = verifyResetToken(token);
  if (!data) return res.status(400).json({ error: "This reset link is invalid or expired." });
  const tenant = await getTenantById(data.tenantId);
  // hashPrefix must still match — makes the link single-use.
  if (!tenant || tenant.passwordHash.slice(0, 12) !== data.hashPrefix) {
    return res.status(400).json({ error: "This reset link has already been used." });
  }
  await updatePassword(tenant.id, password);
  res.json({ ok: true });
});

// ── Super-admin (platform owner) ─────────────────────────────────────────────
app.post("/api/superadmin/login", authLimiter, (req, res) => {
  const email = (req.body?.email || "").toString();
  const password = (req.body?.password || "").toString();
  if (!SUPERADMIN_EMAIL || !SUPERADMIN_PASSWORD) return res.status(403).json({ error: "Super-admin is not configured." });
  if (!safeEqual(email, SUPERADMIN_EMAIL) || !safeEqual(password, SUPERADMIN_PASSWORD)) {
    return res.status(401).json({ error: "Invalid credentials" });
  }
  issueSuperSession(res);
  res.json({ ok: true });
});

app.get("/api/superadmin/me", (req, res) => res.json({ authed: sessionIsSuper(req) }));
app.post("/api/superadmin/logout", (_req, res) => { clearSession(res); res.json({ ok: true }); });

app.get("/api/superadmin/tenants", requireSuper, async (_req, res) => {
  res.json({ tenants: await listAllTenants() });
});

app.post("/api/superadmin/tenants", requireSuper, async (req, res) => {
  const name = (req.body?.name || "").toString().trim();
  const siteUrl = (req.body?.siteUrl || "").toString().trim();
  const email = (req.body?.email || "").toString().trim();
  const password = (req.body?.password || "").toString();
  const openaiKey = (req.body?.openaiKey || "").toString().trim();
  if (name.length < 2) return res.status(400).json({ error: "Enter a company/site name." });
  if (!isValidEmail(email)) return res.status(400).json({ error: "Enter a valid admin email." });
  if (password.length < 6) return res.status(400).json({ error: "Password must be at least 6 characters." });
  try {
    const t = await createTenant({ name, siteUrl, email, password, openaiKey });
    res.json({ ok: true, publicKey: t.publicKey });
  } catch (e) {
    res.status(400).json({ error: e.message.includes("exists") ? e.message : "Could not create tenant." });
  }
});

app.patch("/api/superadmin/tenants/:id", requireSuper, async (req, res) => {
  const id = Number(req.params.id);
  const b = req.body || {};
  try {
    if (typeof b.status === "string" && (b.status === "active" || b.status === "paused")) await setTenantStatus(id, b.status);
    if (b.password && b.password.length < 6) return res.status(400).json({ error: "Password must be at least 6 characters." });
    if (b.email && !isValidEmail(b.email)) return res.status(400).json({ error: "Invalid email." });
    await editTenant(id, { name: b.name, siteUrl: b.siteUrl, email: b.email, password: b.password });
    res.json({ ok: true });
  } catch (e) {
    res.status(400).json({ error: e.message || "Update failed." });
  }
});

app.delete("/api/superadmin/tenants/:id", requireSuper, async (req, res) => {
  await deleteTenant(Number(req.params.id));
  res.json({ ok: true });
});

// Never leak secrets to the browser — return only whether they're configured.
function publicTenant(t) {
  const s = t.settings;
  return {
    name: t.name, publicKey: t.publicKey, siteUrl: t.siteUrl,
    adminEmail: t.adminEmail, status: t.status,
    knowledgeBaseReady: Boolean(t.vectorStoreId),
    settings: {
      assistantName: s.assistantName, greeting: s.greeting, accentColor: s.accentColor,
      systemPrompt: s.systemPrompt, notifyEmail: s.notifyEmail,
      openaiKeySet: Boolean(s.openaiKey),
      smtp: { host: s.smtp.host, port: s.smtp.port, secure: s.smtp.secure, user: s.smtp.user, from: s.smtp.from, passSet: Boolean(s.smtp.pass) },
    },
  };
}

app.get("/api/me", async (req, res) => {
  const tenantId = sessionTenantId(req);
  const t = tenantId ? await getTenantById(tenantId) : null;
  if (!t) return res.json({ authed: false });
  res.json({ authed: true, tenant: publicTenant(t) });
});

// ── Admin knowledge base (auth required, tenant-scoped) ──────────────────────
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024, files: 10 }, fileFilter: (_r, f, cb) => cb(null, f.mimetype === "application/pdf" || f.originalname.toLowerCase().endsWith(".pdf")) });

app.get("/admin/documents", requireAuth, async (req, res) => {
  try {
    const t = await getTenantById(req.tenantId);
    if (!t.vectorStoreId) return res.json({ documents: [] });
    res.json({ documents: await listDocuments(clientFor(t), t.vectorStoreId) });
  } catch (e) { console.error("list docs:", e.message); res.status(500).json({ error: "Could not list documents. Check your OpenAI key in Settings." }); }
});

app.post("/admin/upload", requireAuth, upload.array("files", 10), async (req, res) => {
  try {
    if (!req.files?.length) return res.status(400).json({ error: "No PDF uploaded." });
    const t = await getTenantById(req.tenantId);
    const vectorStoreId = await ensureVectorStore(t);
    const files = req.files.map((f) => ({ name: f.originalname, stream: f.buffer }));
    const { batch, fileCount } = await ingestStreams(clientFor(t), vectorStoreId, files);
    res.json({ ok: true, uploaded: fileCount, indexed: batch.file_counts.completed, failed: batch.file_counts.failed });
  } catch (e) { console.error("upload:", e.message); res.status(500).json({ error: "Upload failed. " + e.message }); }
});

app.post("/admin/crawl", requireAuth, async (req, res) => {
  const url = (req.body?.url || "").toString().trim();
  const maxPages = Number(req.body?.maxPages) || 0; // 0 = crawl the whole site
  let parsed;
  try { parsed = new URL(url); } catch { return res.status(400).json({ error: "Enter a full URL, e.g. https://example.com" }); }
  if (!/^https?:$/.test(parsed.protocol)) return res.status(400).json({ error: "URL must start with http:// or https://" });
  try {
    const t = await getTenantById(req.tenantId);
    const vectorStoreId = await ensureVectorStore(t);
    const pages = await crawlSite(url, maxPages);
    if (!pages.length) return res.status(400).json({ error: "No readable pages found at that URL." });
    const { batch } = await ingestText(clientFor(t), vectorStoreId, `site-${parsed.hostname}-${Date.now()}.txt`, pagesToText(pages));
    res.json({ ok: true, pages: pages.length, indexed: batch.file_counts.completed });
  } catch (e) { console.error("crawl:", e.message); res.status(500).json({ error: "Crawl failed. " + e.message }); }
});

app.delete("/admin/documents/:id", requireAuth, async (req, res) => {
  try {
    const t = await getTenantById(req.tenantId);
    await deleteDocument(clientFor(t), t.vectorStoreId, req.params.id);
    res.json({ ok: true });
  } catch (e) { console.error("delete doc:", e.message); res.status(500).json({ error: "Delete failed." }); }
});

// ── Admin conversations (auth required, tenant-scoped) ───────────────────────
async function ownedConversation(req) {
  const c = await getConversation(req.params.id);
  if (!c || c.tenantId !== req.tenantId) return null;
  return c;
}

app.get("/admin/conversations", requireAuth, async (req, res) => {
  const list = await listConversations(req.tenantId);
  res.json({ conversations: list.map((c) => ({ id: c.id, name: c.name, email: c.email, mode: c.mode, status: c.status, handoffRequested: c.handoffRequested, agentUnread: c.agentUnread, updatedAt: c.updatedAt, last: c.last })) });
});

app.get("/admin/conversations/:id", requireAuth, async (req, res) => {
  if (!(await ownedConversation(req))) return res.status(404).json({ error: "Not found" });
  await clearAgentUnread(req.params.id);
  res.json({ conversation: await getConversationWithMessages(req.params.id) });
});

app.post("/admin/conversations/:id/reply", requireAuth, async (req, res) => {
  const c = await ownedConversation(req);
  if (!c) return res.status(404).json({ error: "Not found" });
  const text = (req.body?.text || "").toString().trim();
  if (!text) return res.status(400).json({ error: "Empty message" });
  if (c.mode !== "human") { await setMode(c.id, "human"); await addMessage(c.id, "system", "You're now connected with a member of our team. 👋"); }
  const msg = await addMessage(c.id, "agent", text);
  res.json({ ok: true, message: msg });
});

app.post("/admin/conversations/:id/mode", requireAuth, async (req, res) => {
  const c = await ownedConversation(req);
  if (!c) return res.status(404).json({ error: "Not found" });
  const mode = req.body?.mode === "human" ? "human" : "ai";
  await setMode(c.id, mode);
  await addMessage(c.id, "system", mode === "human" ? "You're now connected with a member of our team. 👋" : "You're back with our automated assistant.");
  res.json({ ok: true, mode });
});

app.post("/admin/conversations/:id/close", requireAuth, async (req, res) => {
  const c = await ownedConversation(req);
  if (!c) return res.status(404).json({ error: "Not found" });
  if (c.mode !== "ai") await setMode(c.id, "ai");
  await setStatus(c.id, "closed");
  res.json({ ok: true });
});

// ── Admin settings (branding, OpenAI key, SMTP, notify email) ────────────────
app.post("/admin/settings", requireAuth, async (req, res) => {
  const b = req.body || {};
  const patch = {};
  for (const k of ["assistantName", "greeting", "accentColor", "systemPrompt", "notifyEmail"]) {
    if (typeof b[k] === "string") patch[k] = b[k].slice(0, 8000);
  }
  // Secrets: only overwrite when a non-empty value is provided.
  if (typeof b.openaiKey === "string" && b.openaiKey.trim()) patch.openaiKey = b.openaiKey.trim();
  if (b.smtp && typeof b.smtp === "object") {
    const smtp = {};
    for (const k of ["host", "user", "from"]) if (typeof b.smtp[k] === "string") smtp[k] = b.smtp[k].trim();
    if (b.smtp.port != null) smtp.port = Number(b.smtp.port) || 587;
    if (b.smtp.secure != null) smtp.secure = !!b.smtp.secure;
    if (typeof b.smtp.pass === "string" && b.smtp.pass) smtp.pass = b.smtp.pass;
    patch.smtp = smtp;
  }
  await updateSettings(req.tenantId, patch);
  const t = await getTenantById(req.tenantId);
  res.json({ ok: true, tenant: publicTenant(t) });
});

// ── Static: widget.js, dashboard, demo ──────────────────────────────────────
app.get("/admin", (_req, res) => res.sendFile(path.join(__dirname, "public", "admin.html")));
app.get("/superadmin", (_req, res) => res.sendFile(path.join(__dirname, "public", "superadmin.html")));
app.get("/reset", (_req, res) => res.sendFile(path.join(__dirname, "public", "reset.html")));
app.use(express.static(path.join(__dirname, "public")));

setupDatabase()
  .then(() => {
    app.listen(PORT, () => {
      console.log(`Chat widget SaaS running on http://localhost:${PORT}`);
      console.log(`  Dashboard: http://localhost:${PORT}/admin`);
      console.log(`  Demo:      http://localhost:${PORT}/test.html?t=YOUR_WIDGET_KEY`);
    });
  })
  .catch((e) => { console.error("DB setup failed:", e.message); process.exit(1); });
