const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const express = require("express");
const session = require("express-session");
const bcrypt = require("bcryptjs");
const multer = require("multer");
const sharp = require("sharp");
const nodemailer = require("nodemailer");
const archiver = require("archiver");
const config = require("./config");
const {
  db,
  setSetting,
  getSetting,
  allSettings,
  ensureCategory,
  saveRevision,
  audit
} = require("./database");
const {
  escapeHtml,
  cleanRichText,
  makeSlug,
  token,
  now,
  bytes,
  checkbox,
  listFromText,
  safeJson
} = require("./utils");
const {
  publicLayout,
  adminLayout,
  authLayout,
  setting,
  button,
  itemCard,
  serviceCard,
  contactForm,
  sectionHtml,
  mediaOption,
  mediaGrid
} = require("./render");

const app = express();
app.set("trust proxy", 1);

class SQLiteSessionStore extends session.Store {
  get(sid, cb) {
    try {
      const row = db.prepare("SELECT sess FROM sessions WHERE sid = ? AND expired_at > ?").get(sid, Date.now());
      cb(null, row ? JSON.parse(row.sess) : null);
    } catch (error) {
      cb(error);
    }
  }
  set(sid, sess, cb) {
    try {
      const maxAge = sess.cookie?.maxAge || 1000 * 60 * 60 * 8;
      db.prepare(`
        INSERT INTO sessions (sid, sess, expired_at)
        VALUES (?, ?, ?)
        ON CONFLICT(sid) DO UPDATE SET sess = excluded.sess, expired_at = excluded.expired_at
      `).run(sid, JSON.stringify(sess), Date.now() + maxAge);
      cb(null);
    } catch (error) {
      cb(error);
    }
  }
  destroy(sid, cb) {
    try {
      db.prepare("DELETE FROM sessions WHERE sid = ?").run(sid);
      cb(null);
    } catch (error) {
      cb(error);
    }
  }
}

app.use(express.urlencoded({ extended: true, limit: "1mb" }));
app.use(express.json({ limit: "1mb" }));
app.use(session({
  store: new SQLiteSessionStore(),
  name: "dws.sid",
  secret: config.sessionSecret,
  resave: false,
  saveUninitialized: false,
  rolling: true,
  cookie: {
    httpOnly: true,
    sameSite: "lax",
    secure: config.production,
    maxAge: 1000 * 60 * 60 * 8
  }
}));
app.use((req, res, next) => {
  if (!req.session.csrfToken) req.session.csrfToken = token(16);
  res.locals.csrfToken = req.session.csrfToken;
  next();
});
app.use((req, res, next) => {
  if (["POST", "PUT", "PATCH", "DELETE"].includes(req.method)) {
    const submitted = req.body?._csrf || req.headers["x-csrf-token"];
    if (!submitted || submitted !== req.session.csrfToken) return res.status(403).send("Security check failed. Please go back and try again.");
  }
  next();
});
app.use("/css", express.static(path.join(config.root, "public/css"), { maxAge: "7d" }));
app.use("/js", express.static(path.join(config.root, "public/js"), { maxAge: "7d" }));
app.use("/uploads", express.static(config.uploadDir, { maxAge: "30d", fallthrough: true }));

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 8 * 1024 * 1024, files: 10 },
  fileFilter(req, file, cb) {
    const ok = ["image/jpeg", "image/png", "image/webp"].includes(file.mimetype);
    cb(ok ? null : new Error("Only JPG, PNG, and WebP images are allowed."), ok);
  }
});

function adminCount() {
  return db.prepare("SELECT COUNT(*) AS count FROM administrators").get().count;
}

function currentAdmin(req) {
  if (!req.session.adminId) return null;
  return db.prepare("SELECT id, name, email, role FROM administrators WHERE id = ?").get(req.session.adminId);
}

function requireAdmin(req, res, next) {
  const admin = currentAdmin(req);
  if (!admin) return res.redirect("/admin/login");
  req.admin = admin;
  next();
}

function navLinks() {
  return db.prepare("SELECT * FROM navigation WHERE is_published = 1 ORDER BY display_order, id").all();
}

function mediaRows() {
  return db.prepare("SELECT * FROM media ORDER BY created_at DESC").all();
}

function pageBySlug(slug) {
  return db.prepare("SELECT * FROM pages WHERE slug = ?").get(slug);
}

function pageSections(pageId, includeHidden = false) {
  return db.prepare(`
    SELECT ps.*, m.url image_url, m.alt_text image_alt, bm.url background_url
    FROM page_sections ps
    LEFT JOIN media m ON m.id = ps.image_id
    LEFT JOIN media bm ON bm.id = ps.background_image_id
    WHERE ps.page_id = ? ${includeHidden ? "" : "AND ps.is_published = 1"}
    ORDER BY ps.display_order, ps.id
  `).all(pageId);
}

function publicItems(where = "", params = []) {
  return db.prepare(`
    SELECT i.*, c.name category_name, m.url image_url, m.alt_text image_alt
    FROM items i
    LEFT JOIN categories c ON c.id = i.category_id
    LEFT JOIN media m ON m.id = i.main_image_id
    WHERE i.is_published = 1 AND i.is_archived = 0 ${where}
    ORDER BY i.name COLLATE NOCASE
  `).all(...params);
}

function publicServices() {
  return db.prepare(`
    SELECT s.*, m.url image_url, m.alt_text image_alt
    FROM services s
    LEFT JOIN media m ON m.id = s.image_id
    WHERE s.is_published = 1
    ORDER BY s.display_order, s.name
  `).all();
}

function renderPublic(req, res, opts) {
  const settings = allSettings();
  if (maintenanceActive(req, settings)) {
    return res.status(503).send(publicLayout({
      title: "Website Maintenance",
      description: "The website is temporarily in maintenance mode.",
      settings,
      nav: navLinks(),
      csrfToken: res.locals.csrfToken,
      currentPath: req.path,
      body: maintenancePage(settings)
    }));
  }
  res.send(publicLayout({
    ...opts,
    settings,
    nav: navLinks(),
    csrfToken: res.locals.csrfToken,
    currentPath: req.path
  }));
}

function renderAdmin(req, res, title, active, body) {
  res.send(adminLayout({ title, active, body, admin: req.admin, csrfToken: res.locals.csrfToken }));
}

function maintenanceActive(req, settings) {
  return setting(settings, "maintenance.enabled", "false") === "true" && !req.session.adminId && !req.path.startsWith("/admin") && req.path !== "/healthz";
}

function maintenancePage(settings) {
  const artwork = setting(settings, "maintenance.artwork", "");
  return `<section class="maintenance">
    ${artwork ? `<img src="${escapeHtml(artwork)}" alt="">` : ""}
    <p class="eyebrow">Website maintenance</p>
    <h1>${escapeHtml(setting(settings, "business.name", "Debbie Windler Seamstress"))}</h1>
    <p>${escapeHtml(setting(settings, "maintenance.message", "The website is getting a careful refresh. Please check back soon."))}</p>
    ${setting(settings, "maintenance.return_note", "") ? `<p>${escapeHtml(setting(settings, "maintenance.return_note"))}</p>` : ""}
  </section>`;
}

function ipHash(req) {
  return crypto.createHash("sha256").update(`${req.ip}|${config.sessionSecret}`).digest("hex");
}

function rateLimit(key, limit, windowMs) {
  const resetAt = Date.now() + windowMs;
  const row = db.prepare("SELECT * FROM rate_limits WHERE key = ?").get(key);
  if (!row || row.reset_at < Date.now()) {
    db.prepare("INSERT INTO rate_limits (key, count, reset_at) VALUES (?, 1, ?) ON CONFLICT(key) DO UPDATE SET count = 1, reset_at = excluded.reset_at").run(key, resetAt);
    return true;
  }
  if (row.count >= limit) return false;
  db.prepare("UPDATE rate_limits SET count = count + 1 WHERE key = ?").run(key);
  return true;
}

async function storeImage(file, { title = "", alt = "", category = "" } = {}) {
  if (!file) return null;
  const metadata = await sharp(file.buffer, { failOn: "error" }).metadata();
  if (!["jpeg", "png", "webp"].includes(metadata.format)) throw new Error("That image format is not allowed.");
  const base = `${Date.now()}-${token(4)}`;
  const webName = `${base}.webp`;
  const thumbName = `${base}-thumb.webp`;
  const webPath = path.join(config.uploadDir, webName);
  const thumbPath = path.join(config.uploadDir, thumbName);
  await sharp(file.buffer).rotate().resize({ width: 1800, withoutEnlargement: true }).webp({ quality: 82 }).toFile(webPath);
  await sharp(file.buffer).rotate().resize(420, 300, { fit: "cover" }).webp({ quality: 78 }).toFile(thumbPath);
  const finalMeta = await sharp(webPath).metadata();
  const created = now();
  const result = db.prepare(`
    INSERT INTO media (file_name, original_name, title, alt_text, category, mime_type, size_bytes, width, height, url, thumb_url, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, 'image/webp', ?, ?, ?, ?, ?, ?, ?)
  `).run(webName, file.originalname, title || file.originalname, alt, category, fs.statSync(webPath).size, finalMeta.width, finalMeta.height, `/uploads/${webName}`, `/uploads/${thumbName}`, created, created);
  return db.prepare("SELECT * FROM media WHERE id = ?").get(result.lastInsertRowid);
}

