#!/usr/bin/env node
// Read-only admin dashboard for the Comiida content pipeline.
// Serves at /admin (proxied by Apache). Auth via secret token (?key= or cookie).
// Node built-ins only — no external deps.
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
import { dirname, resolve, join, extname } from "node:path";
import { fileURLToPath } from "node:url";
import { readFrontmatter, fmString, fmArray, loadJson, todayInTz, addDays, slugify } from "../scripts/lib/util.mjs";
import { addEntry } from "../scripts/lib/calendar.mjs";

const HERE = dirname(fileURLToPath(import.meta.url));
const PIPELINE = resolve(HERE, "..");

// --- env (.env) ---
function loadEnv() {
  const p = join(PIPELINE, ".env");
  if (!existsSync(p)) return;
  for (const line of readFileSync(p, "utf8").split("\n")) {
    const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
    if (m && !(m[1] in process.env)) process.env[m[1]] = m[2];
  }
}
loadEnv();

const cfg = loadJson(join(PIPELINE, "config.json"));
const root = cfg.paths.projectRoot;
const TOKEN = process.env.ADMIN_TOKEN || "";
const PORT = parseInt(process.env.ADMIN_PORT, 10) || 3010;

if (!TOKEN) {
  console.error("[admin] ADMIN_TOKEN not set in .env — refusing to start.");
  process.exit(1);
}

// --- data gathering ---
const draftsDir = () => join(root, cfg.paths.draftsDir);
const blogDir = () => join(root, cfg.paths.blogContentDir);

function calendar() {
  const p = join(root, cfg.paths.calendar);
  return existsSync(p) ? loadJson(p) : [];
}
function isPublished(slug) {
  return existsSync(join(blogDir(), slug, "index.mdx"));
}
function draftSlugs() {
  const d = draftsDir();
  return existsSync(d)
    ? readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).map((x) => x.name)
    : [];
}
function draftMeta(slug) {
  const mdx = join(draftsDir(), slug, "index.mdx");
  if (!existsSync(mdx)) return null;
  const fm = readFrontmatter(readFileSync(mdx, "utf8"));
  let seo = null;
  const seoPath = join(draftsDir(), slug, "seo-review.json");
  if (existsSync(seoPath)) {
    try {
      seo = JSON.parse(readFileSync(seoPath, "utf8"));
    } catch {
      seo = null;
    }
  }
  return {
    slug,
    title: fmString(fm, "title") || slug,
    category: fmString(fm, "category") || "",
    tags: fmArray(fm, "tags"),
    date: fmString(fm, "date") || "",
    hasCover: existsSync(join(draftsDir(), slug, "cover.jpg")),
    seo,
  };
}

// --- helpers ---
const esc = (s) =>
  String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));

const STATUS_COLORS = {
  planned: "#8a8f98",
  drafting: "#d98324",
  drafted: "#2f6feb",
  "drafted-no-image": "#b58900",
  "draft-failed": "#cb2431",
  published: "#1a7f37",
};
const badge = (status) =>
  `<span style="background:${STATUS_COLORS[status] || "#666"};color:#fff;border-radius:999px;padding:2px 10px;font-size:12px;white-space:nowrap">${esc(status)}</span>`;

const newsBadge = `<span style="background:#b5179e;color:#fff;border-radius:999px;padding:2px 10px;font-size:12px">NEWS</span>`;
const suggestedBadge = `<span style="background:#7048e8;color:#fff;border-radius:999px;padding:2px 10px;font-size:12px">SUGGESTED</span>`;
function freshNewsCount() {
  const p = join(root, "agents", "news-queue.json");
  if (!existsSync(p)) return 0;
  try {
    return JSON.parse(readFileSync(p, "utf8")).filter((i) => i.status === "fresh").length;
  } catch {
    return 0;
  }
}

const SEO_COLORS = { pass: "#1a7f37", revise: "#d98324", fail: "#cb2431" };
const seoBadge = (seo) =>
  seo
    ? `<span style="background:${SEO_COLORS[seo.verdict] || "#666"};color:#fff;border-radius:999px;padding:2px 10px;font-size:12px;white-space:nowrap">SEO ${esc(seo.verdict)} ${esc(seo.overall)}</span>`
    : `<span style="background:#aaa;color:#fff;border-radius:999px;padding:2px 10px;font-size:12px">SEO —</span>`;

const messagesPath = () => join(root, "agents", "messages.json");
function messages() {
  const p = messagesPath();
  if (!existsSync(p)) return [];
  try {
    return JSON.parse(readFileSync(p, "utf8"));
  } catch {
    return [];
  }
}

