/**
 * Filesystem side of the media pipeline (PLANNING.md §7).
 *
 * Layout:
 *   MEDIA_ROOT/{page}/{content_hash}/{width}.{ext}   — Apache-served derivatives
 *   STORAGE_ROOT/originals/{content_hash}.{ext}      — normalised originals (not web-served)
 *   STORAGE_ROOT/tmp/…                               — multer's landing zone
 *
 * Content-addressed: the same bytes never regenerate, and a changed image is a
 * new path rather than a cache-busting problem.
 */
import { randomBytes } from 'node:crypto';
import { createReadStream, mkdirSync } from 'node:fs';
import { mkdir, open, rename, rm, writeFile, access, stat } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { env } from '../../env';

export const tmpDir = (): string => join(env.STORAGE_ROOT, 'tmp');
const originalsDir = (): string => join(env.STORAGE_ROOT, 'originals');

// The upload landing zone + originals store must exist before multer runs.
mkdirSync(tmpDir(), { recursive: true });
mkdirSync(originalsDir(), { recursive: true });

/** Absolute path of a derivative from its ASSET_BASE_URL-relative path. */
export function derivativeFsPath(relPath: string): string {
  return join(env.MEDIA_ROOT, relPath);
}

/** The directory holding every derivative of one image. */
export function imageDir(page: string, contentHash: string): string {
  return join(env.MEDIA_ROOT, page, contentHash);
}

async function exists(path: string): Promise<boolean> {
  try {
    await access(path);
    return true;
  } catch {
    return false;
  }
}

/** Write a derivative atomically (temp file + rename). Returns the abs path. */
export async function writeDerivative(relPath: string, data: Buffer): Promise<string> {
  const dest = derivativeFsPath(relPath);
  await mkdir(dirname(dest), { recursive: true });
  const tmp = `${dest}.${randomBytes(6).toString('hex')}.part`;
  await writeFile(tmp, data);
  await rename(tmp, dest);
  return dest;
}

export function derivativeExists(relPath: string): Promise<boolean> {
  return exists(derivativeFsPath(relPath));
}

/** Move the normalised original from a tmp path into STORAGE_ROOT/originals. */
export async function storeOriginal(
  tmpPath: string,
  contentHash: string,
  ext: string,
): Promise<void> {
  await mkdir(originalsDir(), { recursive: true });
  const dest = join(originalsDir(), `${contentHash}.${ext}`);
  if (await exists(dest)) {
    await rm(tmpPath, { force: true });
    return;
  }
  try {
    await rename(tmpPath, dest);
  } catch {
    // rename across devices fails on some hosts — fall back to copy.
    await writeFile(dest, await streamToBuffer(tmpPath));
    await rm(tmpPath, { force: true });
  }
}

export function originalPath(contentHash: string, ext: string): string {
  return join(originalsDir(), `${contentHash}.${ext}`);
}

/** Find a stored original by hash regardless of its extension. */
export async function resolveOriginal(contentHash: string): Promise<string | null> {
  for (const ext of ['jpg', 'png', 'webp', 'avif', 'tiff', 'gif']) {
    const p = originalPath(contentHash, ext);
    if (await exists(p)) return p;
  }
  return null;
}

/** Best-effort tmp cleanup — safe to call on any code path. */
export async function cleanupTmp(path: string | undefined): Promise<void> {
  if (path) await rm(path, { force: true }).catch(() => undefined);
}

/** Hard-remove every derivative of one image (the 30-day GC, PLANNING.md §6c). */
export async function removeImageDir(page: string, contentHash: string): Promise<void> {
  await rm(imageDir(page, contentHash), { recursive: true, force: true });
}

export async function fileSize(path: string): Promise<number> {
  return (await stat(path)).size;
}

/** Read the first `n` bytes of a file — for magic-byte sniffing (§7). */
export async function readHead(path: string, n: number): Promise<Buffer> {
  const fh = await open(path, 'r');
  try {
    const { buffer, bytesRead } = await fh.read({ buffer: Buffer.alloc(n), position: 0 });
    return buffer.subarray(0, bytesRead);
  } finally {
    await fh.close();
  }
}

function streamToBuffer(path: string): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    createReadStream(path)
      .on('data', (c) => chunks.push(c as Buffer))
      .on('end', () => resolve(Buffer.concat(chunks)))
      .on('error', reject);
  });
}