async function notifyOwner(message) {
  const ownerEmail = config.smtp.ownerEmail || getSetting("business.email", "");
  if (!config.smtp.host || !ownerEmail) return { status: "not_configured", error: "" };
  try {
    const transporter = nodemailer.createTransport({
      host: config.smtp.host,
      port: config.smtp.port,
      secure: config.smtp.secure,
      auth: config.smtp.user ? { user: config.smtp.user, pass: config.smtp.pass } : undefined
    });
    await transporter.sendMail({
      from: config.smtp.from,
      to: ownerEmail,
      replyTo: message.email,
      subject: `Website inquiry: ${message.subject}`,
      text: [
        `Name: ${message.name}`,
        `Email: ${message.email}`,
        `Phone: ${message.phone || ""}`,
        `Preferred contact: ${message.preferred_contact || ""}`,
        `Related: ${message.related || ""}`,
        `Source: ${message.source_page || ""}`,
        "",
        message.message
      ].join("\n")
    });
    return { status: "sent", error: "" };
  } catch (error) {
    return { status: "failed", error: error.message.slice(0, 500) };
  }
}

app.get("/healthz", (req, res) => res.json({ ok: true, time: now() }));

app.get("/", (req, res) => {
  const settings = allSettings();
  const featured = publicItems("AND (i.featured = 1 OR i.newly_added = 1 OR i.on_sale = 1)").slice(0, 6);
  const home = pageBySlug("home");
  const sections = pageSections(home.id);
  const overlay = setting(settings, "home.hero.overlay", "light");
  const body = `
    <section class="hero ${overlay}" style="background-image:url('${escapeHtml(setting(settings, "home.hero.image", ""))}');background-position:${escapeHtml(setting(settings, "home.hero.position", "center"))};background-size:${escapeHtml(setting(settings, "home.hero.size", "cover"))};--overlay-opacity:${escapeHtml(setting(settings, "home.hero.overlay_opacity", "0.28"))};color:${escapeHtml(setting(settings, "home.hero.text_color", "#2d2522"))}">
      <div class="hero-inner">
        <p class="eyebrow">${escapeHtml(setting(settings, "business.tagline", ""))}</p>
        <h1>${escapeHtml(setting(settings, "home.hero.title", "Debbie Windler Seamstress"))}</h1>
        <p>${escapeHtml(setting(settings, "home.hero.subtitle", ""))}</p>
        <p class="hero-contact">${escapeHtml(setting(settings, "home.hero.contact", ""))}</p>
        <div class="button-row">
          ${button(setting(settings, "home.hero.button1_label"), setting(settings, "home.hero.button1_url"))}
          ${button(setting(settings, "home.hero.button2_label"), setting(settings, "home.hero.button2_url"), "button secondary")}
        </div>
      </div>
    </section>
    <section class="showcase" style="background:${escapeHtml(setting(settings, "home.highlight.background_color", "#f4e8df"))}">
      <div class="section-heading">
        <p class="eyebrow">Showcase</p>
        <h2>${escapeHtml(setting(settings, "home.highlight.title", "Featured and Newly Added"))}</h2>
        <p>${escapeHtml(setting(settings, "home.highlight.intro", ""))}</p>
      </div>
      <div class="cards-grid">${featured.map(itemCard).join("")}</div>
    </section>
    ${sections.map(sectionHtml).join("")}
  `;
  res.send(publicLayout({
    title: setting(settings, "site.title", "Debbie Windler Seamstress"),
    description: setting(settings, "site.description", ""),
    settings,
    nav: navLinks(),
    csrfToken: res.locals.csrfToken,
    currentPath: req.path,
    body
  }));
});

app.get("/items", (req, res) => {
  const settings = allSettings();
  const q = String(req.query.q || "").trim();
  const category = String(req.query.category || "").trim();
  const availability = String(req.query.availability || "").trim();
  const where = [];
  const params = [];
  if (q) {
    where.push("AND (i.name LIKE ? OR i.short_description LIKE ? OR i.full_description LIKE ?)");
    params.push(`%${q}%`, `%${q}%`, `%${q}%`);
  }
  if (category) {
    where.push("AND c.slug = ?");
    params.push(category);
  }
  if (availability) {
    where.push("AND i.availability_status = ?");
    params.push(availability);
  }
  const items = publicItems(where.join(" "), params);
  const categories = db.prepare("SELECT * FROM categories ORDER BY name").all();
  const statuses = db.prepare("SELECT DISTINCT availability_status FROM items WHERE is_published = 1 ORDER BY availability_status").all();
  const alphabet = [...new Set(items.map((item) => item.name[0]?.toUpperCase()).filter(Boolean))];
  const body = `
    <section class="page-hero compact"><h1>Items Available and Custom Orders</h1><p>Browse sample listings alphabetically. Every item can be edited, hidden, marked new, put on sale, or offered as a custom order.</p></section>
    <section class="filters">
      <form method="get">
        <label>Search <input name="q" value="${escapeHtml(q)}" placeholder="Name or description"></label>
        <label>Category <select name="category"><option value="">All categories</option>${categories.map((c) => `<option value="${escapeHtml(c.slug)}" ${category === c.slug ? "selected" : ""}>${escapeHtml(c.name)}</option>`).join("")}</select></label>
        <label>Availability <select name="availability"><option value="">All statuses</option>${statuses.map((s) => `<option ${availability === s.availability_status ? "selected" : ""}>${escapeHtml(s.availability_status)}</option>`).join("")}</select></label>
        <button type="submit">Filter</button>
      </form>
      ${alphabet.length > 6 ? `<div class="alphabet">${alphabet.map((letter) => `<a href="#letter-${letter}">${letter}</a>`).join("")}</div>` : ""}
    </section>
    <section class="cards-grid">${items.map(itemCard).join("") || "<p>No items match that search yet.</p>"}</section>
  `;
  renderPublic(req, res, { title: "Items Available and Custom Orders", description: "Items, custom orders, and sewing examples.", body });
});

