"use server";

import { createHash } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import sharp from "sharp";
import { revalidatePath } from "next/cache";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { db } from "@/lib/db/client";
import { mediaAssets } from "@/lib/db/schema";
import { requireAdmin } from "@/lib/auth/admin-guard";
import { logAdminAction } from "@/lib/audit";

const THUMB_WIDTH = 320;
const THUMB_MIMES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);

async function ensureThumbnail(
  absPath: string,
  mime: string,
): Promise<{ thumbnailRel: string; width: number; height: number } | null> {
  if (!THUMB_MIMES.has(mime)) return null;
  const dir = path.dirname(absPath);
  const base = path.basename(absPath).replace(/\.[a-z0-9]+$/i, "");
  const thumbName = `${base}.thumb.webp`;
  const thumbAbs = path.join(dir, thumbName);
  try {
    const buf = await fs.readFile(absPath);
    const pipeline = sharp(buf, { failOn: "none" }).rotate();
    const meta = await pipeline.metadata();
    await pipeline
      .resize({ width: THUMB_WIDTH, withoutEnlargement: true })
      .webp({ quality: 78 })
      .toFile(thumbAbs);
    const rel = thumbAbs
      .slice(path.join(process.cwd(), "public").length)
      .replace(/\\/g, "/");
    return {
      thumbnailRel: rel,
      width: meta.width ?? 0,
      height: meta.height ?? 0,
    };
  } catch (err) {
    console.error("[media-thumb] failed", absPath, err);
    return null;
  }
}

const IMAGE_EXTS = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg"]);

async function walk(dir: string): Promise<string[]> {
  const out: string[] = [];
  let entries: { name: string; isDirectory: () => boolean }[] = [];
  try {
    entries = await fs.readdir(dir, { withFileTypes: true });
  } catch {
    return out;
  }
  for (const e of entries) {
    const full = path.join(dir, e.name);
    if (e.isDirectory()) {
      const nested = await walk(full);
      out.push(...nested);
    } else if (IMAGE_EXTS.has(path.extname(e.name).toLowerCase())) {
      out.push(full);
    }
  }
  return out;
}

function guessMime(ext: string): string {
  const e = ext.toLowerCase();
  if (e === ".jpg" || e === ".jpeg") return "image/jpeg";
  if (e === ".png") return "image/png";
  if (e === ".webp") return "image/webp";
  if (e === ".gif") return "image/gif";
  if (e === ".svg") return "image/svg+xml";
  return "application/octet-stream";
}

/**
 * Walk /public/uploads and insert rows for any image not yet registered.
 * Existing rows are left alone. Use after a bulk file-copy migration.
 */
export async function scanMediaLibrary() {
  const session = await requireAdmin();
  const adminId = (session.user as { id?: string }).id!;

  const root = path.join(process.cwd(), "public", "uploads");
  const files = await walk(root);

  let inserted = 0;
  for (const abs of files) {
    const rel = abs.slice(path.join(process.cwd(), "public").length).replace(/\\/g, "/");
    const existing = await db
      .select({ id: mediaAssets.id })
      .from(mediaAssets)
      .where(eq(mediaAssets.path, rel))
      .limit(1);
    if (existing.length > 0) continue;

    try {
      const stat = await fs.stat(abs);
      const buf = await fs.readFile(abs);
      const sha256 = createHash("sha256").update(buf).digest("hex");
      const mime = guessMime(path.extname(rel));
      const thumb = await ensureThumbnail(abs, mime);
      await db
        .insert(mediaAssets)
        .values({
          path: rel,
          thumbnailPath: thumb?.thumbnailRel,
          originalName: path.basename(rel),
          mimeType: mime,
          fileSize: stat.size,
          width: thumb?.width,
          height: thumb?.height,
          sha256,
          uploadedBy: adminId,
        })
        .onConflictDoUpdate({
          target: mediaAssets.path,
          set: {
            thumbnailPath: thumb?.thumbnailRel,
            width: thumb?.width,
            height: thumb?.height,
          },
        });
      inserted += 1;
    } catch (err) {
      console.error("[media-scan] failed for", rel, err);
    }
  }

  await logAdminAction({
    adminUserId: adminId,
    action: "media_library_scan",
    metadata: { inserted, total: files.length },
  });
  revalidatePath("/admin/media");
}

const updateMetaSchema = z.object({
  id: z.string().uuid(),
  altText: z.string().max(500).optional().default(""),
  caption: z.string().max(1000).optional().default(""),
});

export async function updateMediaMeta(formData: FormData) {
  const session = await requireAdmin();
  const adminId = (session.user as { id?: string }).id!;
  const parsed = updateMetaSchema.parse({
    id: formData.get("id"),
    altText: formData.get("altText") ?? "",
    caption: formData.get("caption") ?? "",
  });
  await db
    .update(mediaAssets)
    .set({ altText: parsed.altText, caption: parsed.caption })
    .where(eq(mediaAssets.id, parsed.id));
  await logAdminAction({
    adminUserId: adminId,
    action: "media_update_meta",
    targetId: parsed.id,
  });
  revalidatePath("/admin/media");
}

export async function deleteMediaAsset(formData: FormData) {
  const session = await requireAdmin();
  const adminId = (session.user as { id?: string }).id!;
  const id = String(formData.get("id") ?? "");
  if (!id) throw new Error("id required");
  const [row] = await db
    .select({ path: mediaAssets.path })
    .from(mediaAssets)
    .where(eq(mediaAssets.id, id))
    .limit(1);
  if (!row) return;
  const abs = path.join(process.cwd(), "public", row.path);
  try {
    await fs.unlink(abs);
  } catch {
    // Best-effort — file may already be gone.
  }
  await db.delete(mediaAssets).where(eq(mediaAssets.id, id));
  await logAdminAction({
    adminUserId: adminId,
    action: "media_delete",
    targetId: id,
    metadata: { path: row.path },
  });
  revalidatePath("/admin/media");
}
