/**
 * Per-branch / per-PR preview URLs (migrations 0062, 0077).
 *
 *   GET  /:owner/:repo/previews               — list view
 *   GET  /:owner/:repo/pull/:prNumber/preview — redirect to live preview for this PR
 *   GET  /previews/:owner/:repo/:prBranch/*   — serve built static files
 *   POST /api/previews/rebuild/:prId          — trigger a rebuild (webhook)
 *
 * All page-local CSS is scoped under `.preview-*` so it can't bleed
 * into the shared layout (per CLAUDE.md: do NOT modify shared
 * layout/components/ui). Mirrors the gradient hairline + orb pattern
 * used by environments.tsx / admin-integrations.tsx.
 */

import { Hono } from "hono";
import { and, eq, desc } from "drizzle-orm";
import { db } from "../db";
import { repositories, users, pullRequests, prPreviews } from "../db/schema";
import { softAuth, requireAuth } from "../middleware/auth";
import type { AuthEnv } from "../middleware/auth";
import { Layout } from "../views/layout";
import { RepoHeader, RepoNav } from "../views/components";
import { getUnreadCount } from "../lib/unread";
import {
  formatExpiresIn,
  listPreviewsForRepo,
  previewStatusLabel,
} from "../lib/branch-previews";
import { buildPreview } from "../lib/preview-builder";
import { join } from "path";
import { existsSync } from "fs";

/** Minimal MIME type lookup for common web assets. */
function getMimeType(filePath: string): string {
  const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
  const map: Record<string, string> = {
    html: "text/html; charset=utf-8",
    htm: "text/html; charset=utf-8",
    css: "text/css",
    js: "application/javascript",
    mjs: "application/javascript",
    json: "application/json",
    png: "image/png",
    jpg: "image/jpeg",
    jpeg: "image/jpeg",
    gif: "image/gif",
    svg: "image/svg+xml",
    ico: "image/x-icon",
    woff: "font/woff",
    woff2: "font/woff2",
    ttf: "font/ttf",
    txt: "text/plain",
    xml: "application/xml",
    webp: "image/webp",
    avif: "image/avif",
    mp4: "video/mp4",
    webm: "video/webm",
    pdf: "application/pdf",
    wasm: "application/wasm",
  };
  return map[ext] || "application/octet-stream";
}

const r = new Hono<AuthEnv>();
r.use("*", softAuth);

async function loadRepo(owner: string, repo: string) {
  try {
    const [row] = await db
      .select({
        id: repositories.id,
        name: repositories.name,
        defaultBranch: repositories.defaultBranch,
        ownerId: repositories.ownerId,
        starCount: repositories.starCount,
        forkCount: repositories.forkCount,
        previewBuildsEnabled: repositories.previewBuildsEnabled,
      })
      .from(repositories)
      .innerJoin(users, eq(repositories.ownerId, users.id))
      .where(and(eq(users.username, owner), eq(repositories.name, repo)))
      .limit(1);
    return row || null;
  } catch (err) {
    console.error("[previews] loadRepo failed:", err);
    return null;
  }
}

/* ─────────────────────────────────────────────────────────────────────────
 * Scoped CSS — every class prefixed `.preview-*` so this page can't bleed
 * into the layout. Same gradient hairline + orb language as environments.
 * ───────────────────────────────────────────────────────────────────── */