app.get("/items/:slug", (req, res) => {
  const item = db.prepare(`
    SELECT i.*, c.name category_name, m.url image_url, m.alt_text image_alt
    FROM items i
    LEFT JOIN categories c ON c.id = i.category_id
    LEFT JOIN media m ON m.id = i.main_image_id
    WHERE i.slug = ? AND i.is_published = 1 AND i.is_archived = 0
  `).get(req.params.slug);
  if (!item) return res.status(404).send("Item not found");
  const gallery = db.prepare(`
    SELECT m.* FROM item_images ii JOIN media m ON m.id = ii.media_id
    WHERE ii.item_id = ? ORDER BY ii.display_order, ii.id
  `).all(item.id);
  const body = `<section class="detail-layout">
    <div>${item.image_url ? `<img class="detail-image" src="${escapeHtml(item.image_url)}" alt="${escapeHtml(item.image_alt || item.name)}">` : `<div class="image-placeholder large"></div>`}</div>
    <div>
      <p class="eyebrow">${escapeHtml(item.category_name || "Item")}</p>
      <h1>${escapeHtml(item.name)}</h1>
      <p class="status">${escapeHtml(item.availability_status)}</p>
      <p class="price">${item.contact_for_pricing ? "Contact for pricing" : escapeHtml(item.price || item.starting_price || item.price_range || "")}</p>
      ${cleanRichText(item.full_description || item.short_description || "")}
      ${item.material_notes ? `<h2>Materials</h2><p>${escapeHtml(item.material_notes)}</p>` : ""}
      ${item.color_size_notes ? `<h2>Color or Size Notes</h2><p>${escapeHtml(item.color_size_notes)}</p>` : ""}
      ${item.care_instructions ? `<h2>Care Instructions</h2><p>${escapeHtml(item.care_instructions)}</p>` : ""}
      ${button("Ask About This Item", `/about-contact?item=${encodeURIComponent(item.name)}#contact-form`)}
    </div>
  </section>
  ${gallery.length ? `<section class="gallery">${gallery.map((m) => `<img src="${escapeHtml(m.url)}" alt="${escapeHtml(m.alt_text || m.title || "")}" loading="lazy">`).join("")}</section>` : ""}`;
  renderPublic(req, res, { title: item.name, description: item.short_description, body });
});

app.get("/services", (req, res) => {
  const page = pageBySlug("services");
  const services = publicServices();
  const sections = pageSections(page.id);
  const body = `<section class="page-hero compact"><h1>${escapeHtml(page.title)}</h1><p>${escapeHtml(page.intro || "")}</p></section>
    <section class="cards-grid">${services.map(serviceCard).join("")}</section>
    ${sections.map(sectionHtml).join("")}`;
  renderPublic(req, res, { title: page.title, description: page.meta_description, body });
});

app.get("/about-contact", (req, res) => {
  const settings = allSettings();
  const page = pageBySlug("about-contact");
  const sections = pageSections(page.id);
  const equipment = db.prepare(`
    SELECT e.*, m.url image_url, m.alt_text image_alt
    FROM equipment e LEFT JOIN media m ON m.id = e.image_id
    WHERE e.is_published = 1 ORDER BY e.display_order, e.name
  `).all();
  const body = `<section class="page-hero compact"><h1>${escapeHtml(page.title)}</h1><p>${escapeHtml(page.intro || "")}</p></section>
    ${sections.map(sectionHtml).join("")}
    <section class="equipment">
      <div class="section-heading"><p class="eyebrow">Equipment</p><h2>Sewing Room and Equipment</h2></div>
      <div class="cards-grid">${equipment.map((e) => `<article class="service-card">${e.image_url ? `<img src="${escapeHtml(e.image_url)}" alt="${escapeHtml(e.image_alt || e.name)}" loading="lazy">` : `<div class="image-placeholder thread"></div>`}<div class="card-body"><h3>${escapeHtml(e.name)}</h3><p>${escapeHtml([e.brand, e.model].filter(Boolean).join(" "))}</p><p>${escapeHtml(e.description || "")}</p></div></article>`).join("")}</div>
    </section>
    <section class="contact-details">
      <h2>Contact Information</h2>
      <dl>
        <div><dt>Email</dt><dd>${escapeHtml(setting(settings, "business.email", "To be supplied"))}</dd></div>
        <div><dt>Phone</dt><dd>${escapeHtml(setting(settings, "business.phone", "To be supplied"))}</dd></div>
        <div><dt>Service area</dt><dd>${escapeHtml(setting(settings, "business.service_area", "To be supplied"))}</dd></div>
        <div><dt>Hours</dt><dd>${escapeHtml(setting(settings, "business.hours", "By appointment"))}</dd></div>
      </dl>
    </section>
    ${contactForm({ csrfToken: res.locals.csrfToken, item: req.query.item, service: req.query.service, success: req.query.sent === "1", settings })}`;
  renderPublic(req, res, { title: page.title, description: page.meta_description, body });
});

app.post("/contact", async (req, res) => {
  if (!rateLimit(`contact:${ipHash(req)}`, 5, 60 * 60 * 1000)) return res.status(429).send("Too many messages were sent recently. Please try again later.");
  if (req.body.website) return res.redirect("/about-contact?sent=1#contact-form");
  const fields = {
    name: String(req.body.name || "").trim().slice(0, 120),
    email: String(req.body.email || "").trim().slice(0, 180),
    phone: String(req.body.phone || "").trim().slice(0, 80),
    preferred_contact: String(req.body.preferred_contact || "").trim().slice(0, 80),
    subject: String(req.body.subject || "").trim().slice(0, 160),
    message: String(req.body.message || "").trim().slice(0, 3000),
    related: String(req.body.related || "").trim().slice(0, 160),
    source_page: String(req.body.source_page || req.get("referer") || "").slice(0, 300)
  };
  if (!fields.name || !fields.email || !fields.subject || !fields.message || !req.body.consent || !fields.email.includes("@")) {
    return res.status(400).send("Please complete the required contact form fields and try again.");
  }
  const emailResult = await notifyOwner(fields);
  const created = now();
  db.prepare(`
    INSERT INTO messages
      (name, email, phone, preferred_contact, subject, message, related_item, consent, source_page, ip_hash, user_agent, email_notification_status, email_error, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
  `).run(fields.name, fields.email, fields.phone, fields.preferred_contact, fields.subject, fields.message, fields.related, fields.source_page, ipHash(req), String(req.get("user-agent") || "").slice(0, 300), emailResult.status, emailResult.error, created, created);
  res.redirect("/about-contact?sent=1#contact-form");
});

app.get("/robots.txt", (req, res) => {
  const settings = allSettings();
  const visible = setting(settings, "seo.visible", "true") !== "false";
  res.type("text/plain").send(`${visible ? "User-agent: *\nAllow: /" : "User-agent: *\nDisallow: /"}\nSitemap: ${config.baseUrl}/sitemap.xml\n`);
});

app.get("/sitemap.xml", (req, res) => {
  const items = publicItems();
  const urls = ["/", "/items", "/services", "/about-contact", ...items.map((item) => `/items/${item.slug}`)];
  res.type("application/xml").send(`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.map((url) => `<url><loc>${escapeHtml(config.baseUrl + url)}</loc></url>`).join("")}</urlset>`);
});

app.get("/admin", (req, res) => {
  if (!adminCount()) return res.redirect("/admin/setup");
  if (!currentAdmin(req)) return res.redirect("/admin/login");
  res.redirect("/admin/dashboard");
});

app.get("/admin/setup", (req, res) => {
  if (adminCount()) return res.redirect("/admin/login");
  res.send(authLayout("Create Owner Account", `<p>Create the first owner login. No password is stored as plain text.</p>
    <form method="post">
      <input type="hidden" name="_csrf" value="${escapeHtml(res.locals.csrfToken)}">
      <label>Name <input required name="name" maxlength="120"></label>
      <label>Email <input required type="email" name="email" maxlength="180"></label>
      <label>Password <input required type="password" name="password" minlength="10"></label>
      <button type="submit">Create Owner Account</button>
    </form>`));
});

app.post("/admin/setup", async (req, res) => {
  if (adminCount()) return res.redirect("/admin/login");
  const password = String(req.body.password || "");
  if (password.length < 10) return res.status(400).send("Please use a password with at least 10 characters.");
  const hash = await bcrypt.hash(password, 12);
  const ts = now();
  const result = db.prepare("INSERT INTO administrators (name, email, password_hash, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(String(req.body.name || "Owner").trim(), String(req.body.email || "").trim().toLowerCase(), hash, ts, ts);
  req.session.adminId = result.lastInsertRowid;
  audit(result.lastInsertRowid, "Created owner account", "", req.ip);
  res.redirect("/admin/dashboard");
});

app.get("/admin/login", (req, res) => {
  if (!adminCount()) return res.redirect("/admin/setup");
  res.send(authLayout("Owner Login", `<form method="post">
    <input type="hidden" name="_csrf" value="${escapeHtml(res.locals.csrfToken)}">
    <label>Email <input required type="email" name="email" autocomplete="username"></label>
    <label>Password <input required type="password" name="password" autocomplete="current-password"></label>
    <button type="submit">Log In</button>
  </form>`));
});

app.post("/admin/login", async (req, res) => {
  if (!rateLimit(`login:${ipHash(req)}`, 8, 15 * 60 * 1000)) return res.status(429).send("Too many login tries. Please wait and try again.");
  const admin = db.prepare("SELECT * FROM administrators WHERE email = ?").get(String(req.body.email || "").trim().toLowerCase());
  if (!admin || !(await bcrypt.compare(String(req.body.password || ""), admin.password_hash))) return res.status(401).send("Login failed. Check the email and password.");
  req.session.regenerate((error) => {
    if (error) return res.status(500).send("Login could not be completed.");
    req.session.adminId = admin.id;
    req.session.csrfToken = token(16);
    db.prepare("UPDATE administrators SET last_login_at = ? WHERE id = ?").run(now(), admin.id);
    audit(admin.id, "Logged in", "", req.ip);
    res.redirect("/admin/dashboard");
  });
});

app.post("/admin/logout", requireAdmin, (req, res) => {
  req.session.destroy(() => res.redirect("/admin/login"));
});

app.get("/admin/dashboard", requireAdmin, (req, res) => {
  const stats = {
    unread: db.prepare("SELECT COUNT(*) count FROM messages WHERE status = 'unread'").get().count,
    publishedItems: db.prepare("SELECT COUNT(*) count FROM items WHERE is_published = 1 AND is_archived = 0").get().count,
    hiddenItems: db.prepare("SELECT COUNT(*) count FROM items WHERE is_published = 0 OR is_archived = 1").get().count,
    newItems: db.prepare("SELECT COUNT(*) count FROM items WHERE newly_added = 1").get().count,
    featuredItems: db.prepare("SELECT COUNT(*) count FROM items WHERE featured = 1").get().count,
    services: db.prepare("SELECT COUNT(*) count FROM services WHERE is_published = 1").get().count,
    media: db.prepare("SELECT COALESCE(SUM(size_bytes),0) bytes FROM media").get().bytes,
    backups: db.prepare("SELECT * FROM backups ORDER BY created_at DESC LIMIT 1").get()
  };
  const messages = db.prepare("SELECT * FROM messages ORDER BY created_at DESC LIMIT 5").all();
  const changes = db.prepare("SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 8").all();
  const body = `<section class="stats-grid">
    ${Object.entries({
      "Unread messages": stats.unread,
      "Published items": stats.publishedItems,
      "Hidden or draft items": stats.hiddenItems,
      "New items": stats.newItems,
      "Featured items": stats.featuredItems,
      "Published services": stats.services,
      "Storage used": bytes(stats.media),
      "Last backup": stats.backups ? stats.backups.created_at.slice(0, 10) : "No backup yet"
    }).map(([label, value]) => `<article><strong>${escapeHtml(value)}</strong><span>${label}</span></article>`).join("")}
  </section>
  <section class="quick-actions">
    ${button("Add Item", "/admin/items/new")}
    ${button("Add Service", "/admin/services/new")}
    ${button("Upload Image", "/admin/media")}
    ${button("Edit Home Page", "/admin/home")}
    ${button("Read Messages", "/admin/messages")}
    ${button("Edit Contact Information", "/admin/business")}
  </section>
  <section class="two-column">
    <div class="panel"><h2>Recent Messages</h2>${messages.map((m) => `<p><a href="/admin/messages/${m.id}">${escapeHtml(m.subject)}</a><br><small>${escapeHtml(m.name)} · ${escapeHtml(m.created_at)}</small></p>`).join("") || "<p>No messages yet.</p>"}</div>
    <div class="panel"><h2>Recent Website Changes</h2>${changes.map((c) => `<p>${escapeHtml(c.action)}<br><small>${escapeHtml(c.created_at)}</small></p>`).join("") || "<p>No changes yet.</p>"}</div>
  </section>`;
  renderAdmin(req, res, "Dashboard", "dashboard", body);
});

app.get("/admin/home", requireAdmin, (req, res) => {
  const s = allSettings();
  const body = `<form method="post" class="editor-form">
    ${csrf(res)}
    <section class="panel"><h2>Hero Area</h2>
      ${input("Business name in hero", "home.hero.title", setting(s, "home.hero.title"))}
      ${textarea("Subtitle", "home.hero.subtitle", setting(s, "home.hero.subtitle"))}
      ${input("Contact line", "home.hero.contact", setting(s, "home.hero.contact"))}
      ${input("Hero background URL", "home.hero.image", setting(s, "home.hero.image"))}
      ${select("Image position", "home.hero.position", ["center", "top", "bottom", "left", "right"], setting(s, "home.hero.position"))}
      ${select("Image sizing", "home.hero.size", ["cover", "contain", "auto"], setting(s, "home.hero.size"))}
      ${select("Overlay style", "home.hero.overlay", ["light", "dark", "none"], setting(s, "home.hero.overlay"))}
      ${input("Overlay opacity", "home.hero.overlay_opacity", setting(s, "home.hero.overlay_opacity"), "number", "0", "1", "0.05")}
      ${input("Hero text color", "home.hero.text_color", setting(s, "home.hero.text_color"), "color")}
      ${input("First button label", "home.hero.button1_label", setting(s, "home.hero.button1_label"))}
      ${input("First button link", "home.hero.button1_url", setting(s, "home.hero.button1_url"))}
      ${input("Second button label", "home.hero.button2_label", setting(s, "home.hero.button2_label"))}
      ${input("Second button link", "home.hero.button2_url", setting(s, "home.hero.button2_url"))}
    </section>
    <section class="panel"><h2>Featured and Newly Added Area</h2>
      ${input("Section title", "home.highlight.title", setting(s, "home.highlight.title"))}
      ${textarea("Intro text", "home.highlight.intro", setting(s, "home.highlight.intro"))}
      ${input("Background color", "home.highlight.background_color", setting(s, "home.highlight.background_color"), "color")}
    </section>
    <button type="submit">Save Home Page Settings</button>
    <a class="button secondary" href="/" target="_blank" rel="noopener noreferrer">Preview Home Page</a>
  </form>`;
  renderAdmin(req, res, "Home Page Editor", "home", body);
});

app.post("/admin/home", requireAdmin, (req, res) => {
  Object.keys(req.body).filter((key) => key.startsWith("home.")).forEach((key) => setSetting(key, req.body[key]));
  audit(req.admin.id, "Updated home page settings", "", req.ip);
  res.redirect("/admin/home?saved=1");
});

app.get("/admin/pages", requireAdmin, (req, res) => {
  const pages = db.prepare("SELECT * FROM pages ORDER BY id").all();
  renderAdmin(req, res, "Page Editor", "pages", `<div class="table-list">${pages.map((p) => `<article><h2>${escapeHtml(p.title)}</h2><p>${escapeHtml(p.intro || "")}</p><a class="button" href="/admin/pages/${p.slug}">Edit Page</a><a class="button secondary" href="${p.slug === "home" ? "/" : "/" + p.slug}" target="_blank">Preview</a></article>`).join("")}</div>`);
});

app.get("/admin/pages/:slug", requireAdmin, (req, res) => {
  const page = pageBySlug(req.params.slug);
  if (!page) return res.status(404).send("Page not found");
  const sections = pageSections(page.id, true);
  const media = mediaRows();
  const body = `<form method="post" class="editor-form">
    ${csrf(res)}
    <section class="panel">
      ${input("Page title", "title", page.title)}
      ${input("Page subtitle", "subtitle", page.subtitle || "")}
      ${textarea("Introduction", "intro", page.intro || "")}
      ${input("Search title", "meta_title", page.meta_title || "")}
      ${textarea("Search description", "meta_description", page.meta_description || "")}
      ${select("Search visibility", "search_visible", [["1","Visible to search engines"], ["0","Hidden from search engines"]], String(page.search_visible))}
      ${select("Status", "status", ["published", "draft"], page.status)}
    </section>
    <button type="submit">Save Page</button>
    <a class="button secondary" href="${page.slug === "home" ? "/" : "/" + page.slug}" target="_blank">Preview Page</a>
  </form>
  <section class="panel"><h2>Content Sections</h2>
    ${sections.map((section) => sectionForm(section, media, res.locals.csrfToken, `/admin/pages/${page.slug}/sections/${section.id}`)).join("")}
    <form method="post" action="/admin/pages/${page.slug}/sections/new" class="inline-form">
      ${csrf(res)}<button type="submit">Add Section</button>
    </form>
  </section>`;
  renderAdmin(req, res, `Edit ${page.title}`, "pages", body);
});

app.post("/admin/pages/:slug", requireAdmin, (req, res) => {
  const page = pageBySlug(req.params.slug);
  saveRevision("page", page.id, page.title, page, req.admin.id);
  db.prepare("UPDATE pages SET title=?, subtitle=?, intro=?, meta_title=?, meta_description=?, search_visible=?, status=?, updated_at=? WHERE id=?")
    .run(req.body.title, req.body.subtitle, req.body.intro, req.body.meta_title, req.body.meta_description, checkbox(req.body.search_visible === "1"), req.body.status, now(), page.id);
  audit(req.admin.id, `Updated page ${page.title}`, "", req.ip);
  res.redirect(`/admin/pages/${page.slug}`);
});

app.post("/admin/pages/:slug/sections/new", requireAdmin, (req, res) => {
  const page = pageBySlug(req.params.slug);
  db.prepare("INSERT INTO page_sections (page_id, title, body, layout, display_order, is_published, updated_at) VALUES (?, 'New Section', '<p>Edit this section.</p>', 'text', (SELECT COALESCE(MAX(display_order),0)+10 FROM page_sections WHERE page_id=?), 0, ?)")
    .run(page.id, page.id, now());
  res.redirect(`/admin/pages/${page.slug}`);
});

app.post("/admin/pages/:slug/sections/:id", requireAdmin, (req, res) => {
  const page = pageBySlug(req.params.slug);
  const section = db.prepare("SELECT * FROM page_sections WHERE id = ?").get(req.params.id);
  saveRevision("section", section.id, section.title, section, req.admin.id);
  db.prepare("UPDATE page_sections SET title=?, body=?, layout=?, image_id=?, background_image_id=?, background_color=?, button_label=?, button_url=?, display_order=?, is_published=?, updated_at=? WHERE id=?")
    .run(req.body.title, cleanRichText(req.body.body), req.body.layout, req.body.image_id || null, req.body.background_image_id || null, req.body.background_color, req.body.button_label, req.body.button_url, Number(req.body.display_order || 0), checkbox(req.body.is_published), now(), section.id);
  audit(req.admin.id, `Updated section on ${page.title}`, "", req.ip);
  res.redirect(`/admin/pages/${page.slug}`);
});

function csrf(res) {
  return `<input type="hidden" name="_csrf" value="${escapeHtml(res.locals.csrfToken)}">`;
}

function input(label, name, value = "", type = "text", min = "", max = "", step = "") {
  return `<label>${escapeHtml(label)} <input type="${type}" name="${escapeHtml(name)}" value="${escapeHtml(value)}" ${min !== "" ? `min="${min}"` : ""} ${max !== "" ? `max="${max}"` : ""} ${step !== "" ? `step="${step}"` : ""}></label>`;
}

function textarea(label, name, value = "") {
  return `<label>${escapeHtml(label)} <textarea name="${escapeHtml(name)}" rows="5">${escapeHtml(value)}</textarea></label>`;
}

function select(label, name, options, selected = "") {
  const html = options.map((option) => {
    const value = Array.isArray(option) ? option[0] : option;
    const text = Array.isArray(option) ? option[1] : option;
    return `<option value="${escapeHtml(value)}" ${String(selected) === String(value) ? "selected" : ""}>${escapeHtml(text)}</option>`;
  }).join("");
  return `<label>${escapeHtml(label)} <select name="${escapeHtml(name)}">${html}</select></label>`;
}

function checkboxInput(label, name, checked) {
  return `<label class="checkbox"><input type="checkbox" name="${escapeHtml(name)}" value="1" ${checked ? "checked" : ""}> ${escapeHtml(label)}</label>`;
}

function sectionForm(section, media, csrfToken, action) {
  return `<details class="section-editor"><summary>${escapeHtml(section.title || "Untitled section")} ${section.is_published ? "" : "(hidden)"}</summary>
    <form method="post" action="${escapeHtml(action)}" class="editor-form nested">
      <input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}">
      ${input("Heading", "title", section.title || "")}
      ${textarea("Text", "body", section.body || "")}
      ${select("Layout", "layout", [["text", "Text only"], ["image-left", "Image left, text right"], ["image-right", "Text left, image right"], ["full-image", "Full-width image"], ["contact-callout", "Contact callout"]], section.layout)}
      <label>Image <select name="image_id"><option value="">No image</option>${media.map((m) => mediaOption(m, section.image_id)).join("")}</select></label>
      <label>Background image <select name="background_image_id"><option value="">No background image</option>${media.map((m) => mediaOption(m, section.background_image_id)).join("")}</select></label>
      ${input("Background color", "background_color", section.background_color || "", "color")}
      ${input("Button label", "button_label", section.button_label || "")}
      ${input("Button link", "button_url", section.button_url || "")}
      ${input("Display order", "display_order", section.display_order || 0, "number")}
      ${checkboxInput("Show this section on the public page", "is_published", section.is_published)}
      <button type="submit">Save Section</button>
    </form>
  </details>`;
}

app.get("/admin/items", requireAdmin, (req, res) => {
  const q = String(req.query.q || "");
  const items = db.prepare(`
    SELECT i.*, c.name category_name FROM items i LEFT JOIN categories c ON c.id = i.category_id
    WHERE i.name LIKE ? OR i.short_description LIKE ?
    ORDER BY i.name
  `).all(`%${q}%`, `%${q}%`);
  const body = `<form method="get" class="toolbar"><input name="q" placeholder="Search items" value="${escapeHtml(q)}"><button>Search</button><a class="button" href="/admin/items/new">Add Item</a></form>
    <div class="table-list">${items.map((i) => `<article><h2>${escapeHtml(i.name)}</h2><p>${escapeHtml(i.category_name || "")} · ${escapeHtml(i.availability_status)} ${i.is_published ? "" : "· Hidden"}</p><a class="button" href="/admin/items/${i.id}/edit">Edit</a><a class="button secondary" href="/items/${i.slug}" target="_blank">Preview</a></article>`).join("")}</div>`;
  renderAdmin(req, res, "Items Manager", "items", body);
});

app.get("/admin/items/new", requireAdmin, (req, res) => renderAdmin(req, res, "Add Item", "items", itemEditor(null, res.locals.csrfToken)));
app.get("/admin/items/:id/edit", requireAdmin, (req, res) => {
  const item = db.prepare("SELECT i.*, c.name category_name FROM items i LEFT JOIN categories c ON c.id = i.category_id WHERE i.id = ?").get(req.params.id);
  if (!item) return res.status(404).send("Item not found");
  renderAdmin(req, res, `Edit ${item.name}`, "items", itemEditor(item, res.locals.csrfToken));
});

app.post("/admin/items/new", requireAdmin, upload.array("images", 8), async (req, res, next) => {
  try {
    const id = await saveItem(req, null);
    res.redirect(`/admin/items/${id}/edit`);
  } catch (error) { next(error); }
});
app.post("/admin/items/:id/edit", requireAdmin, upload.array("images", 8), async (req, res, next) => {
  try {
    await saveItem(req, req.params.id);
    res.redirect(`/admin/items/${req.params.id}/edit`);
  } catch (error) { next(error); }
});
app.post("/admin/items/:id/duplicate", requireAdmin, (req, res) => {
  const item = db.prepare("SELECT * FROM items WHERE id=?").get(req.params.id);
  if (!item) return res.status(404).send("Item not found");
  const copyName = `${item.name} Copy`;
  const copySlug = `${item.slug}-copy-${token(3)}`;
  const ts = now();
  const result = db.prepare(`INSERT INTO items (name, slug, short_description, full_description, category_id, main_image_id, price, starting_price, price_range, contact_for_pricing, quantity, availability_status, custom_order_available, color_size_notes, material_notes, care_instructions, featured, newly_added, on_sale, sale_text, display_order, is_published, is_archived, date_added, published_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, 0, 0, ?, NULL, ?)`)
    .run(copyName, copySlug, item.short_description, item.full_description, item.category_id, item.main_image_id, item.price, item.starting_price, item.price_range, item.contact_for_pricing, item.quantity, item.availability_status, item.custom_order_available, item.color_size_notes, item.material_notes, item.care_instructions, item.sale_text, item.display_order + 1, ts, ts);
  audit(req.admin.id, "Duplicated item", item.name, req.ip);
  res.redirect(`/admin/items/${result.lastInsertRowid}/edit`);
});
app.post("/admin/items/:id/archive", requireAdmin, (req, res) => {
  db.prepare("UPDATE items SET is_archived=1, is_published=0, updated_at=? WHERE id=?").run(now(), req.params.id);
  audit(req.admin.id, "Archived item", String(req.params.id), req.ip);
  res.redirect("/admin/items");
});
app.post("/admin/items/:id/delete", requireAdmin, (req, res) => {
  db.prepare("DELETE FROM items WHERE id=?").run(req.params.id);
  audit(req.admin.id, "Deleted item", String(req.params.id), req.ip);
  res.redirect("/admin/items");
});

async function saveItem(req, id) {
  const categoryId = req.body.category ? ensureCategory(req.body.category.trim()) : null;
  const ts = now();
  const slug = makeSlug(req.body.slug || req.body.name, "item");
  if (id) {
    const old = db.prepare("SELECT * FROM items WHERE id=?").get(id);
    saveRevision("item", id, old.name, old, req.admin.id);
    db.prepare(`UPDATE items SET name=?, slug=?, short_description=?, full_description=?, category_id=?, price=?, starting_price=?, price_range=?, contact_for_pricing=?, quantity=?, availability_status=?, custom_order_available=?, color_size_notes=?, material_notes=?, care_instructions=?, featured=?, newly_added=?, on_sale=?, sale_text=?, display_order=?, is_published=?, updated_at=? WHERE id=?`)
      .run(req.body.name, slug, req.body.short_description, cleanRichText(req.body.full_description), categoryId, req.body.price, req.body.starting_price, req.body.price_range, checkbox(req.body.contact_for_pricing), req.body.quantity, req.body.availability_status, checkbox(req.body.custom_order_available), req.body.color_size_notes, req.body.material_notes, req.body.care_instructions, checkbox(req.body.featured), checkbox(req.body.newly_added), checkbox(req.body.on_sale), req.body.sale_text, Number(req.body.display_order || 0), checkbox(req.body.is_published), ts, id);
  } else {
    const result = db.prepare(`INSERT INTO items (name, slug, short_description, full_description, category_id, price, starting_price, price_range, contact_for_pricing, quantity, availability_status, custom_order_available, color_size_notes, material_notes, care_instructions, featured, newly_added, on_sale, sale_text, display_order, is_published, date_added, published_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
      .run(req.body.name, slug, req.body.short_description, cleanRichText(req.body.full_description), categoryId, req.body.price, req.body.starting_price, req.body.price_range, checkbox(req.body.contact_for_pricing), req.body.quantity, req.body.availability_status, checkbox(req.body.custom_order_available), req.body.color_size_notes, req.body.material_notes, req.body.care_instructions, checkbox(req.body.featured), checkbox(req.body.newly_added), checkbox(req.body.on_sale), req.body.sale_text, Number(req.body.display_order || 0), checkbox(req.body.is_published), ts, ts, ts);
    id = result.lastInsertRowid;
  }
  for (const file of req.files || []) {
    const media = await storeImage(file, { title: req.body.name, alt: req.body.name, category: "Items" });
    db.prepare("INSERT INTO item_images (item_id, media_id, display_order, is_main) VALUES (?, ?, (SELECT COALESCE(MAX(display_order),0)+10 FROM item_images WHERE item_id=?), 0)").run(id, media.id, id);
    const item = db.prepare("SELECT main_image_id FROM items WHERE id=?").get(id);
    if (!item.main_image_id) db.prepare("UPDATE items SET main_image_id=? WHERE id=?").run(media.id, id);
  }
  audit(req.admin.id, id ? "Saved item" : "Added item", req.body.name, req.ip);
  return id;
}

