import { NextResponse } from "next/server";
import { eq } from "drizzle-orm";

import { db } from "@turbostarter/db/server";
import { mesh } from "@turbostarter/db/schema/mesh";

/**
 * `PATCH /api/cli/meshes/:slug` — rename a mesh from the CLI.
 *
 * The `myRouter` (Hono) at `/api/my/meshes/*` runs `enforceAuth`, which
 * calls `auth.api.getSession()` — that only honours better-auth cookies.
 * The CLI's JWT (issued by `/api/auth/cli/device-code/[code]/approve`)
 * is a custom HS256 token signed with `CLI_SYNC_SECRET`, so it can't
 * authenticate against `/api/my/*`. Until better-auth gets a bearer
 * plugin wired up, CLI-only mutations live under `/api/cli/*` and
 * validate the JWT inline — same pattern as `/api/cli-sync-token`.
 */

interface CliJwtPayload {
  sub: string;
  email?: string;
  type?: string;
  exp?: number;
}

function base64UrlDecode(input: string): Uint8Array {
  const pad = input.length % 4 === 0 ? "" : "=".repeat(4 - (input.length % 4));
  const b64 = (input + pad).replace(/-/g, "+").replace(/_/g, "/");
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return bytes;
}

async function verifyCliJwt(token: string, secret: string): Promise<CliJwtPayload | null> {
  const parts = token.split(".");
  if (parts.length !== 3) return null;
  const [headerB64, payloadB64, sigB64] = parts;
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["verify"],
  );
  const ok = await crypto.subtle.verify(
    "HMAC",
    key,
    base64UrlDecode(sigB64!),
    encoder.encode(`${headerB64}.${payloadB64}`),
  );
  if (!ok) return null;
  try {
    const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(payloadB64!))) as CliJwtPayload;
    if (payload.exp && Date.now() / 1000 > payload.exp) return null;
    return payload;
  } catch {
    return null;
  }
}

export async function PATCH(
  request: Request,
  { params }: { params: Promise<{ slug: string }> },
) {
  const auth = request.headers.get("authorization");
  if (!auth?.startsWith("Bearer ")) {
    return NextResponse.json({ error: "missing bearer token" }, { status: 401 });
  }
  const token = auth.slice("Bearer ".length).trim();
  const secret = process.env.CLI_SYNC_SECRET;
  if (!secret) {
    return NextResponse.json({ error: "server not configured" }, { status: 500 });
  }
  const payload = await verifyCliJwt(token, secret);
  if (!payload) {
    return NextResponse.json({ error: "invalid or expired token" }, { status: 401 });
  }

  const { slug } = await params;
  let body: { slug?: string };
  try {
    body = (await request.json()) as { slug?: string };
  } catch {
    return NextResponse.json({ error: "invalid JSON body" }, { status: 400 });
  }
  const newSlug = body.slug?.trim();
  if (!newSlug) {
    return NextResponse.json({ error: "slug is required" }, { status: 400 });
  }
  // Slug regex matches the CLI's pre-flight check. Lowercase only,
  // must start with alnum, may contain hyphens, 2-32 chars total.
  // Slugs are NOT globally unique (mesh.id is canonical) — see schema
  // comment on mesh.slug — so we don't enforce a uniqueness collision
  // here. Local CLI configs key on slug, so the picker collides
  // locally; that's the user's call.
  if (!/^[a-z0-9][a-z0-9-]{1,31}$/.test(newSlug)) {
    return NextResponse.json(
      { error: "slug must be 2-32 chars, lowercase alnum + hyphens, start with alnum" },
      { status: 400 },
    );
  }

  // Look up the mesh first so we can distinguish "doesn't exist"
  // (404) from "exists but you don't own it" (403). The CLI was
  // collapsing both into a bare "API error 404" — unhelpful when
  // the user has multiple accounts and signs in to the wrong one.
  const [existing] = await db
    .select({ ownerUserId: mesh.ownerUserId })
    .from(mesh)
    .where(eq(mesh.slug, slug))
    .limit(1);

  if (!existing) {
    return NextResponse.json(
      { error: `mesh "${slug}" not found` },
      { status: 404 },
    );
  }
  if (existing.ownerUserId !== payload.sub) {
    return NextResponse.json(
      {
        error: `you are signed in as a different account than the owner of "${slug}". Run \`claudemesh logout && claudemesh login\` and pick the owning account.`,
      },
      { status: 403 },
    );
  }

  // Soft-collapse: name and slug are a single concept user-facing,
  // so we always sync name = slug. mesh.name column stays for now
  // (avoids touching ~25 reader sites); a future migration drops it.
  //
  // The DB has a unique constraint on mesh.slug (mesh_slug_unique)
  // even though the schema comment incorrectly claims "NOT unique" —
  // catch the duplicate-key error and surface it as 409 instead of
  // a generic 500.
  try {
    const [updated] = await db
      .update(mesh)
      .set({ slug: newSlug, name: newSlug })
      .where(eq(mesh.slug, slug))
      .returning({ slug: mesh.slug });
    return NextResponse.json(updated);
  } catch (e) {
    const err = e as { cause?: { constraint_name?: string; code?: string } };
    if (err.cause?.constraint_name === "mesh_slug_unique" || err.cause?.code === "23505") {
      return NextResponse.json(
        { error: `slug "${newSlug}" is already taken` },
        { status: 409 },
      );
    }
    throw e;
  }
}