const previewStyles = `
  .preview-wrap { max-width: 1680px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }

  .preview-head {
    position: relative;
    margin-bottom: var(--space-5);
    padding: var(--space-4) var(--space-5);
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: 14px;
    overflow: hidden;
  }
  .preview-head::before {
    content: '';
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 2px;
    background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
    opacity: 0.7;
    pointer-events: none;
  }
  .preview-head-orb {
    position: absolute;
    inset: -30% -10% auto auto;
    width: 320px; height: 320px;
    background: radial-gradient(circle, rgba(91,110,232,0.18), rgba(95,143,160,0.08) 45%, transparent 70%);
    filter: blur(70px);
    opacity: 0.7;
    pointer-events: none;
    z-index: 0;
  }
  .preview-head-inner {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
  }
  .preview-head-text { flex: 1; min-width: 240px; max-width: 720px; }
  .preview-eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--text-muted);
    margin-bottom: 10px;
  }
  .preview-eyebrow-dot {
    width: 8px; height: 8px;
    border-radius: 9999px;
    background: linear-gradient(135deg, #5b6ee8, #5f8fa0);
    box-shadow: 0 0 0 3px rgba(91,110,232,0.18);
  }
  .preview-title {
    margin: 0 0 6px;
    font-family: var(--font-display);
    font-size: clamp(22px, 2.6vw, 30px);
    font-weight: 800;
    letter-spacing: -0.022em;
    line-height: 1.1;
    color: var(--text-strong);
  }
  .preview-title-grad {
    background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }
  .preview-sub {
    margin: 0;
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--text-muted);
  }

  .preview-col-title {
    margin: 0 0 var(--space-2);
    font-family: var(--font-mono);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    font-weight: 600;
    color: var(--text-muted);
  }

  .preview-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin-bottom: var(--space-5);
  }

  .preview-card {
    position: relative;
    padding: var(--space-4) var(--space-4);
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: 14px;
    overflow: hidden;
    transition: transform 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
  }
  .preview-card::before {
    content: '';
    position: absolute;
    top: 0; left: 14px; right: 14px;
    height: 1px;
    background: linear-gradient(90deg, transparent 0%, rgba(91,110,232,0.45) 30%, rgba(95,143,160,0.45) 70%, transparent 100%);
    opacity: 0;
    transition: opacity 160ms ease;
  }
  .preview-card:hover {
    transform: translateY(-1px);
    border-color: rgba(91,110,232,0.32);
    box-shadow: 0 8px 22px -10px rgba(0,0,0,0.40);
  }
  .preview-card:hover::before { opacity: 1; }

  .preview-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-2);
  }
  .preview-card-titles { flex: 1; min-width: 200px; }
  .preview-card-branch {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 15px;
    font-weight: 700;
    color: var(--text-strong);
    word-break: break-all;
  }
  .preview-card-url {
    margin-top: 4px;
    font-family: var(--font-mono);
    font-size: 12.5px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .preview-card-url a { color: var(--accent, #5b6ee8); text-decoration: none; }
  .preview-card-url a:hover { color: var(--accent, #5f8fa0); text-decoration: underline; }
  .preview-card-meta {
    margin-top: 6px;
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    font-size: 11.5px;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
  }
  .preview-card-meta code {
    font-family: var(--font-mono);
    font-size: 11.5px;
    color: var(--text);
    background: rgba(255,255,255,0.04);
    padding: 1px 6px;
    border-radius: 6px;
  }

  /* ─── status pills ─── */
  .preview-pill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 9px;
    border-radius: 9999px;
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    font-family: var(--font-mono);
    background: rgba(255,255,255,0.04);
    color: var(--text-muted);
    box-shadow: inset 0 0 0 1px var(--border);
  }
  .preview-pill.is-building {
    background: rgba(251,191,36,0.10);
    color: #fde68a;
    box-shadow: inset 0 0 0 1px rgba(251,191,36,0.30);
  }
  .preview-pill.is-ready {
    background: rgba(52,211,153,0.10);
    color: #6ee7b7;
    box-shadow: inset 0 0 0 1px rgba(52,211,153,0.30);
  }
  .preview-pill.is-failed {
    background: rgba(248,113,113,0.10);
    color: #fecaca;
    box-shadow: inset 0 0 0 1px rgba(248,113,113,0.35);
  }
  .preview-pill.is-expired {
    background: rgba(100,116,139,0.10);
    color: #94a3b8;
    box-shadow: inset 0 0 0 1px rgba(100,116,139,0.28);
  }

  /* ─── expired card treatment ─── */
  .preview-card.is-expired .preview-card-branch { color: var(--text-muted); }
  .preview-card.is-expired .preview-card-url-expired {
    color: var(--text-muted);
    text-decoration: line-through;
    text-decoration-color: rgba(148,163,184,0.45);
  }
  .preview-rebuild-btn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    margin-top: var(--space-2);
    padding: 4px 12px;
    border-radius: 8px;
    font-size: 12px;
    font-weight: 600;
    font-family: var(--font-mono);
    color: #5b6ee8;
    background: rgba(91,110,232,0.08);
    border: 1px solid rgba(91,110,232,0.22);
    cursor: pointer;
    text-decoration: none;
    transition: background 120ms ease, border-color 120ms ease;
  }
  .preview-rebuild-btn:hover {
    background: rgba(91,110,232,0.15);
    border-color: rgba(91,110,232,0.40);
  }
  .preview-pill-dot {
    width: 6px; height: 6px;
    border-radius: 9999px;
    background: currentColor;
  }
  .preview-pill.is-building .preview-pill-dot {
    animation: previewPulse 1.4s ease-in-out infinite;
  }
  @keyframes previewPulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
  }

  .preview-error {
    margin-top: var(--space-2);
    padding: 8px 12px;
    border-radius: 8px;
    font-family: var(--font-mono);
    font-size: 12px;
    color: #fecaca;
    background: rgba(248,113,113,0.06);
    border: 1px solid rgba(248,113,113,0.30);
    white-space: pre-wrap;
    word-break: break-word;
  }

  /* ─── empty state ─── */
  .preview-empty {
    position: relative;
    padding: var(--space-6) var(--space-5);
    background: var(--bg-elevated);
    border: 1px dashed var(--border-strong, var(--border));
    border-radius: 14px;
    text-align: center;
    overflow: hidden;
    margin-bottom: var(--space-5);
  }
  .preview-empty-orb {
    position: absolute;
    inset: auto auto -40% 50%;
    transform: translateX(-50%);
    width: 320px; height: 320px;
    background: radial-gradient(circle, rgba(91,110,232,0.18), rgba(95,143,160,0.08) 45%, transparent 70%);
    filter: blur(70px);
    opacity: 0.7;
    pointer-events: none;
  }
  .preview-empty-inner { position: relative; z-index: 1; max-width: 460px; margin: 0 auto; }
  .preview-empty-icon {
    width: 44px; height: 44px;
    margin: 0 auto var(--space-3);
    border-radius: 14px;
    background: linear-gradient(135deg, rgba(91,110,232,0.18), rgba(95,143,160,0.14));
    box-shadow: inset 0 0 0 1px rgba(91,110,232,0.32);
    display: flex; align-items: center; justify-content: center;
    color: #5b6ee8;
  }
  .preview-empty-title {
    font-family: var(--font-display);
    font-size: 16px;
    font-weight: 700;
    color: var(--text-strong);
    margin: 0 0 6px;
    letter-spacing: -0.01em;
  }
  .preview-empty-body {
    font-size: 13.5px;
    color: var(--text-muted);
    line-height: 1.5;
    margin: 0 0 var(--space-3);
  }
  .preview-empty code {
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--text);
    background: rgba(255,255,255,0.06);
    padding: 1px 6px;
    border-radius: 6px;
  }

  /* ─── secondary tab strip — only used when RepoNav has no slot ─── */
  .preview-tabbar {
    display: flex;
    gap: 4px;
    margin-bottom: var(--space-3);
    border-bottom: 1px solid var(--border);
    padding-bottom: 0;
  }
  .preview-tabbar a {
    padding: 8px 12px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-muted);
    text-decoration: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
  }
  .preview-tabbar a.is-active {
    color: var(--text-strong);
    border-bottom-color: #5b6ee8;
  }
  .preview-tabbar a:hover { color: var(--text); }
`;