function itemEditor(item, csrfToken) {
  const values = item || { availability_status: "Available", is_published: 1 };
  return `<form method="post" enctype="multipart/form-data" class="editor-form">
    <input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}">
    ${input("Item name", "name", values.name || "")}
    ${input("Clean page address", "slug", values.slug || "")}
    ${textarea("Short description", "short_description", values.short_description || "")}
    ${textarea("Full description", "full_description", values.full_description || "")}
    ${input("Category", "category", values.category_name || "")}
    ${input("Price", "price", values.price || "")}
    ${input("Starting price", "starting_price", values.starting_price || "")}
    ${input("Price range", "price_range", values.price_range || "")}
    ${checkboxInput("Contact for pricing", "contact_for_pricing", values.contact_for_pricing)}
    ${input("Available quantity", "quantity", values.quantity || "")}
    ${select("Availability", "availability_status", ["Available", "Made to Order", "Custom Order", "Sold", "Seasonal", "On Sale", "Hidden"], values.availability_status)}
    ${checkboxInput("Custom order available", "custom_order_available", values.custom_order_available)}
    ${textarea("Color or size notes", "color_size_notes", values.color_size_notes || "")}
    ${textarea("Material notes", "material_notes", values.material_notes || "")}
    ${textarea("Care instructions", "care_instructions", values.care_instructions || "")}
    ${checkboxInput("Feature this item", "featured", values.featured)}
    ${checkboxInput("Mark as newly added", "newly_added", values.newly_added)}
    ${checkboxInput("Mark as on sale", "on_sale", values.on_sale)}
    ${input("Sale text", "sale_text", values.sale_text || "")}
    ${input("Display order", "display_order", values.display_order || 0, "number")}
    ${checkboxInput("Publish this item", "is_published", values.is_published)}
    <label>Upload item images <input type="file" name="images" multiple accept="image/png,image/jpeg,image/webp"></label>
    <button type="submit">Save Item</button>
  </form>
  ${values.id ? `<section class="panel quick-actions">
    <form method="post" action="/admin/items/${values.id}/duplicate"><input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}"><button>Duplicate Item</button></form>
    <form method="post" action="/admin/items/${values.id}/archive"><input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}"><button>Archive Item</button></form>
    <form method="post" action="/admin/items/${values.id}/delete"><input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}"><button class="danger">Delete Item</button></form>
  </section>` : ""}`;
}

