"use server";

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

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

async function generateThumbnail(
  buffer: Buffer,
  absDir: string,
  filename: string,
  mime: string,
): Promise<{ thumbnailPath: string; width: number; height: number } | null> {
  if (!SUPPORTS_THUMB.has(mime)) return null;
  try {
    const pipeline = sharp(buffer, { failOn: "none" }).rotate();
    const meta = await pipeline.metadata();
    const thumbName = filename.replace(/\.[a-z0-9]+$/i, "") + ".thumb.webp";
    const thumbAbs = path.join(absDir, thumbName);
    await pipeline
      .resize({ width: THUMB_WIDTH, withoutEnlargement: true })
      .webp({ quality: 78 })
      .toFile(thumbAbs);
    return {
      thumbnailPath: thumbName,
      width: meta.width ?? 0,
      height: meta.height ?? 0,
    };
  } catch (err) {
    console.error("[upload] thumbnail generation failed", err);
    return null;
  }
}

const MAX_BYTES = 8 * 1024 * 1024; // 8 MB cap — plenty for cover images, rejects dumps
const ALLOWED_MIME = new Set([
  "image/jpeg",
  "image/png",
  "image/webp",
  "image/gif",
  "image/svg+xml",
]);
const MIME_TO_EXT: Record<string, string> = {
  "image/jpeg": "jpg",
  "image/png": "png",
  "image/webp": "webp",
  "image/gif": "gif",
  "image/svg+xml": "svg",
};

export type UploadResult =
  | { ok: true; url: string; sizeBytes: number; mime: string }
  | { ok: false; error: string };

const folderSchema = z
  .string()
  .regex(/^[a-z0-9-]{1,32}$/i, "invalid folder")
  .default("misc");

/**
 * Accept a single image from an admin form, dedupe by SHA-256, store it at
 * /public/uploads/{folder}/{yyyy-mm}/{hash}.{ext}, and return the resolved
 * public URL. Safe to call repeatedly — identical files won't be rewritten.
 *
 * Usage from a client component:
 *   const formData = new FormData();
 *   formData.set("file", fileBlob);
 *   formData.set("folder", "blog");
 *   const result = await uploadAdminImage(formData);
 *   if (result.ok) setCoverImage(result.url);
 */
export async function uploadAdminImage(formData: FormData): Promise<UploadResult> {
  const session = await requireAdmin();
  const adminId = (session.user as { id?: string }).id!;

  const file = formData.get("file");
  if (!(file instanceof File)) {
    return { ok: false, error: "no file" };
  }

  const folderRaw = formData.get("folder");
  const folderResult = folderSchema.safeParse(
    typeof folderRaw === "string" && folderRaw ? folderRaw : "misc",
  );
  if (!folderResult.success) {
    return { ok: false, error: "invalid folder" };
  }
  const folder = folderResult.data;

  if (file.size === 0) return { ok: false, error: "empty file" };
  if (file.size > MAX_BYTES) {
    return { ok: false, error: `file exceeds ${MAX_BYTES / 1024 / 1024} MB` };
  }
  if (!ALLOWED_MIME.has(file.type)) {
    return { ok: false, error: `unsupported type: ${file.type || "unknown"}` };
  }

  const buffer = Buffer.from(await file.arrayBuffer());
  const hash = createHash("sha256").update(buffer).digest("hex").slice(0, 16);
  const ext = MIME_TO_EXT[file.type] ?? "bin";

  const now = new Date();
  const yyyyMm = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
  const dir = path.join(process.cwd(), "public", "uploads", folder, yyyyMm);
  const filename = `${hash}.${ext}`;
  const fullPath = path.join(dir, filename);
  const publicUrl = `/uploads/${folder}/${yyyyMm}/${filename}`;

  await fs.mkdir(dir, { recursive: true });

  // Deduping: if the target file already exists with the same hash prefix, skip.
  try {
    await fs.access(fullPath);
  } catch {
    await fs.writeFile(fullPath, buffer);
  }

  const sha256Full = createHash("sha256").update(buffer).digest("hex");
  const originalName =
    typeof (file as { name?: string }).name === "string"
      ? (file as { name: string }).name
      : "";

  const thumb = await generateThumbnail(buffer, dir, filename, file.type);
  const thumbnailPublicUrl = thumb
    ? `/uploads/${folder}/${yyyyMm}/${thumb.thumbnailPath}`
    : null;

  try {
    await db
      .insert(mediaAssets)
      .values({
        path: publicUrl,
        thumbnailPath: thumbnailPublicUrl,
        originalName,
        mimeType: file.type,
        fileSize: file.size,
        width: thumb?.width,
        height: thumb?.height,
        sha256: sha256Full,
        uploadedBy: adminId,
      })
      .onConflictDoUpdate({
        target: mediaAssets.path,
        set: {
          thumbnailPath: thumbnailPublicUrl,
          width: thumb?.width,
          height: thumb?.height,
        },
      });
  } catch (err) {
    console.error("[upload] media_assets insert failed", err);
  }

  await logAdminAction({
    adminUserId: adminId,
    action: "upload_image",
    metadata: {
      url: publicUrl,
      folder,
      sizeBytes: file.size,
      mime: file.type,
    },
  });

  return {
    ok: true,
    url: publicUrl,
    sizeBytes: file.size,
    mime: file.type,
  };
}