function authed(req) {
  const url = new URL(req.url, "http://x");
  const qkey = url.searchParams.get("key");
  if (qkey && qkey === TOKEN) return { ok: true, setCookie: true };
  const cookie = (req.headers.cookie || "").match(/(?:^|;\s*)admin_key=([^;]+)/);
  if (cookie && decodeURIComponent(cookie[1]) === TOKEN) return { ok: true };
  return { ok: false };
}

// --- pages ---
function dashboardHtml() {
  const today = todayInTz(cfg.editorial.timezone);
  const weekEnd = addDays(today, 6);
  const cal = calendar().slice().sort((a, b) => (a.date || "").localeCompare(b.date || ""));
  const drafts = draftSlugs().map(draftMeta).filter(Boolean);

  const inWeek = cal.filter((e) => e.date >= today && e.date <= weekEnd);
  const counts = cal.reduce((m, e) => ((m[e.status] = (m[e.status] || 0) + 1), m), {});
  const newsSlugs = new Set(cal.filter((e) => e.news).map((e) => e.slug));
  drafts.forEach((d) => (d.news = newsSlugs.has(d.slug)));

  const row = (e) => `
    <tr>
      <td style="white-space:nowrap;color:#555">${esc(e.date)}</td>
      <td><strong>${esc(e.workingTitle || e.slug)}</strong> ${e.suggested ? suggestedBadge : ""}${e.news ? " " + newsBadge : ""}<br><span style="color:#888;font-size:12px">${esc(e.primaryKeyword || "")}</span></td>
      <td><span style="color:#555;font-size:13px">${esc(e.type)}</span></td>
      <td>${badge(isPublished(e.slug) ? "published" : e.status)}</td>
      <td>${
        existsSync(join(draftsDir(), e.slug, "index.mdx"))
          ? `<a href="/admin/draft/${esc(e.slug)}">preview</a>`
          : isPublished(e.slug)
            ? `<a href="${esc(cfg.site.url)}/blog/${esc(e.slug)}/" target="_blank">live ↗</a>`
            : "—"
      }</td>
    </tr>`;

  const draftCards = drafts.length
    ? drafts
        .map(
          (d) => `
      <div style="border:1px solid #e2e4e8;border-radius:10px;padding:14px;display:flex;gap:14px;align-items:center">
        ${d.hasCover ? `<img src="/admin/cover/${esc(d.slug)}" style="width:96px;height:64px;object-fit:cover;border-radius:6px;flex:none">` : `<div style="width:96px;height:64px;background:#f0f1f3;border-radius:6px;flex:none"></div>`}
        <div style="flex:1">
          <div style="display:flex;align-items:center;gap:8px"><strong>${esc(d.title)}</strong> ${d.news ? newsBadge : ""} ${seoBadge(d.seo)}</div>
          <div style="color:#888;font-size:12px">${esc(d.date)} · ${esc(d.category)} · ${esc(d.tags.join(", "))}</div>
          ${d.seo?.blocking?.length ? `<div style="color:#cb2431;font-size:12px;margin-top:4px">⚠ ${esc(d.seo.blocking.length)} blocking: ${esc(d.seo.blocking[0])}${d.seo.blocking.length > 1 ? " …" : ""}</div>` : ""}
          <div style="margin-top:6px"><a href="/admin/draft/${esc(d.slug)}">preview</a> · <code style="font-size:12px">approve.mjs ${esc(d.slug)}</code></div>
        </div>
      </div>`
        )
        .join("")
    : `<p style="color:#888">No drafts awaiting review.</p>`;

  const countPills = Object.entries(counts)
    .map(([k, v]) => `${badge(k)} <span style="color:#555">${v}</span>`)
    .join("&nbsp;&nbsp;");

  const msgs = messages().slice().reverse();
  const msgHtml = msgs.length
    ? msgs
        .map(
          (m) => `
      <div style="border:1px solid #e2e4e8;border-radius:10px;padding:12px 14px;position:relative">
        <button onclick="deleteMsg('${esc(m.id || m.at)}')" title="Delete message" style="position:absolute;top:10px;right:10px;border:0;background:#f0f1f3;border-radius:6px;padding:3px 9px;cursor:pointer;color:#999;line-height:1">✕</button>
        <div style="font-size:13px;padding-right:36px"><strong>${esc(m.name)}</strong> <span style="color:#888">&lt;${esc(m.email)}&gt;</span> <span style="color:#aaa">· ${esc((m.at || "").slice(0, 16).replace("T", " "))}</span></div>
        <div style="margin-top:6px;white-space:pre-wrap">${esc(m.message)}</div>
      </div>`
        )
        .join("")
    : `<p class="muted">No messages yet.</p>`;

  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Comiida — Content Admin</title>
<style>
  body{font:15px/1.5 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#1a1a1a;margin:0;background:#fafbfc}
  .wrap{max-width:960px;margin:0 auto;padding:28px 20px 60px}
  h1{font-size:22px;margin:0 0 4px} h2{font-size:16px;margin:30px 0 12px}
  table{width:100%;border-collapse:collapse} td,th{text-align:left;padding:10px 8px;border-bottom:1px solid #eceef1;vertical-align:top}
  th{font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:#888}
  a{color:#2f6feb;text-decoration:none} a:hover{text-decoration:underline}
  .muted{color:#888;font-size:13px} .grid{display:flex;flex-direction:column;gap:10px}
  .btn{display:inline-block;background:#7048e8;color:#fff;border:0;border-radius:8px;padding:9px 16px;font-size:14px;font-weight:600;cursor:pointer}
  .btn:hover{opacity:.92}
  .overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);display:none;align-items:flex-start;justify-content:center;z-index:10}
  .overlay.open{display:flex}
  .modal{background:#fff;border-radius:12px;max-width:520px;width:calc(100% - 32px);margin-top:8vh;padding:22px;box-shadow:0 12px 40px rgba(0,0,0,.2)}
  .modal h3{margin:0 0 4px;font-size:18px} .modal label{display:block;font-size:13px;font-weight:600;margin:14px 0 5px}
  .modal input,.modal textarea{width:100%;box-sizing:border-box;border:1px solid #d6d9de;border-radius:8px;padding:9px 11px;font-size:14px;font-family:inherit}
  .modal .row2{display:flex;gap:10px;justify-content:flex-end;margin-top:18px}
  .modal .cancel{background:#eceef1;color:#333}
</style></head><body><div class="wrap">
  <div style="display:flex;align-items:center;justify-content:space-between;gap:12px">
    <h1>Comiida — Content Admin</h1>
    <button class="btn" onclick="document.getElementById('suggestOverlay').classList.add('open')">+ Suggest an article</button>
  </div>
  <div class="muted">Today ${esc(today)} (${esc(cfg.editorial.timezone)}) · ${countPills} · 📰 news queue: ${freshNewsCount()} fresh</div>

  <div class="overlay" id="suggestOverlay">
    <div class="modal">
      <h3>Suggest an article</h3>
      <div class="muted">Added as a top-priority topic — the writer researches & drafts it next.</div>
      <form id="suggestForm">
        <label>Title *</label>
        <input name="title" required maxlength="160" placeholder="e.g. The best late-night eats in Laureles" />
        <label>Brief description</label>
        <textarea name="description" rows="3" maxlength="600" placeholder="What angle / what to cover?"></textarea>
        <label>Prompt / instructions for the agent (optional)</label>
        <textarea name="instructions" rows="3" maxlength="4000" placeholder="e.g. Focus on vegan spots in Laureles; research the new Provenza openings; compare prices and hours"></textarea>
        <label>Draft (optional)</label>
        <textarea name="draft" rows="6" maxlength="20000" placeholder="Paste a draft or rough notes — the agent will build on, fact-check, and expand it."></textarea>
        <label>Source link (optional)</label>
        <input name="source" type="url" placeholder="https://… where you saw the idea" />
        <div class="row2">
          <button type="button" class="btn cancel" onclick="document.getElementById('suggestOverlay').classList.remove('open')">Cancel</button>
          <button type="submit" class="btn" id="suggestSubmit">Add suggestion</button>
        </div>
      </form>
    </div>
  </div>
  <script>
    document.getElementById('suggestForm').addEventListener('submit', async (e) => {
      e.preventDefault();
      const btn = document.getElementById('suggestSubmit');
      const f = e.target;
      const payload = { title: f.title.value.trim(), description: f.description.value.trim(), instructions: f.instructions.value.trim(), draft: f.draft.value.trim(), source: f.source.value.trim() };
      if (!payload.title) return;
      btn.disabled = true; btn.textContent = 'Adding…';
      try {
        const r = await fetch('/admin/suggest', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
        if (!r.ok) throw new Error((await r.json()).error || 'failed');
        location.reload();
      } catch (err) {
        btn.disabled = false; btn.textContent = 'Add suggestion';
        alert('Could not add suggestion: ' + err.message);
      }
    });
  </script>

  <h2>📬 Messages (${msgs.length})</h2>
  <div class="grid">${msgHtml}</div>
  <script>
    window.deleteMsg = async (id) => {
      if (!confirm('Delete this message?')) return;
      try {
        const r = await fetch('/admin/message-delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) });
        if (!r.ok) throw new Error();
        location.reload();
      } catch { alert('Could not delete the message.'); }
    };
  </script>

  <h2>This week (${esc(today)} → ${esc(weekEnd)})</h2>
  ${inWeek.length ? `<table><tr><th>Date</th><th>Topic</th><th>Type</th><th>Status</th><th></th></tr>${inWeek.map(row).join("")}</table>` : `<p class="muted">Nothing scheduled this week. Run research.mjs to plan more.</p>`}

  <h2>Drafts awaiting review (${drafts.length})</h2>
  <div class="grid">${draftCards}</div>

  <h2>Full calendar (${cal.length})</h2>
  <table><tr><th>Date</th><th>Topic</th><th>Type</th><th>Status</th><th></th></tr>${cal.map(row).join("")}</table>
</div></body></html>`;
}

function draftDetailHtml(slug) {
  const dir = join(draftsDir(), slug);
  const mdxPath = join(dir, "index.mdx");
  if (!existsSync(mdxPath)) return null;
  const raw = readFileSync(mdxPath, "utf8");
  const fm = readFrontmatter(raw);
  const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, "");
  const sources = existsSync(join(dir, "sources.json"))
    ? readFileSync(join(dir, "sources.json"), "utf8")
    : "(none)";
  let seo = null;
  if (existsSync(join(dir, "seo-review.json"))) {
    try {
      seo = JSON.parse(readFileSync(join(dir, "seo-review.json"), "utf8"));
    } catch {
      seo = null;
    }
  }
  const seoSection = seo
    ? `<h2>SEO audit ${seoBadge(seo)}</h2>
       <p class="muted">${esc(seo.summary || "")}</p>
       <ul>${Object.entries(seo.dimensions || {})
         .map(([k, v]) => `<li><strong>${esc(k)}</strong>: ${esc(v.score)}${v.issues?.length ? " — " + esc(v.issues.join("; ")) : ""}</li>`)
         .join("")}</ul>
       ${seo.blocking?.length ? `<p style="color:#cb2431"><strong>Blocking:</strong></p><ul>${seo.blocking.map((b) => `<li>${esc(b)}</li>`).join("")}</ul>` : ""}
       ${seo.topFixes?.length ? `<p><strong>Top fixes:</strong></p><ul>${seo.topFixes.map((f) => `<li>[${esc(f.severity)}/${esc(f.area)}] ${esc(f.fix)}</li>`).join("")}</ul>` : ""}`
    : `<h2>SEO audit ${seoBadge(null)}</h2><p class="muted">No audit yet. Run <code>./run.sh write-daily.mjs</code> (auto-audits) or <code>node scripts/seo-review.mjs ${esc(slug)}</code>.</p>`;
  const hasCover = existsSync(join(dir, "cover.jpg"));
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(fmString(fm, "title") || slug)} — draft</title>
<style>
  body{font:15px/1.6 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#1a1a1a;margin:0;background:#fafbfc}
  .wrap{max-width:760px;margin:0 auto;padding:24px 20px 60px}
  pre{background:#f4f5f7;border:1px solid #e6e8eb;border-radius:8px;padding:12px;overflow:auto;font-size:12.5px;white-space:pre-wrap}
  a{color:#2f6feb} img{max-width:100%;border-radius:8px}
  h1{font-size:21px}
</style></head><body><div class="wrap">
  <p><a href="/admin">← back to dashboard</a></p>
  ${hasCover ? `<img src="/admin/cover/${esc(slug)}">` : ""}
  ${seoSection}
  <h2>Frontmatter</h2><pre>${esc(fm)}</pre>
  <h2>Body (raw MDX)</h2><pre>${esc(body)}</pre>
  <h2>sources.json</h2><pre>${esc(sources)}</pre>
  <p class="muted">To publish: <code>./run.sh approve.mjs ${esc(slug)}</code></p>
</div></body></html>`;
}

// --- server ---
const send = (res, code, body, type = "text/html; charset=utf-8", extra = {}) => {
  res.writeHead(code, { "content-type": type, "cache-control": "no-store", ...extra });
  res.end(body);
};

const server = createServer((req, res) => {
  const url = new URL(req.url, "http://x");
  const path = url.pathname.replace(/\/+$/, "") || "/admin";

  // PUBLIC (no auth): contact form submission → saved to messages.json, viewed in /admin.
  if (req.method === "POST" && path === "/contact-submit") {
    let body = "";
    req.on("data", (c) => {
      body += c;
      if (body.length > 20000) req.destroy(); // basic size guard
    });
    req.on("end", () => {
      try {
        const d = JSON.parse(body || "{}");
        if (d.website) return send(res, 200, JSON.stringify({ ok: true }), "application/json"); // honeypot → silently drop
        const name = String(d.name || "").trim().slice(0, 120);
        const email = String(d.email || "").trim().slice(0, 160);
        const message = String(d.message || "").trim().slice(0, 4000);
        if (!name || !message || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
          return send(res, 400, JSON.stringify({ error: "Please fill in your name, a valid email, and a message." }), "application/json");
        }
        const arr = messages();
        arr.push({ id: randomUUID(), at: new Date().toISOString(), name, email, message });
        writeFileSync(messagesPath(), JSON.stringify(arr, null, 2));
        send(res, 200, JSON.stringify({ ok: true }), "application/json");
      } catch (e) {
        send(res, 500, JSON.stringify({ error: e.message }), "application/json");
      }
    });
    return;
  }

  const auth = authed(req);
  if (!auth.ok) {
    return send(res, 401, "<h1>401</h1><p>Add ?key=YOUR_TOKEN to the URL.</p>");
  }
  const cookieHeader = auth.setCookie
    ? { "set-cookie": `admin_key=${encodeURIComponent(TOKEN)}; Path=/admin; HttpOnly; SameSite=Lax; Max-Age=2592000` }
    : {};

  // suggest an article (POST) — inject a top-priority calendar entry
  if (req.method === "POST" && path === "/admin/suggest") {
    let body = "";
    req.on("data", (c) => (body += c));
    req.on("end", async () => {
      try {
        const data = JSON.parse(body || "{}");
        const title = String(data.title || "").trim();
        if (!title) return send(res, 400, JSON.stringify({ error: "Title is required" }), "application/json");
        const description = String(data.description || "").trim();
        const source = String(data.source || "").trim();
        const instructions = String(data.instructions || "").trim().slice(0, 4000);
        const draft = String(data.draft || "").trim().slice(0, 20000);
        const today = todayInTz(cfg.editorial.timezone);
        const entry = {
          date: today,
          slug: slugify(title),
          workingTitle: title,
          type: "guide",
          primaryKeyword: title,
          secondaryKeywords: [],
          searchIntent: "informational",
          audienceAngle: description,
          sourceHints: source ? [source] : [],
          eeatAngle: "User-suggested topic; research thoroughly and cite every claim.",
          suggested: true,
          status: "planned",
        };
        if (instructions) entry.instructions = instructions;
        if (draft) entry.draft = draft;
        await addEntry(join(root, cfg.paths.calendar), entry);
        send(res, 200, JSON.stringify({ ok: true, slug: entry.slug }), "application/json", cookieHeader);
      } catch (e) {
        send(res, 500, JSON.stringify({ error: e.message }), "application/json");
      }
    });
    return;
  }

  // delete a message (token-gated)
  if (req.method === "POST" && path === "/admin/message-delete") {
    let body = "";
    req.on("data", (c) => (body += c));
    req.on("end", () => {
      try {
        const { id } = JSON.parse(body || "{}");
        const arr = messages().filter((m) => (m.id || m.at) !== id);
        writeFileSync(messagesPath(), JSON.stringify(arr, null, 2));
        send(res, 200, JSON.stringify({ ok: true }), "application/json", cookieHeader);
      } catch (e) {
        send(res, 500, JSON.stringify({ error: e.message }), "application/json");
      }
    });
    return;
  }

  // cover image
  const cover = path.match(/^\/admin\/cover\/([a-z0-9-]+)$/i);
  if (cover) {
    const f = join(draftsDir(), cover[1], "cover.jpg");
    if (!existsSync(f)) return send(res, 404, "not found", "text/plain");
    return send(res, 200, readFileSync(f), "image/jpeg", cookieHeader);
  }

  // draft detail
  const draft = path.match(/^\/admin\/draft\/([a-z0-9-]+)$/i);
  if (draft) {
    const html = draftDetailHtml(draft[1]);
    return html ? send(res, 200, html, "text/html; charset=utf-8", cookieHeader) : send(res, 404, "<h1>404</h1>");
  }

  // dashboard
  if (path === "/admin" || path === "/admin/") {
    return send(res, 200, dashboardHtml(), "text/html; charset=utf-8", cookieHeader);
  }

  return send(res, 404, "<h1>404</h1>");
});

server.listen(PORT, "127.0.0.1", () => console.log(`[admin] listening on 127.0.0.1:${PORT}`));