app.get("/admin/services", requireAdmin, (req, res) => {
  const services = db.prepare("SELECT * FROM services ORDER BY display_order, name").all();
  renderAdmin(req, res, "Services Manager", "services", `<p><a class="button" href="/admin/services/new">Add Service</a></p><div class="table-list">${services.map((s) => `<article><h2>${escapeHtml(s.name)}</h2><p>${s.is_published ? "Published" : "Hidden"} · Order ${s.display_order}</p><a class="button" href="/admin/services/${s.id}/edit">Edit</a></article>`).join("")}</div>`);
});

app.get("/admin/services/new", requireAdmin, (req, res) => renderAdmin(req, res, "Add Service", "services", serviceEditor(null, res.locals.csrfToken)));
app.get("/admin/services/:id/edit", requireAdmin, (req, res) => {
  const service = db.prepare("SELECT * FROM services WHERE id=?").get(req.params.id);
  renderAdmin(req, res, `Edit ${service.name}`, "services", serviceEditor(service, res.locals.csrfToken));
});
app.post("/admin/services/new", requireAdmin, upload.single("image"), async (req, res, next) => {
  try { const id = await saveService(req, null); res.redirect(`/admin/services/${id}/edit`); } catch (e) { next(e); }
});
app.post("/admin/services/:id/edit", requireAdmin, upload.single("image"), async (req, res, next) => {
  try { await saveService(req, req.params.id); res.redirect(`/admin/services/${req.params.id}/edit`); } catch (e) { next(e); }
});
app.post("/admin/services/:id/duplicate", requireAdmin, (req, res) => {
  const s = db.prepare("SELECT * FROM services WHERE id=?").get(req.params.id);
  if (!s) return res.status(404).send("Service not found");
  const result = db.prepare("INSERT INTO services (name, slug, image_id, short_description, full_description, pricing_note, display_order, featured, is_published, published_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?)")
    .run(`${s.name} Copy`, `${s.slug}-copy-${token(3)}`, s.image_id, s.short_description, s.full_description, s.pricing_note, s.display_order + 1, now());
  audit(req.admin.id, "Duplicated service", s.name, req.ip);
  res.redirect(`/admin/services/${result.lastInsertRowid}/edit`);
});
app.post("/admin/services/:id/delete", requireAdmin, (req, res) => {
  db.prepare("DELETE FROM services WHERE id=?").run(req.params.id);
  audit(req.admin.id, "Deleted service", String(req.params.id), req.ip);
  res.redirect("/admin/services");
});

