import { createHash } from "crypto";
import { mkdir, writeFile } from "fs/promises";
import path from "path";

/** Uploads live beside db.json, so one folder is the whole backup. */
export const UPLOAD_DIR = path.join(process.cwd(), "data", "uploads");

const SIGNATURES: { ext: string; mime: string; test: (b: Buffer) => boolean }[] = [
  { ext: "jpg", mime: "image/jpeg", test: (b) => b[0] === 0xff && b[1] === 0xd8 },
  { ext: "png", mime: "image/png", test: (b) => b[0] === 0x89 && b.subarray(1, 4).toString() === "PNG" },
  { ext: "gif", mime: "image/gif", test: (b) => b.subarray(0, 3).toString() === "GIF" },
  { ext: "webp", mime: "image/webp", test: (b) => b.subarray(0, 4).toString() === "RIFF" && b.subarray(8, 12).toString() === "WEBP" },
  { ext: "mp4", mime: "video/mp4", test: (b) => b.subarray(4, 8).toString() === "ftyp" },
  { ext: "webm", mime: "video/webm", test: (b) => b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3 },
];

/**
 * Settings, but only if the database is already open.
 *
 * getDb() runs a media migration on start-up that calls into this file.
 * Awaiting getDb() from here waits on a build that is waiting on us — a
 * deadlock, and before the database was built once per boot, a recursion
 * that made a fresh copy for every file it touched until the heap gave
 * out. During start-up the defaults are the right answer anyway.
 */
function settingsIfOpen(): Record<string, unknown> | null {
  const g = globalThis as unknown as {
    __notrdb?: { data?: { siteSettings?: Record<string, unknown> } };
  };
  return g.__notrdb?.data?.siteSettings ?? null;
}

/**
 * The public address for a stored file. With a CDN configured, files are
 * served from there rather than through this server.
 */
export async function publicUrlFor(name: string) {
  const cdn = String(settingsIfOpen()?.uploadsCdnUrl ?? "").trim();
  return cdn ? `${cdn.replace(/\/+$/, "")}/${name}` : `/media/${name}`;
}

/** Where uploads are kept, which an admin can move. */
export async function uploadDir() {
  const configured = String(settingsIfOpen()?.uploadsDirectory ?? "").trim();
  // Kept inside the project, so a stray value can't write anywhere on disk.
  const safe = configured.replace(/\.\./g, "").replace(/^\/+/, "");
  return safe ? path.join(process.cwd(), safe) : UPLOAD_DIR;
}

/**
 * Identifies a file by its magic bytes rather than trusting the name or the
 * declared type, then writes it under a content-hashed filename.
 */
export async function saveUpload(buffer: Buffer) {
  const match = SIGNATURES.find((s) => s.test(buffer));
  if (!match) return null;

  const isVideo = match.mime.startsWith("video/");
  const dir = await uploadDir();
  await mkdir(dir, { recursive: true });

  // Photos are watermarked before they're stored, so every copy carries it.
  // This happens before hashing so the name matches what's actually saved.
  const { applyWatermark } = await import("./watermark");
  const stored = await applyWatermark(buffer, match.mime);

  // Hashing means the same file uploaded twice costs one copy.
  const hash = createHash("sha1").update(stored).digest("hex").slice(0, 16);
  const name = `${hash}.${match.ext}`;

  // With a bucket configured, the file goes there and the URL points at it;
  // otherwise it stays on disk and is served by the media route.
  const remote = await putToBucket(name, stored, match.mime);
  if (remote) {
    return { name, url: remote, mime: match.mime, isVideo, bytes: stored.length };
  }

  await writeFile(path.join(dir, name), buffer);

  return { name, url: await publicUrlFor(name), mime: match.mime, isVideo, bytes: stored.length };
}

/** Turns a data URL into a stored file, for migrating older records. */
export async function saveDataUrl(dataUrl: string) {
  const m = /^data:([^;]+);base64,([\s\S]+)$/.exec(dataUrl);
  if (!m) return null;
  return saveUpload(Buffer.from(m[2], "base64"));
}

export const MIME_BY_EXT: Record<string, string> = {
  jpg: "image/jpeg",
  png: "image/png",
  gif: "image/gif",
  webp: "image/webp",
  mp4: "video/mp4",
  webm: "video/webm",
};


/**
 * Uploads to object storage when one is configured. Returns the public URL,
 * or null to fall back to local disk — a storage misconfiguration shouldn't
 * lose someone's upload.
 */
async function putToBucket(
  name: string,
  body: Buffer,
  contentType: string
): Promise<string | null> {
  /*
   * The database is read only if it is already open.
   *
   * getDb() runs a media migration that calls this function, so awaiting
   * getDb() here waits for a build that is waiting for us. Before the
   * database was built once per boot that recursion built a fresh copy
   * for every data: URL it found -- thousands of them, and the heap with
   * it. During start-up there is no bucket to ask about anyway: media
   * written then goes to local disk, which is what the caller falls back
   * to.
   */
  const g = globalThis as unknown as {
    __notrdb?: {
      data?: {
        storageSettings?: {
          driver?: string;
          bucket?: string;
          accessKeyId?: string;
          secretAccessKey: string;
          region?: string;
          endpoint?: string;
          forcePathStyle?: boolean;
          publicUrl?: string;
        };
      };
    };
  };
  const s = g.__notrdb?.data?.storageSettings;

  if (!s || s.driver !== "s3" || !s.bucket || !s.accessKeyId) return null;

  try {
    const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");

    const client = new S3Client({
      region: s.region || "us-east-1",
      endpoint: s.endpoint || undefined,
      forcePathStyle: s.forcePathStyle,
      credentials: {
        accessKeyId: s.accessKeyId,
        secretAccessKey: s.secretAccessKey,
      },
    });

    await client.send(
      new PutObjectCommand({
        Bucket: s.bucket,
        Key: `media/${name}`,
        Body: body,
        ContentType: contentType,
        // Content-hashed names never change, so they can be cached forever.
        CacheControl: "public, max-age=31536000, immutable",
      })
    );

    if (s.publicUrl) return `${s.publicUrl.replace(/\/$/, "")}/media/${name}`;
    if (s.endpoint) {
      return `${s.endpoint.replace(/\/$/, "")}/${s.bucket}/media/${name}`;
    }
    return `https://${s.bucket}.s3.${s.region}.amazonaws.com/media/${name}`;
  } catch {
    // Fall back to local rather than failing the upload.
    return null;
  }
}