r.get("/:owner/:repo/previews", async (c) => {
  const user = c.get("user");
  const { owner, repo } = c.req.param();
  const repoRow = await loadRepo(owner, repo);
  if (!repoRow) return c.notFound();

  const previews = await listPreviewsForRepo(repoRow.id);
  const unread = user ? await getUnreadCount(user.id) : 0;
  const now = new Date();

  return c.html(
    <Layout
      title={`Previews — ${owner}/${repo}`}
      user={user}
      notificationCount={unread}
    >
      <RepoHeader
        owner={owner}
        repo={repo}
        starCount={repoRow.starCount}
        forkCount={repoRow.forkCount}
        currentUser={user?.username}
      />
      <RepoNav owner={owner} repo={repo} active="code" />

      <div class="preview-wrap">
        <section class="preview-head">
          <div class="preview-head-orb" aria-hidden="true" />
          <div class="preview-head-inner">
            <div class="preview-head-text">
              <div class="preview-eyebrow">
                <span class="preview-eyebrow-dot" aria-hidden="true" />
                Branch previews · {owner}/{repo}
              </div>
              <h2 class="preview-title">
                <span class="preview-title-grad">Previews.</span>
              </h2>
              <p class="preview-sub">
                Every push to a non-default branch gets a unique preview
                URL. Open the URL to see the branch as if it were live —
                no merge required. Previews auto-expire 24 hours after
                the last push.
              </p>
            </div>
          </div>
        </section>

        <nav class="preview-tabbar" aria-label="Repository previews navigation">
          <a href={`/${owner}/${repo}`}>Code</a>
          <a class="is-active" href={`/${owner}/${repo}/previews`}>Previews</a>
          <a href={`/${owner}/${repo}/deployments`}>Deployments</a>
        </nav>

        <h4 class="preview-col-title">
          {previews.length === 0
            ? "No previews yet"
            : `${previews.length} preview${previews.length === 1 ? "" : "s"}`}
        </h4>

        {previews.length === 0 ? (
          <div class="preview-empty">
            <div class="preview-empty-orb" aria-hidden="true" />
            <div class="preview-empty-inner">
              <div class="preview-empty-icon" aria-hidden="true">
                <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                  <circle cx="12" cy="12" r="9" />
                  <path d="M12 7v5l3 2" />
                </svg>
              </div>
              <h3 class="preview-empty-title">Push to a branch to get a preview URL</h3>
              <p class="preview-empty-body">
                Create a branch other than{" "}
                <code>{repoRow.defaultBranch}</code>, push some commits,
                and a unique preview URL will land here within seconds.
                Each preview lasts 24 hours after the last push.
              </p>
            </div>
          </div>
        ) : (
          <div class="preview-list">
            {previews.map((p) => {
              const shortSha = (p.commitSha || "").slice(0, 7);
              const expiresLabel = formatExpiresIn(p.expiresAt, now);
              const isExpired = p.status === "expired";
              const statusKey = p.status as
                | "building"
                | "ready"
                | "failed"
                | "expired";
              const pillClass = `preview-pill is-${statusKey}`;
              const cardClass = isExpired
                ? "preview-card is-expired"
                : "preview-card";
              // Rebuild pushes a branch-preview re-enqueue via the API.
              const rebuildHref = `/${owner}/${repo}/previews/rebuild?branch=${encodeURIComponent(p.branchName)}`;
              return (
                <div class={cardClass}>
                  <div class="preview-card-head">
                    <div class="preview-card-titles">
                      <h3 class="preview-card-branch">{p.branchName}</h3>
                      <div class="preview-card-url">
                        {p.status === "ready" ? (
                          <a href={p.previewUrl} target="_blank" rel="noopener noreferrer">
                            {p.previewUrl}
                          </a>
                        ) : isExpired ? (
                          <span class="preview-card-url-expired">
                            {p.previewUrl}
                          </span>
                        ) : (
                          <span style="color: var(--text-muted)">
                            {p.previewUrl}
                          </span>
                        )}
                      </div>
                      <div class="preview-card-meta">
                        <span>
                          commit <code>{shortSha}</code>
                        </span>
                        <span>
                          {isExpired
                            ? "preview expired · push to branch to rebuild"
                            : `expires in ${expiresLabel}`}
                        </span>
                      </div>
                      {isExpired && (
                        <a
                          href={rebuildHref}
                          class="preview-rebuild-btn"
                          title="Re-enqueue a preview build for this branch"
                        >
                          ↺ Rebuild
                        </a>
                      )}
                    </div>
                    <div>
                      <span class={pillClass}>
                        <span class="preview-pill-dot" aria-hidden="true" />
                        {previewStatusLabel(p.status)}
                      </span>
                    </div>
                  </div>
                  {p.status === "failed" && p.errorMessage && (
                    <div class="preview-error">{p.errorMessage}</div>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </div>

      <style dangerouslySetInnerHTML={{ __html: previewStyles }} />
    </Layout>
  );
});

// ─── PR preview redirect ────────────────────────────────────────────────────
// GET /:owner/:repo/pull/:prNumber/preview
// Redirect to the live preview URL for this PR (from pr_previews table).
// Falls back to the previews list if no ready build exists.
r.get("/:owner/:repo/pull/:prNumber/preview", softAuth, async (c) => {
  const { owner, repo, prNumber } = c.req.param();
  const num = parseInt(prNumber, 10);

  try {
    // Resolve repo
    const [repoRow] = await db
      .select({ id: repositories.id })
      .from(repositories)
      .innerJoin(users, eq(repositories.ownerId, users.id))
      .where(and(eq(users.username, owner), eq(repositories.name, repo)))
      .limit(1);
    if (!repoRow) return c.notFound();

    // Find the PR
    const [pr] = await db
      .select({ id: pullRequests.id })
      .from(pullRequests)
      .where(and(eq(pullRequests.repositoryId, repoRow.id), eq(pullRequests.number, num)))
      .limit(1);
    if (!pr) return c.notFound();

    // Find the most recent ready preview for this PR
    const [preview] = await db
      .select({ previewUrl: prPreviews.previewUrl, status: prPreviews.status })
      .from(prPreviews)
      .where(and(eq(prPreviews.prId, pr.id), eq(prPreviews.status, "ready")))
      .orderBy(desc(prPreviews.id))
      .limit(1);

    if (preview?.previewUrl) {
      return c.redirect(preview.previewUrl, 302);
    }
  } catch (err) {
    console.warn("[previews] PR preview redirect failed:", err instanceof Error ? err.message : err);
  }

  // Fall back to the previews list page
  return c.redirect(`/${owner}/${repo}/previews`, 302);
});

// ─── Static file serving ────────────────────────────────────────────────────
// GET /previews/:owner/:repo/:prBranch/*
// Serves built output from the temp build directory created by preview-builder.
// Only active when PREVIEW_DOMAIN is set (or when the dev fallback applies).
r.get("/previews/:owner/:repo/:prBranch/*", async (c) => {
  const { owner, repo, prBranch } = c.req.param();
  const wildcard = c.req.param("*") || "";

  // Resolve the most recent ready pr_previews row for this branch
  try {
    const [repoRow] = await db
      .select({ id: repositories.id })
      .from(repositories)
      .innerJoin(users, eq(repositories.ownerId, users.id))
      .where(and(eq(users.username, owner), eq(repositories.name, repo)))
      .limit(1);
    if (!repoRow) return c.notFound();

    const [preview] = await db
      .select({
        prId: prPreviews.prId,
        headSha: prPreviews.headSha,
        outputDir: prPreviews.outputDir,
        status: prPreviews.status,
        branchName: prPreviews.branchName,
      })
      .from(prPreviews)
      .where(
        and(
          eq(prPreviews.repoId, repoRow.id),
          eq(prPreviews.status, "ready"),
          eq(prPreviews.branchName, prBranch)
        )
      )
      .orderBy(desc(prPreviews.id))
      .limit(1);

    if (!preview || preview.status !== "ready") {
      return c.text("Preview not ready yet", 404);
    }

    const outputDir = preview.outputDir || "dist";
    // The build dir pattern from preview-builder.ts:
    //   /tmp/previews/<slug(prId)>-<shortSha>/<outputDir>
    // We stored prId in the row, so read it back and reconstruct.
    const PREVIEW_BUILD_DIR = process.env.PREVIEW_BUILD_DIR || "/tmp/previews";
    const shortSha = preview.headSha.slice(0, 8);
    const prIdSlug = preview.prId.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 60);
    const buildBase = `${PREVIEW_BUILD_DIR}/${prIdSlug}-${shortSha}/${outputDir}`;

    let filePath = wildcard ? join(buildBase, wildcard) : join(buildBase, "index.html");

    // Prevent directory traversal
    if (!filePath.startsWith(buildBase)) {
      return c.text("Forbidden", 403);
    }

    // Directory → serve index.html
    if (existsSync(filePath) && !filePath.endsWith("/")) {
      // Check if it's a directory
      try {
        const stat = await Bun.file(filePath).exists();
        if (!stat && existsSync(join(filePath, "index.html"))) {
          filePath = join(filePath, "index.html");
        }
      } catch {}
    }

    if (!existsSync(filePath)) {
      // Try with index.html appended
      const indexPath = join(filePath, "index.html");
      if (existsSync(indexPath)) {
        filePath = indexPath;
      } else {
        return c.text("File not found", 404);
      }
    }

    const file = Bun.file(filePath);
    const contentType = getMimeType(filePath);

    return new Response(file, {
      headers: {
        "Content-Type": contentType,
        "Cache-Control": "public, max-age=300",
        "X-Preview-Sha": preview.headSha.slice(0, 8),
      },
    });
  } catch (err) {
    console.warn("[previews] static serve failed:", err instanceof Error ? err.message : err);
    return c.text("Preview unavailable", 500);
  }
});

// ─── Rebuild trigger API ─────────────────────────────────────────────────────
// POST /api/previews/rebuild/:prId
// Triggers a fresh build for the given PR. Auth required (repo write access).
r.post("/api/previews/rebuild/:prId", requireAuth, async (c) => {
  const { prId } = c.req.param();
  const user = c.get("user");

  try {
    const [pr] = await db
      .select({
        id: pullRequests.id,
        repositoryId: pullRequests.repositoryId,
        headBranch: pullRequests.headBranch,
        authorId: pullRequests.authorId,
      })
      .from(pullRequests)
      .where(eq(pullRequests.id, prId))
      .limit(1);

    if (!pr) return c.json({ error: "PR not found" }, 404);

    // Only the PR author or repo owner can trigger a rebuild
    if (user!.id !== pr.authorId) {
      const [repo] = await db
        .select({ ownerId: repositories.ownerId })
        .from(repositories)
        .where(eq(repositories.id, pr.repositoryId))
        .limit(1);
      if (!repo || repo.ownerId !== user!.id) {
        return c.json({ error: "Unauthorized" }, 403);
      }
    }

    // Get the current head SHA from the most recent preview row, or use a sentinel
    const [existing] = await db
      .select({ headSha: prPreviews.headSha })
      .from(prPreviews)
      .where(eq(prPreviews.prId, prId))
      .orderBy(desc(prPreviews.id))
      .limit(1);

    const headSha = existing?.headSha ?? "unknown";

    // Fire-and-forget rebuild
    buildPreview(prId, pr.repositoryId, headSha).catch((err) =>
      console.warn("[previews] rebuild failed:", err instanceof Error ? err.message : err)
    );

    return c.json({ ok: true, message: "Rebuild triggered" });
  } catch (err) {
    console.warn("[previews] rebuild endpoint failed:", err instanceof Error ? err.message : err);
    return c.json({ error: "Internal error" }, 500);
  }
});

export default r;