async function saveService(req, id) {
  const media = req.file ? await storeImage(req.file, { title: req.body.name, alt: req.body.name, category: "Services" }) : null;
  const ts = now();
  const slug = makeSlug(req.body.slug || req.body.name, "service");
  if (id) {
    const old = db.prepare("SELECT * FROM services WHERE id=?").get(id);
    saveRevision("service", id, old.name, old, req.admin.id);
    db.prepare("UPDATE services SET name=?, slug=?, image_id=COALESCE(?, image_id), short_description=?, full_description=?, pricing_note=?, display_order=?, featured=?, is_published=?, updated_at=? WHERE id=?")
      .run(req.body.name, slug, media?.id || null, req.body.short_description, cleanRichText(req.body.full_description), req.body.pricing_note, Number(req.body.display_order || 0), checkbox(req.body.featured), checkbox(req.body.is_published), ts, id);
  } else {
    const result = db.prepare("INSERT INTO services (name, slug, image_id, short_description, full_description, pricing_note, display_order, featured, is_published, published_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
      .run(req.body.name, slug, media?.id || null, req.body.short_description, cleanRichText(req.body.full_description), req.body.pricing_note, Number(req.body.display_order || 0), checkbox(req.body.featured), checkbox(req.body.is_published), ts, ts);
    id = result.lastInsertRowid;
  }
  audit(req.admin.id, "Saved service", req.body.name, req.ip);
  return id;
}

function serviceEditor(service, csrfToken) {
  const s = service || { is_published: 1, display_order: 0 };
  return `<form method="post" enctype="multipart/form-data" class="editor-form">
    <input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}">
    ${input("Service name", "name", s.name || "")}
    ${input("Clean page address", "slug", s.slug || "")}
    ${textarea("Short description", "short_description", s.short_description || "")}
    ${textarea("Full description", "full_description", s.full_description || "")}
    ${input("Pricing note", "pricing_note", s.pricing_note || "")}
    ${input("Display order", "display_order", s.display_order || 0, "number")}
    ${checkboxInput("Feature this service", "featured", s.featured)}
    ${checkboxInput("Publish this service", "is_published", s.is_published)}
    <label>Service image <input type="file" name="image" accept="image/png,image/jpeg,image/webp"></label>
    <button type="submit">Save Service</button>
  </form>
  ${s.id ? `<section class="panel quick-actions">
    <form method="post" action="/admin/services/${s.id}/duplicate"><input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}"><button>Duplicate Service</button></form>
    <form method="post" action="/admin/services/${s.id}/delete"><input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}"><button class="danger">Delete Service</button></form>
  </section>` : ""}`;
}

app.get("/admin/about-equipment", requireAdmin, (req, res) => {
  const equipment = db.prepare("SELECT * FROM equipment ORDER BY display_order, name").all();
  const body = `<p><a class="button" href="/admin/pages/about-contact">Edit About Page Text</a></p>
    <section class="panel"><h2>Equipment</h2>${equipment.map((e) => equipmentForm(e, res.locals.csrfToken)).join("")}
    <form method="post" action="/admin/about-equipment/new">${csrf(res)}<button>Add Equipment</button></form></section>`;
  renderAdmin(req, res, "About and Equipment Manager", "about", body);
});

app.post("/admin/about-equipment/new", requireAdmin, (req, res) => {
  db.prepare("INSERT INTO equipment (name, type, description, display_order, is_published, updated_at) VALUES ('New Equipment', '', '', 100, 0, ?)").run(now());
  res.redirect("/admin/about-equipment");
});
app.post("/admin/about-equipment/:id", requireAdmin, (req, res) => {
  db.prepare("UPDATE equipment SET name=?, type=?, brand=?, model=?, description=?, display_order=?, is_published=?, updated_at=? WHERE id=?")
    .run(req.body.name, req.body.type, req.body.brand, req.body.model, req.body.description, Number(req.body.display_order || 0), checkbox(req.body.is_published), now(), req.params.id);
  audit(req.admin.id, "Saved equipment", req.body.name, req.ip);
  res.redirect("/admin/about-equipment");
});
function equipmentForm(e, csrfToken) {
  return `<details class="section-editor"><summary>${escapeHtml(e.name)} ${e.is_published ? "" : "(hidden)"}</summary><form method="post" action="/admin/about-equipment/${e.id}" class="editor-form nested">
    <input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}">${input("Equipment name", "name", e.name)}${input("Type", "type", e.type || "")}${input("Brand", "brand", e.brand || "")}${input("Model", "model", e.model || "")}${textarea("Description", "description", e.description || "")}${input("Display order", "display_order", e.display_order, "number")}${checkboxInput("Publish this equipment", "is_published", e.is_published)}<button>Save Equipment</button></form></details>`;
}

app.get("/admin/messages", requireAdmin, (req, res) => {
  const status = req.query.status || "";
  const rows = db.prepare(`SELECT * FROM messages ${status ? "WHERE status=?" : ""} ORDER BY created_at DESC`).all(...(status ? [status] : []));
  const body = `<div class="toolbar"><a class="button secondary" href="/admin/messages/export">Export CSV</a><a href="/admin/messages">All</a><a href="/admin/messages?status=unread">Unread</a><a href="/admin/messages?status=archived">Archived</a></div>
    <div class="table-list">${rows.map((m) => `<article><h2><a href="/admin/messages/${m.id}">${escapeHtml(m.subject)}</a></h2><p>${escapeHtml(m.name)} · ${escapeHtml(m.email)} · ${escapeHtml(m.created_at)} · ${escapeHtml(m.status)}</p></article>`).join("") || "<p>No messages here.</p>"}</div>`;
  renderAdmin(req, res, "Messages Inbox", "messages", body);
});
app.get("/admin/messages/export", requireAdmin, (req, res) => {
  const rows = db.prepare("SELECT created_at,name,email,phone,preferred_contact,subject,message,related_item,source_page,status,email_notification_status FROM messages ORDER BY created_at DESC").all();
  const csv = ["created_at,name,email,phone,preferred_contact,subject,message,related_item,source_page,status,email_notification_status", ...rows.map((r) => Object.values(r).map((v) => `"${String(v ?? "").replaceAll('"', '""')}"`).join(","))].join("\n");
  res.type("text/csv").attachment("debbie-windler-messages.csv").send(csv);
});
app.get("/admin/messages/:id", requireAdmin, (req, res) => {
  const m = db.prepare("SELECT * FROM messages WHERE id=?").get(req.params.id);
  if (!m) return res.status(404).send("Message not found");
  db.prepare("UPDATE messages SET status='read', updated_at=? WHERE id=? AND status='unread'").run(now(), m.id);
  const mailto = `mailto:${encodeURIComponent(m.email)}?subject=${encodeURIComponent(`Re: ${m.subject}`)}`;
  renderAdmin(req, res, m.subject, "messages", `<section class="panel message-detail"><p><strong>From:</strong> ${escapeHtml(m.name)} · ${escapeHtml(m.email)} · ${escapeHtml(m.phone || "")}</p><p><strong>Preferred:</strong> ${escapeHtml(m.preferred_contact || "")}</p><p><strong>Related:</strong> ${escapeHtml(m.related_item || "")}</p><p><strong>Source:</strong> ${escapeHtml(m.source_page || "")}</p><p><strong>Email notice:</strong> ${escapeHtml(m.email_notification_status)} ${m.email_error ? `- ${escapeHtml(m.email_error)}` : ""}</p><pre>${escapeHtml(m.message)}</pre><form method="post" action="/admin/messages/${m.id}/status">${csrf(res)}<button name="status" value="unread">Mark Unread</button><button name="status" value="archived">Archive</button><button name="status" value="deleted" class="danger">Delete</button><a class="button secondary" href="${mailto}">Reply by Email</a></form></section>`);
});
app.post("/admin/messages/:id/status", requireAdmin, (req, res) => {
  db.prepare("UPDATE messages SET status=?, updated_at=? WHERE id=?").run(req.body.status, now(), req.params.id);
  res.redirect("/admin/messages");
});

app.get("/admin/media", requireAdmin, (req, res) => {
  const media = mediaRows();
  const body = `<form method="post" enctype="multipart/form-data" class="panel editor-form">
    ${csrf(res)}${input("Image title", "title", "")}${input("Alt text", "alt", "")}${input("Folder or category", "category", "")}
    <label>Images <input type="file" name="images" multiple accept="image/png,image/jpeg,image/webp"></label><button>Upload Images</button>
  </form>${mediaGrid(media)}
  <div class="table-list">${media.map((m) => `<article><h2>${escapeHtml(m.title || m.original_name)}</h2><p>${escapeHtml(m.url)} · ${bytes(m.size_bytes)}</p><form method="post" action="/admin/media/${m.id}">${csrf(res)}${input("Title", "title", m.title || "")}${input("Alt text", "alt_text", m.alt_text || "")}${input("Folder", "category", m.category || "")}<button>Save Image Details</button></form><form method="post" action="/admin/media/${m.id}/delete">${csrf(res)}<button class="danger">Delete Unused Image</button></form></article>`).join("")}</div>`;
  renderAdmin(req, res, "Media Library", "media", body);
});
app.post("/admin/media", requireAdmin, upload.array("images", 10), async (req, res, next) => {
  try {
    for (const file of req.files || []) await storeImage(file, req.body);
    audit(req.admin.id, "Uploaded images", `${req.files?.length || 0} files`, req.ip);
    res.redirect("/admin/media");
  } catch (e) { next(e); }
});
app.post("/admin/media/:id", requireAdmin, (req, res) => {
  db.prepare("UPDATE media SET title=?, alt_text=?, category=?, updated_at=? WHERE id=?").run(req.body.title, req.body.alt_text, req.body.category, now(), req.params.id);
  res.redirect("/admin/media");
});
app.post("/admin/media/:id/delete", requireAdmin, (req, res) => {
  const id = Number(req.params.id);
  const used = [
    db.prepare("SELECT COUNT(*) count FROM items WHERE main_image_id=?").get(id).count,
    db.prepare("SELECT COUNT(*) count FROM services WHERE image_id=?").get(id).count,
    db.prepare("SELECT COUNT(*) count FROM equipment WHERE image_id=?").get(id).count,
    db.prepare("SELECT COUNT(*) count FROM page_sections WHERE image_id=? OR background_image_id=?").get(id, id).count
  ].reduce((sum, count) => sum + count, 0);
  if (used) return res.status(400).send("This image is currently used on the website. Remove it from those pages or items before deleting it.");
  const media = db.prepare("SELECT * FROM media WHERE id=?").get(id);
  if (media) {
    for (const url of [media.url, media.thumb_url]) {
      if (!url) continue;
      const filePath = path.join(config.root, "public", url.replace(/^\//, ""));
      if (filePath.startsWith(config.uploadDir) && fs.existsSync(filePath)) fs.unlinkSync(filePath);
    }
    db.prepare("DELETE FROM media WHERE id=?").run(id);
    audit(req.admin.id, "Deleted unused image", media.file_name, req.ip);
  }
  res.redirect("/admin/media");
});

app.get("/admin/appearance", requireAdmin, (req, res) => settingsForm(req, res, "Appearance and Theme Settings", "appearance", [
  ["Main background color", "theme.bg", "color"], ["Secondary background color", "theme.bg_alt", "color"], ["Text color", "theme.text", "color"], ["Heading color", "theme.heading", "color"], ["Link color", "theme.link", "color"], ["Button color", "theme.button", "color"], ["Button text color", "theme.button_text", "color"], ["Accent color", "theme.accent", "color"], ["Border radius", "theme.radius", "number"], ["Site width", "theme.width", "number"], ["Heading font", "theme.heading_font"], ["Body font", "theme.body_font"], ["Footer style", "theme.footer_style"]
]));
app.post("/admin/appearance", requireAdmin, (req, res) => saveSettings(req, res, "appearance", "Updated appearance settings"));

app.get("/admin/business", requireAdmin, (req, res) => settingsForm(req, res, "Business Settings", "business", [
  ["Business name", "business.name"], ["Tagline", "business.tagline"], ["Email", "business.email"], ["Phone", "business.phone"], ["Facebook link", "business.facebook"], ["Service area", "business.service_area"], ["Business hours", "business.hours"], ["Preferred contact method", "business.preferred_contact"], ["Footer text", "business.footer_text"], ["Copyright text", "business.copyright"], ["Website title", "site.title"], ["Website description", "site.description"]
]));
app.post("/admin/business", requireAdmin, (req, res) => saveSettings(req, res, "business", "Updated business settings"));

app.get("/admin/seo", requireAdmin, (req, res) => settingsForm(req, res, "Search Engine and Sharing Settings", "seo", [
  ["Website title", "site.title"], ["Website description", "site.description"], ["Canonical URL", "seo.canonical"], ["Search-engine visibility", "seo.visible"]
]));
app.post("/admin/seo", requireAdmin, (req, res) => saveSettings(req, res, "seo", "Updated SEO settings"));

app.get("/admin/maintenance", requireAdmin, (req, res) => settingsForm(req, res, "Maintenance Mode", "maintenance", [
  ["Maintenance enabled (true or false)", "maintenance.enabled"], ["Maintenance message", "maintenance.message"], ["Expected return message", "maintenance.return_note"], ["Artwork URL", "maintenance.artwork"]
]));
app.post("/admin/maintenance", requireAdmin, (req, res) => saveSettings(req, res, "maintenance", "Updated maintenance mode"));

function settingsForm(req, res, title, active, fields) {
  const s = allSettings();
  const body = `<form method="post" class="editor-form panel">${csrf(res)}${fields.map(([label, key, type]) => type === "textarea" ? textarea(label, key, setting(s, key)) : input(label, key, setting(s, key), type || "text")).join("")}<button>Save Settings</button></form>`;
  renderAdmin(req, res, title, active, body);
}
function saveSettings(req, res, redirect, action) {
  Object.entries(req.body).filter(([k]) => k !== "_csrf").forEach(([key, value]) => setSetting(key, value));
  audit(req.admin.id, action, "", req.ip);
  res.redirect(`/admin/${redirect}`);
}

app.get("/admin/navigation", requireAdmin, (req, res) => {
  const rows = db.prepare("SELECT * FROM navigation ORDER BY display_order, id").all();
  const body = `<section class="panel">${rows.map((n) => `<form method="post" action="/admin/navigation/${n.id}" class="inline-grid">${csrf(res)}${input("Label", "label", n.label)}${input("Link", "url", n.url)}${select("Kind", "kind", ["internal", "external"], n.kind)}${input("Order", "display_order", n.display_order, "number")}${checkboxInput("Show", "is_published", n.is_published)}${checkboxInput("Open in new tab", "opens_new_tab", n.opens_new_tab)}<button>Save Link</button></form>`).join("")}<form method="post" action="/admin/navigation/new">${csrf(res)}<button>Add Navigation Link</button></form></section>`;
  renderAdmin(req, res, "Navigation Manager", "navigation", body);
});
app.post("/admin/navigation/new", requireAdmin, (req, res) => {
  db.prepare("INSERT INTO navigation (label,url,kind,display_order,is_published) VALUES ('New Link','/','internal',100,0)").run();
  res.redirect("/admin/navigation");
});
app.post("/admin/navigation/:id", requireAdmin, (req, res) => {
  const url = String(req.body.url || "/");
  if (url.startsWith("/admin")) return res.status(400).send("Administration links cannot be added to the public menu.");
  db.prepare("UPDATE navigation SET label=?, url=?, kind=?, display_order=?, is_published=?, opens_new_tab=? WHERE id=?").run(req.body.label, url, req.body.kind, Number(req.body.display_order || 0), checkbox(req.body.is_published), checkbox(req.body.opens_new_tab), req.params.id);
  res.redirect("/admin/navigation");
});

app.get("/admin/security", requireAdmin, (req, res) => {
  renderAdmin(req, res, "User and Security Settings", "security", `<section class="panel"><h2>Change Password</h2><form method="post" class="editor-form">${csrf(res)}<label>Current password <input type="password" name="current_password" required></label><label>New password <input type="password" name="new_password" minlength="10" required></label><button>Change Password</button></form></section><section class="panel"><h2>Security Included</h2><ul><li>Secure password hashing</li><li>Session expiration</li><li>Login rate limiting</li><li>CSRF protection</li><li>Upload validation</li><li>Audit log for important changes</li></ul></section>`);
});
app.post("/admin/security", requireAdmin, async (req, res) => {
  const row = db.prepare("SELECT * FROM administrators WHERE id=?").get(req.admin.id);
  if (!(await bcrypt.compare(req.body.current_password || "", row.password_hash))) return res.status(400).send("Current password was not correct.");
  if (String(req.body.new_password || "").length < 10) return res.status(400).send("Use at least 10 characters.");
  db.prepare("UPDATE administrators SET password_hash=?, updated_at=? WHERE id=?").run(await bcrypt.hash(req.body.new_password, 12), now(), req.admin.id);
  audit(req.admin.id, "Changed password", "", req.ip);
  res.redirect("/admin/security");
});

app.get("/admin/backups", requireAdmin, (req, res) => {
  const rows = db.prepare("SELECT * FROM backups ORDER BY created_at DESC").all();
  renderAdmin(req, res, "Backup and Restore", "backups", `<section class="panel"><form method="post" action="/admin/backups/create">${csrf(res)}<button>Create Manual Backup</button></form><p>Backups include the SQLite database and uploaded images. Secret environment files are not included.</p></section><div class="table-list">${rows.map((b) => `<article><h2>${escapeHtml(b.file_name)}</h2><p>${bytes(b.size_bytes)} · ${escapeHtml(b.created_at)}</p><a class="button" href="/admin/backups/${b.id}/download">Download Backup</a></article>`).join("") || "<p>No backups yet.</p>"}</div>`);
});
app.post("/admin/backups/create", requireAdmin, async (req, res, next) => {
  try {
    const backup = await createBackup();
    audit(req.admin.id, "Created backup", backup.file_name, req.ip);
    res.redirect("/admin/backups");
  } catch (e) { next(e); }
});
app.get("/admin/backups/:id/download", requireAdmin, (req, res) => {
  const backup = db.prepare("SELECT * FROM backups WHERE id=?").get(req.params.id);
  if (!backup || !fs.existsSync(backup.file_path)) return res.status(404).send("Backup not found");
  res.download(backup.file_path, backup.file_name);
});

async function createBackup() {
  fs.mkdirSync(config.backupDir, { recursive: true });
  const fileName = `debbie-windler-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.zip`;
  const filePath = path.join(config.backupDir, fileName);
  await new Promise((resolve, reject) => {
    const output = fs.createWriteStream(filePath);
    const archive = archiver("zip", { zlib: { level: 9 } });
    output.on("close", resolve);
    archive.on("error", reject);
    archive.pipe(output);
    archive.file(config.databasePath, { name: "site.sqlite" });
    if (fs.existsSync(config.uploadDir)) archive.directory(config.uploadDir, "uploads");
    archive.finalize();
  });
  const stat = fs.statSync(filePath);
  const result = db.prepare("INSERT INTO backups (file_name, file_path, size_bytes, created_at) VALUES (?, ?, ?, ?)").run(fileName, filePath, stat.size, now());
  return db.prepare("SELECT * FROM backups WHERE id=?").get(result.lastInsertRowid);
}

app.use((err, req, res, next) => {
  console.error(err);
  const message = config.production ? "Something went wrong. Please try again." : err.message;
  res.status(500).send(`<h1>Something went wrong</h1><p>${escapeHtml(message)}</p>`);
});

if (require.main === module) {
  app.listen(config.port, () => {
    console.log(`Debbie Windler Seamstress running at http://localhost:${config.port}`);
  });
}

module.exports = { app, createBackup };
