const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const { TextDecoder } = require("util");

const { db } = require("./db");
const { getSetting, setSetting } = require("./settings");
const { safeDownloadFilename } = require("./upload-security");

const DATA_DIR = path.join(__dirname, "..", "..", "data", "content-library");
const FILES_DIR = path.join(DATA_DIR, "files");
const INCOMING_DIR = path.join(DATA_DIR, ".incoming");
const TRASH_DIR = path.join(DATA_DIR, ".trash");

const ACCESS_LEVELS = Object.freeze(["locked", "exposed"]);
const DEFAULT_STORAGE_RESERVE_BYTES = 512 * 1024 * 1024;
const DEFAULT_MAX_FILE_BYTES = 2 * 1024 * 1024 * 1024;
const HARD_MAX_FILE_BYTES = 8 * 1024 * 1024 * 1024;
const DEFAULT_UPLOAD_MAX_FILES = 20;
const HARD_UPLOAD_MAX_FILES = 50;
const SIGNED_URL_MAX_TTL_SECONDS = 24 * 60 * 60;
const TEXT_RESOURCE_MAX_BYTES = 64 * 1024 * 1024;
let initialized = false;

const FORMAT_DEFINITIONS = Object.freeze({
  ".png": media("image/png", "image", "image", matchPng),
  ".apng": media("image/apng", "image", "image", matchPng),
  ".jpg": media("image/jpeg", "image", "image", matchJpeg),
  ".jpeg": media("image/jpeg", "image", "image", matchJpeg),
  ".jfif": media("image/jpeg", "image", "image", matchJpeg),
  ".webp": media("image/webp", "image", "image", matchWebp),
  ".gif": media("image/gif", "image", "image", matchGif),
  ".avif": media("image/avif", "image", "image", (buffer) => matchIsoBrand(buffer, ["avif", "avis"])),
  ".heic": media("image/heic", "image", "image", (buffer) => matchIsoBrand(buffer, ["heic", "heix", "hevc", "hevx", "mif1", "msf1"])),
  ".heif": media("image/heif", "image", "image", (buffer) => matchIsoBrand(buffer, ["heif", "heim", "heis", "mif1", "msf1"])),
  ".bmp": media("image/bmp", "image", "image", (buffer) => buffer.subarray(0, 2).toString("ascii") === "BM"),
  ".tif": media("image/tiff", "image", "image", matchTiff),
  ".tiff": media("image/tiff", "image", "image", matchTiff),
  ".ico": media("image/x-icon", "image", "image", (buffer) => startsWith(buffer, [0x00, 0x00, 0x01, 0x00])),
  ".psd": media("image/vnd.adobe.photoshop", "image", "none", (buffer) => buffer.subarray(0, 4).toString("ascii") === "8BPS"),
  ".svg": media("image/svg+xml; charset=utf-8", "image", "image", matchSafeSvg, true),

  ".mp3": media("audio/mpeg", "audio", "audio", matchMp3),
  ".wav": media("audio/wav", "audio", "audio", (buffer) => matchRiff(buffer, "WAVE")),
  ".wave": media("audio/wav", "audio", "audio", (buffer) => matchRiff(buffer, "WAVE")),
  ".ogg": media("audio/ogg", "audio", "audio", matchOgg),
  ".oga": media("audio/ogg", "audio", "audio", matchOgg),
  ".opus": media("audio/ogg", "audio", "audio", matchOgg),
  ".flac": media("audio/flac", "audio", "audio", (buffer) => buffer.subarray(0, 4).toString("ascii") === "fLaC"),
  ".m4a": media("audio/mp4", "audio", "audio", matchIsoMedia),
  ".aac": media("audio/aac", "audio", "audio", matchAac),
  ".weba": media("audio/webm", "audio", "audio", matchEbml),
  ".mka": media("audio/x-matroska", "audio", "audio", matchEbml),
  ".wma": media("audio/x-ms-wma", "audio", "audio", matchAsf),
  ".caf": media("audio/x-caf", "audio", "audio", (buffer) => buffer.subarray(0, 4).toString("ascii") === "caff"),
  ".aiff": media("audio/aiff", "audio", "audio", (buffer) => matchForm(buffer, ["AIFF", "AIFC"])),
  ".aif": media("audio/aiff", "audio", "audio", (buffer) => matchForm(buffer, ["AIFF", "AIFC"])),

  ".mp4": media("video/mp4", "video", "video", matchIsoMedia),
  ".m4v": media("video/x-m4v", "video", "video", matchIsoMedia),
  ".mov": media("video/quicktime", "video", "video", matchIsoMedia),
  ".webm": media("video/webm", "video", "video", matchEbml),
  ".ogv": media("video/ogg", "video", "video", matchOgg),
  ".mkv": media("video/x-matroska", "video", "video", matchEbml),
  ".avi": media("video/x-msvideo", "video", "video", (buffer) => matchRiff(buffer, "AVI ")),
  ".mpeg": media("video/mpeg", "video", "video", matchMpegVideo),
  ".mpg": media("video/mpeg", "video", "video", matchMpegVideo),
  ".ts": media("video/mp2t", "video", "video", matchTransportStream),
  ".mts": media("video/mp2t", "video", "video", matchTransportStream),
  ".m2ts": media("video/mp2t", "video", "video", matchTransportStream),
  ".3gp": media("video/3gpp", "video", "video", matchIsoMedia),
  ".flv": media("video/x-flv", "video", "video", (buffer) => buffer.subarray(0, 3).toString("ascii") === "FLV"),
  ".wmv": media("video/x-ms-wmv", "video", "video", matchAsf),
  ".mxf": media("application/mxf", "video", "video", (buffer) => startsWith(buffer, [0x06, 0x0e, 0x2b, 0x34])),

  ".vtt": media("text/vtt; charset=utf-8", "caption", "text", matchWebVtt, true),
  ".webvtt": media("text/vtt; charset=utf-8", "caption", "text", matchWebVtt, true),
  ".srt": media("application/x-subrip; charset=utf-8", "caption", "text", matchSubRip, true),
  ".ass": media("text/x-ssa; charset=utf-8", "caption", "text", matchSubStationAlpha, true),
  ".ssa": media("text/x-ssa; charset=utf-8", "caption", "text", matchSubStationAlpha, true),
  ".json": media("application/json; charset=utf-8", "data", "text", matchJson, true),
  ".lottie": media("application/json; charset=utf-8", "data", "text", matchJson, true),
  ".pdf": media("application/pdf", "document", "pdf", (buffer) => buffer.subarray(0, 5).toString("ascii") === "%PDF-"),

  ".woff": media("font/woff", "font", "font", (buffer) => buffer.subarray(0, 4).toString("ascii") === "wOFF"),
  ".woff2": media("font/woff2", "font", "font", (buffer) => buffer.subarray(0, 4).toString("ascii") === "wOF2"),
  ".ttf": media("font/ttf", "font", "font", matchTrueType),
  ".ttc": media("font/collection", "font", "font", (buffer) => buffer.subarray(0, 4).toString("ascii") === "ttcf"),
  ".otf": media("font/otf", "font", "font", (buffer) => buffer.subarray(0, 4).toString("ascii") === "OTTO")
});

function media(mime, category, previewKind, matches, text = false) {
  return Object.freeze({ mime, category, previewKind, matches, text });
}

function ensureContentLibrary() {
  if (initialized) return;
  fs.mkdirSync(FILES_DIR, { recursive: true });
  fs.mkdirSync(INCOMING_DIR, { recursive: true });
  fs.mkdirSync(TRASH_DIR, { recursive: true });
  ensureSettingDefault("content_storage_limit_bytes", 0);
  ensureSettingDefault("content_storage_reserve_bytes", DEFAULT_STORAGE_RESERVE_BYTES);
  ensureSettingDefault("content_max_file_bytes", DEFAULT_MAX_FILE_BYTES);
  ensureSettingDefault("content_upload_max_files", DEFAULT_UPLOAD_MAX_FILES);
  cleanupStaleWorkingFiles();
  initialized = true;
}

function ensureSettingDefault(key, value) {
  if (getSetting(key, null) === null) setSetting(key, value);
}

function cleanupStaleWorkingFiles() {
  const cutoff = Date.now() - 24 * 60 * 60 * 1000;
  for (const directory of [INCOMING_DIR, TRASH_DIR]) {
    let entries = [];
    try {
      entries = fs.readdirSync(directory, { withFileTypes: true });
    } catch {
      continue;
    }
    for (const entry of entries) {
      if (!entry.isFile()) continue;
      const target = path.join(directory, entry.name);
      try {
        if (fs.statSync(target).mtimeMs < cutoff) fs.rmSync(target, { force: true });
      } catch {}
    }
  }
}

function supportedFormats() {
  return Object.entries(FORMAT_DEFINITIONS).map(([extension, definition]) => ({
    extension,
    mime: definition.mime,
    category: definition.category,
    preview_kind: definition.previewKind
  }));
}

function supportedExtensions() {
  return Object.keys(FORMAT_DEFINITIONS);
}

function uploadLimits() {
  return {
    max_file_bytes: clampInteger(
      getSetting("content_max_file_bytes", DEFAULT_MAX_FILE_BYTES),
      1 * 1024 * 1024,
      HARD_MAX_FILE_BYTES,
      DEFAULT_MAX_FILE_BYTES
    ),
    max_files: clampInteger(
      getSetting("content_upload_max_files", DEFAULT_UPLOAD_MAX_FILES),
      1,
      HARD_UPLOAD_MAX_FILES,
      DEFAULT_UPLOAD_MAX_FILES
    )
  };
}

function getStorageStats() {
  ensureContentLibrary();
  const usedRow = db.prepare("SELECT COALESCE(SUM(size), 0) AS used FROM content_resources").get();
  const usedBytes = nonNegativeNumber(usedRow?.used);
  const limitBytes = nonNegativeNumber(getSetting("content_storage_limit_bytes", 0));
  const reserveBytes = nonNegativeNumber(
    getSetting("content_storage_reserve_bytes", DEFAULT_STORAGE_RESERVE_BYTES)
  );
  const disk = diskStats(DATA_DIR);
  const diskUsableBytes = disk.available
    ? Math.max(0, disk.available_bytes - reserveBytes)
    : Number.MAX_SAFE_INTEGER;
  const quotaRemainingBytes = limitBytes > 0
    ? Math.max(0, limitBytes - usedBytes)
    : Number.MAX_SAFE_INTEGER;
  const effectiveAvailableBytes = Math.min(diskUsableBytes, quotaRemainingBytes);
  return {
    used_bytes: usedBytes,
    file_count: Number(db.prepare("SELECT COUNT(*) AS count FROM content_resources").get()?.count || 0),
    limit_bytes: limitBytes,
    quota_enabled: limitBytes > 0,
    quota_remaining_bytes: limitBytes > 0 ? quotaRemainingBytes : null,
    reserve_bytes: reserveBytes,
    disk_available: disk.available,
    disk_free_bytes: disk.available ? disk.available_bytes : null,
    disk_total_bytes: disk.available ? disk.total_bytes : null,
    disk_usable_bytes: disk.available ? diskUsableBytes : null,
    effective_available_bytes: Number.isFinite(effectiveAvailableBytes)
      ? effectiveAvailableBytes
      : null,
    usage_percent: limitBytes > 0
      ? percent(usedBytes, limitBytes)
      : disk.available
        ? percent(disk.total_bytes - disk.available_bytes, disk.total_bytes)
        : 0,
    quota_usage_percent: limitBytes > 0 ? percent(usedBytes, limitBytes) : null,
    disk_usage_percent: disk.available
      ? percent(disk.total_bytes - disk.available_bytes, disk.total_bytes)
      : null,
    ...uploadLimits()
  };
}

function diskStats(target) {
  try {
    if (typeof fs.statfsSync !== "function") return { available: false };
    const stat = fs.statfsSync(target);
    return {
      available: true,
      available_bytes: Number(stat.bavail) * Number(stat.bsize),
      total_bytes: Number(stat.blocks) * Number(stat.bsize)
    };
  } catch {
    return { available: false };
  }
}

function preflightIncomingRequest(contentLength) {
  const bytes = nonNegativeNumber(contentLength);
  if (!bytes) return { ok: true };
  const stats = getStorageStats();
  if (stats.effective_available_bytes !== null && bytes > stats.effective_available_bytes) {
    return {
      ok: false,
      status: 507,
      reason: "The upload is larger than the space currently available to Lumi."
    };
  }
  return { ok: true };
}

async function importUploadedFiles(files, options = {}) {
  ensureContentLibrary();
  const list = Array.isArray(files) ? files.filter(Boolean) : [];
  const limits = uploadLimits();
  if (!list.length) throw new Error("Choose at least one file to upload.");
  if (list.length > limits.max_files) {
    throw new Error(`Upload no more than ${limits.max_files} files at once.`);
  }

  const accessLevel = normalizeAccessLevel(options.access_level);
  const prepared = [];
  try {
    for (const file of list) {
      prepared.push(await inspectUpload(file, limits.max_file_bytes));
    }
    ensureImportCapacity(prepared.reduce((sum, item) => sum + item.size, 0));

    const moved = [];
    try {
      for (const item of prepared) {
        const id = crypto.randomUUID();
        const storedName = `${id}${item.extension}`;
        const destination = safeStoragePath(storedName);
        moveFile(item.temp_path, destination);
        moved.push(destination);
        const token = accessLevel === "exposed" ? createStoredToken() : null;
        item.row = {
          id,
          display_name: displayNameFromFilename(item.original_name),
          original_name: item.original_name,
          stored_name: storedName,
          mime: item.mime,
          extension: item.extension,
          category: item.category,
          preview_kind: item.preview_kind,
          size: item.size,
          checksum_sha256: item.checksum_sha256,
          access_level: accessLevel,
          public_token_hash: token?.hash || null,
          public_token_encrypted: token?.encrypted || null,
          uploaded_by: options.uploaded_by || null,
          created_at: Date.now(),
          updated_at: Date.now()
        };
      }

      const insert = db.prepare(`
        INSERT INTO content_resources (
          id, display_name, original_name, stored_name, mime, extension,
          category, preview_kind, size, checksum_sha256, access_level,
          public_token_hash, public_token_encrypted, uploaded_by, created_at, updated_at
        ) VALUES (
          @id, @display_name, @original_name, @stored_name, @mime, @extension,
          @category, @preview_kind, @size, @checksum_sha256, @access_level,
          @public_token_hash, @public_token_encrypted, @uploaded_by, @created_at, @updated_at
        )
      `);
      const transaction = db.transaction((items) => {
        items.forEach((item) => insert.run(item.row));
      });
      transaction(prepared);
      return prepared.map((item) => hydrateResource(item.row));
    } catch (error) {
      moved.forEach((target) => {
        try { fs.rmSync(target, { force: true }); } catch {}
      });
      throw error;
    }
  } finally {
    list.forEach((file) => {
      if (!file?.path) return;
      try { fs.rmSync(file.path, { force: true }); } catch {}
    });
  }
}

async function inspectUpload(file, maxFileBytes) {
  if (!file?.path) throw new Error("One of the uploaded files could not be read.");
  const originalName = safeDownloadFilename(file.originalname, "resource");
  const extension = path.extname(originalName).toLowerCase();
  const definition = FORMAT_DEFINITIONS[extension];
  if (!definition) {
    throw new Error(`${extension || "That file type"} is not supported by Lumi's media library.`);
  }
  const stat = fs.statSync(file.path);
  if (!stat.isFile() || stat.size < 1) throw new Error(`${originalName} is empty.`);
  if (stat.size > maxFileBytes) {
    throw new Error(`${originalName} is larger than the configured per-file limit (${formatBytes(maxFileBytes)}).`);
  }
  if (definition.text && stat.size > TEXT_RESOURCE_MAX_BYTES) {
    throw new Error(`${originalName} is too large for a text-based resource (${formatBytes(TEXT_RESOURCE_MAX_BYTES)} maximum).`);
  }
  const sample = definition.text ? fs.readFileSync(file.path) : readPrefix(file.path, 128 * 1024);
  if (!definition.matches(sample)) {
    throw new Error(`${originalName} does not match its filename extension or is not a valid supported media file.`);
  }
  return {
    temp_path: file.path,
    original_name: originalName,
    extension,
    mime: definition.mime,
    category: definition.category,
    preview_kind: definition.previewKind,
    size: stat.size,
    checksum_sha256: await hashFile(file.path)
  };
}

function ensureImportCapacity(bytes) {
  const stats = getStorageStats();
  if (stats.quota_enabled && bytes > stats.quota_remaining_bytes) {
    throw new Error("Uploading these files would exceed Lumi's configured content-library limit.");
  }
  if (stats.disk_available && stats.disk_free_bytes < stats.reserve_bytes) {
    throw new Error("The upload reached Lumi's reserved free-space boundary. Remove files or lower the reserve before retrying.");
  }
  if (stats.disk_available && bytes > stats.disk_usable_bytes) {
    throw new Error("Lumi cannot safely finish storing these files without crossing the reserved free-space boundary.");
  }
}

function listResources(filters = {}) {
  ensureContentLibrary();
  const where = [];
  const params = {};
  const category = String(filters.category || "").trim().toLowerCase();
  const accessLevel = String(filters.access_level || "").trim().toLowerCase();
  const search = String(filters.search || "").trim();
  if (category) {
    where.push("category = @category");
    params.category = category;
  }
  if (ACCESS_LEVELS.includes(accessLevel)) {
    where.push("access_level = @access_level");
    params.access_level = accessLevel;
  }
  if (search) {
    where.push("(display_name LIKE @search ESCAPE '\\' OR original_name LIKE @search ESCAPE '\\' OR mime LIKE @search ESCAPE '\\')");
    params.search = `%${search.replace(/[\\%_]/g, "\\$&")}%`;
  }
  const orderBy = filters.sort === "name"
    ? "display_name COLLATE NOCASE ASC"
    : filters.sort === "size"
      ? "size DESC"
      : "created_at DESC";
  const sql = `SELECT * FROM content_resources${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY ${orderBy}`;
  return db.prepare(sql).all(params).map(hydrateResource);
}

function getResource(id) {
  ensureContentLibrary();
  const row = db.prepare("SELECT * FROM content_resources WHERE id = ?").get(String(id || ""));
  return row ? hydrateResource(row) : null;
}

function getResourceByExposedToken(token) {
  ensureContentLibrary();
  const row = db.prepare(
    "SELECT * FROM content_resources WHERE public_token_hash = ? AND access_level = 'exposed'"
  ).get(tokenHash(token));
  return row ? hydrateResource(row) : null;
}

function updateResourceName(id, value) {
  const current = requireResource(id);
  const displayName = normalizeDisplayName(value, current.display_name);
  db.prepare("UPDATE content_resources SET display_name = ?, updated_at = ? WHERE id = ?")
    .run(displayName, Date.now(), current.id);
  return getResource(current.id);
}

function setResourceAccess(id, value) {
  const current = requireResource(id);
  const accessLevel = normalizeAccessLevel(value);
  if (current.access_level === accessLevel) return current;
  const token = accessLevel === "exposed" ? createStoredToken() : null;
  db.prepare(`
    UPDATE content_resources
    SET access_level = ?, public_token_hash = ?, public_token_encrypted = ?, updated_at = ?
    WHERE id = ?
  `).run(accessLevel, token?.hash || null, token?.encrypted || null, Date.now(), current.id);
  return getResource(current.id);
}

function deleteResource(id) {
  const current = requireResource(id);
  const source = safeStoragePath(current.stored_name);
  if (fs.existsSync(source)) {
    const stat = fs.lstatSync(source);
    if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("The resource storage entry is not a regular file.");
    // Deleting directly is more reliable than renaming to a temporary trash
    // path on Windows network drives. If the database write then fails, the
    // retained metadata can still be removed safely by retrying this action.
    fs.rmSync(source, { force: true });
  }
  db.prepare("DELETE FROM content_resources WHERE id = ?").run(current.id);
  return current;
}

function updateStorageSettings(values = {}) {
  for (const field of ["limit_gib", "reserve_gib", "max_file_mib", "max_files"]) {
    if (values[field] === undefined || values[field] === null || String(values[field]).trim() === "") {
      throw new Error("Complete every storage-limit field before saving.");
    }
  }
  const limitBytes = gibToBytes(values.limit_gib, { allowZero: true, max: 1024 * 1024 });
  const reserveBytes = gibToBytes(values.reserve_gib, { allowZero: true, max: 1024 });
  const maxFileBytes = mibToBytes(values.max_file_mib, {
    min: 1,
    max: HARD_MAX_FILE_BYTES / (1024 * 1024),
    fallback: DEFAULT_MAX_FILE_BYTES
  });
  const maxFiles = clampInteger(values.max_files, 1, HARD_UPLOAD_MAX_FILES, DEFAULT_UPLOAD_MAX_FILES);
  const usedBytes = getStorageStats().used_bytes;
  if (limitBytes > 0 && limitBytes < usedBytes) {
    throw new Error(`The storage limit cannot be lower than the ${formatBytes(usedBytes)} already stored.`);
  }
  db.transaction(() => {
    setSetting("content_storage_limit_bytes", limitBytes);
    setSetting("content_storage_reserve_bytes", reserveBytes);
    setSetting("content_max_file_bytes", maxFileBytes);
    setSetting("content_upload_max_files", maxFiles);
  })();
  return getStorageStats();
}

function resourceFilePath(resourceOrId) {
  const resource = typeof resourceOrId === "string" ? requireResource(resourceOrId) : resourceOrId;
  if (!resource?.stored_name) throw new Error("Resource storage metadata is missing.");
  const target = safeStoragePath(resource.stored_name);
  const stat = fs.lstatSync(target);
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("The resource file is unavailable.");
  return target;
}

function openReadStream(id, options = {}) {
  const resource = requireResource(id);
  const filePath = resourceFilePath(resource);
  return {
    resource,
    file_path: filePath,
    stream: fs.createReadStream(filePath, options)
  };
}

function publicUrl(resourceOrId, baseUrl = "") {
  const resource = typeof resourceOrId === "string" ? requireResource(resourceOrId) : resourceOrId;
  if (resource.access_level !== "exposed" || !resource.public_token) return null;
  return joinBaseUrl(baseUrl, `/media/${encodeURIComponent(resource.public_token)}/${encodeURIComponent(resource.original_name)}`);
}

function adminUrl(resourceOrId, baseUrl = "") {
  const resource = typeof resourceOrId === "string" ? requireResource(resourceOrId) : resourceOrId;
  return joinBaseUrl(baseUrl, `/admin/resources/${encodeURIComponent(resource.id)}/raw/${encodeURIComponent(resource.original_name)}`);
}

function downloadUrl(resourceOrId, baseUrl = "") {
  const resource = typeof resourceOrId === "string" ? requireResource(resourceOrId) : resourceOrId;
  return joinBaseUrl(baseUrl, `/admin/resources/${encodeURIComponent(resource.id)}/download/${encodeURIComponent(resource.original_name)}`);
}

function createSignedUrl(id, options = {}) {
  const resource = requireResource(id);
  const ttlSeconds = clampInteger(
    options.ttl_seconds,
    1,
    SIGNED_URL_MAX_TTL_SECONDS,
    5 * 60
  );
  const expires = Math.floor(Date.now() / 1000) + ttlSeconds;
  const signature = signedResourceSignature(resource.id, expires);
  const route = `/internal/media/${encodeURIComponent(resource.id)}/${encodeURIComponent(resource.original_name)}?expires=${expires}&sig=${encodeURIComponent(signature)}`;
  return joinBaseUrl(options.base_url || "", route);
}

function verifySignedResource(id, expires, signature) {
  const normalizedId = String(id || "");
  const expiry = Number(expires);
  if (!normalizedId || !Number.isFinite(expiry) || expiry < Math.floor(Date.now() / 1000)) return null;
  if (expiry > Math.floor(Date.now() / 1000) + SIGNED_URL_MAX_TTL_SECONDS + 60) return null;
  const expected = Buffer.from(signedResourceSignature(normalizedId, expiry));
  const received = Buffer.from(String(signature || ""));
  if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) return null;
  return getResource(normalizedId);
}

function serializeResource(resource, options = {}) {
  const baseUrl = options.base_url || "";
  return {
    id: resource.id,
    display_name: resource.display_name,
    original_name: resource.original_name,
    mime: resource.mime,
    extension: resource.extension,
    category: resource.category,
    preview_kind: resource.preview_kind,
    size: resource.size,
    size_display: formatBytes(resource.size),
    checksum_sha256: resource.checksum_sha256,
    access_level: resource.access_level,
    created_at: resource.created_at,
    updated_at: resource.updated_at,
    admin_url: adminUrl(resource, baseUrl),
    download_url: downloadUrl(resource, baseUrl),
    public_url: publicUrl(resource, baseUrl)
  };
}

function frameworkApi() {
  return Object.freeze({
    list: (filters) => listResources(filters).map((resource) => serializeResource(resource)),
    get: (id) => {
      const resource = getResource(id);
      return resource ? serializeResource(resource) : null;
    },
    resolvePath: resourceFilePath,
    openReadStream,
    publicUrl,
    createSignedUrl,
    storage: getStorageStats,
    supportedFormats
  });
}

function hydrateResource(row) {
  let publicToken = "";
  if (row.public_token_encrypted) {
    try { publicToken = decryptSecret(row.public_token_encrypted); } catch { publicToken = ""; }
  }
  return {
    ...row,
    size: Number(row.size),
    created_at: Number(row.created_at),
    updated_at: Number(row.updated_at),
    public_token: publicToken
  };
}

function requireResource(id) {
  const resource = getResource(id);
  if (!resource) {
    const error = new Error("Resource not found.");
    error.status = 404;
    throw error;
  }
  return resource;
}

function safeStoragePath(storedName) {
  const base = path.resolve(FILES_DIR);
  const target = path.resolve(FILES_DIR, path.basename(String(storedName || "")));
  if (path.dirname(target) !== base) throw new Error("Invalid resource storage path.");
  return target;
}

function normalizeAccessLevel(value) {
  const normalized = String(value || "locked").trim().toLowerCase();
  if (!ACCESS_LEVELS.includes(normalized)) throw new Error("Choose locked or exposed access.");
  return normalized;
}

function normalizeDisplayName(value, fallback = "Resource") {
  const normalized = String(value || "")
    .normalize("NFKC")
    .replace(/[\u0000-\u001f\u007f]/g, "")
    .trim()
    .slice(0, 160);
  return normalized || fallback;
}

function displayNameFromFilename(filename) {
  const extension = path.extname(filename);
  return normalizeDisplayName(path.basename(filename, extension), filename);
}

function moveFile(source, destination) {
  try {
    fs.renameSync(source, destination);
  } catch (error) {
    if (!["EXDEV", "EPERM", "EACCES"].includes(error.code)) throw error;
    fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
    fs.rmSync(source, { force: true });
  }
}

function readPrefix(filePath, length) {
  const descriptor = fs.openSync(filePath, "r");
  try {
    const buffer = Buffer.alloc(length);
    const bytesRead = fs.readSync(descriptor, buffer, 0, length, 0);
    return buffer.subarray(0, bytesRead);
  } finally {
    fs.closeSync(descriptor);
  }
}

function hashFile(filePath) {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash("sha256");
    const stream = fs.createReadStream(filePath);
    stream.on("data", (chunk) => hash.update(chunk));
    stream.on("error", reject);
    stream.on("end", () => resolve(hash.digest("hex")));
  });
}

function encryptionKey() {
  const secret = getSetting("session_secret", "");
  if (!secret) throw new Error("Lumi's session secret is not initialized.");
  return crypto.createHash("sha256").update(`lumi-content-library:${secret}`).digest();
}

function createStoredToken() {
  const token = crypto.randomBytes(32).toString("base64url");
  return { token, hash: tokenHash(token), encrypted: encryptSecret(token) };
}

function tokenHash(token) {
  return crypto.createHash("sha256").update(String(token || "")).digest("hex");
}

function encryptSecret(value) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", encryptionKey(), iv);
  const encrypted = Buffer.concat([cipher.update(String(value), "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();
  return `v1.${iv.toString("base64url")}.${tag.toString("base64url")}.${encrypted.toString("base64url")}`;
}

function decryptSecret(value) {
  const [version, ivValue, tagValue, encryptedValue] = String(value || "").split(".");
  if (version !== "v1" || !ivValue || !tagValue || !encryptedValue) throw new Error("Invalid stored token.");
  const decipher = crypto.createDecipheriv(
    "aes-256-gcm",
    encryptionKey(),
    Buffer.from(ivValue, "base64url")
  );
  decipher.setAuthTag(Buffer.from(tagValue, "base64url"));
  return Buffer.concat([
    decipher.update(Buffer.from(encryptedValue, "base64url")),
    decipher.final()
  ]).toString("utf8");
}

function signedResourceSignature(id, expires) {
  return crypto.createHmac("sha256", encryptionKey())
    .update(`resource:${id}:${expires}`)
    .digest("base64url");
}

function joinBaseUrl(baseUrl, route) {
  return `${String(baseUrl || "").replace(/\/$/, "")}${route}`;
}

function matchPng(buffer) {
  return startsWith(buffer, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
}

function matchJpeg(buffer) {
  return startsWith(buffer, [0xff, 0xd8, 0xff]);
}

function matchWebp(buffer) {
  return buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP";
}

function matchGif(buffer) {
  const header = buffer.subarray(0, 6).toString("ascii");
  return header === "GIF87a" || header === "GIF89a";
}

function matchTiff(buffer) {
  return startsWith(buffer, [0x49, 0x49, 0x2a, 0x00]) || startsWith(buffer, [0x4d, 0x4d, 0x00, 0x2a]);
}

function matchMp3(buffer) {
  return buffer.subarray(0, 3).toString("ascii") === "ID3" ||
    (buffer.length >= 2 && buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0);
}

function matchAac(buffer) {
  return buffer.length >= 2 && buffer[0] === 0xff && (buffer[1] & 0xf6) === 0xf0;
}

function matchOgg(buffer) {
  return buffer.subarray(0, 4).toString("ascii") === "OggS";
}

function matchRiff(buffer, formType) {
  return buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === formType;
}

function matchForm(buffer, types) {
  return buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "FORM" && types.includes(buffer.subarray(8, 12).toString("ascii"));
}

function matchIsoMedia(buffer) {
  return buffer.length >= 12 && buffer.subarray(4, 8).toString("ascii") === "ftyp";
}

function matchIsoBrand(buffer, brands) {
  if (!matchIsoMedia(buffer)) return false;
  const brandBlock = buffer.subarray(8, Math.min(buffer.length, 64)).toString("ascii");
  return brands.some((brand) => brandBlock.includes(brand));
}

function matchEbml(buffer) {
  return startsWith(buffer, [0x1a, 0x45, 0xdf, 0xa3]);
}

function matchMpegVideo(buffer) {
  for (let index = 0; index < Math.min(buffer.length - 3, 4096); index += 1) {
    if (buffer[index] === 0x00 && buffer[index + 1] === 0x00 && buffer[index + 2] === 0x01 && [0xb3, 0xba].includes(buffer[index + 3])) return true;
  }
  return false;
}

function matchTransportStream(buffer) {
  const offsets = [0, 4];
  return offsets.some((offset) => buffer.length > offset + 376 && buffer[offset] === 0x47 && buffer[offset + 188] === 0x47 && buffer[offset + 376] === 0x47);
}

function matchWebVtt(buffer) {
  const text = decodeText(buffer).replace(/^\uFEFF/, "");
  return /^WEBVTT(?:[ \t]|\r?\n)/.test(text);
}

function matchSubRip(buffer) {
  const text = decodeText(buffer);
  return /^\s*\d+\s*\r?\n\s*\d{1,2}:\d{2}:\d{2}[,.]\d{3}\s*-->\s*\d{1,2}:\d{2}:\d{2}[,.]\d{3}/m.test(text);
}

function matchJson(buffer) {
  try {
    const value = JSON.parse(decodeText(buffer));
    return value !== null && typeof value === "object";
  } catch {
    return false;
  }
}

function matchAsf(buffer) {
  return startsWith(buffer, [0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c]);
}

function matchSubStationAlpha(buffer) {
  const text = decodeText(buffer);
  return /^\s*\[Script Info\]/im.test(text) && /^\s*\[Events\]/im.test(text);
}

function matchSafeSvg(buffer) {
  const source = decodeText(buffer).replace(/^\uFEFF/, "").trim();
  if (!source || (!/^<\?xml\b[^>]*>\s*/i.test(source) && !/^<svg\b/i.test(source))) return false;
  if (!/<svg\b/i.test(source)) return false;
  return !/<\s*(script|foreignObject|iframe|object|embed|audio|video)\b/i.test(source)
    && !/\bon[a-z]+\s*=/i.test(source)
    && !/javascript\s*:/i.test(source)
    && !/@import\b/i.test(source)
    && !/<\s*!DOCTYPE|<\s*!ENTITY/i.test(source)
    && !/\b(?:href|xlink:href)\s*=\s*["']\s*(?!#)/i.test(source)
    && !/\burl\s*\(\s*["']?\s*(?!#)/i.test(source);
}

function matchTrueType(buffer) {
  return startsWith(buffer, [0x00, 0x01, 0x00, 0x00]) || buffer.subarray(0, 4).toString("ascii") === "true";
}

function decodeText(buffer) {
  if (buffer.includes(0)) return "";
  try {
    return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
  } catch {
    return "";
  }
}

function startsWith(buffer, bytes) {
  return buffer.length >= bytes.length && bytes.every((byte, index) => buffer[index] === byte);
}

function formatBytes(value) {
  const bytes = nonNegativeNumber(value);
  if (bytes < 1024) return `${bytes} B`;
  const units = ["KB", "MB", "GB", "TB"];
  let amount = bytes;
  let unit = "B";
  for (const next of units) {
    amount /= 1024;
    unit = next;
    if (amount < 1024) break;
  }
  return `${amount >= 10 ? amount.toFixed(1) : amount.toFixed(2)} ${unit}`;
}

function percent(value, total) {
  if (!total) return 0;
  return Math.max(0, Math.min(100, Math.round((Number(value) / Number(total)) * 1000) / 10));
}

function nonNegativeNumber(value) {
  const number = Number(value);
  return Number.isFinite(number) && number > 0 ? number : 0;
}

function clampInteger(value, min, max, fallback) {
  const number = Math.floor(Number(value));
  if (!Number.isFinite(number)) return Math.floor(fallback);
  return Math.max(min, Math.min(max, number));
}

function gibToBytes(value, options = {}) {
  const number = Number(value);
  if (options.allowZero && number === 0) return 0;
  if (!Number.isFinite(number) || number < 0) throw new Error("Storage values must be positive numbers.");
  const bounded = Math.min(options.max || number, number);
  return Math.floor(bounded * 1024 * 1024 * 1024);
}

function mibToBytes(value, options = {}) {
  const number = Number(value);
  const fallbackBytes = Number(options.fallback || DEFAULT_MAX_FILE_BYTES);
  if (!Number.isFinite(number)) return fallbackBytes;
  const bounded = Math.max(options.min || 1, Math.min(options.max || number, number));
  return Math.floor(bounded * 1024 * 1024);
}

module.exports = {
  ACCESS_LEVELS,
  DATA_DIR,
  FILES_DIR,
  FORMAT_DEFINITIONS,
  HARD_MAX_FILE_BYTES,
  HARD_UPLOAD_MAX_FILES,
  INCOMING_DIR,
  adminUrl,
  createSignedUrl,
  deleteResource,
  downloadUrl,
  ensureContentLibrary,
  formatBytes,
  frameworkApi,
  getResource,
  getResourceByExposedToken,
  getStorageStats,
  importUploadedFiles,
  listResources,
  normalizeAccessLevel,
  openReadStream,
  preflightIncomingRequest,
  publicUrl,
  resourceFilePath,
  serializeResource,
  setResourceAccess,
  supportedExtensions,
  supportedFormats,
  updateResourceName,
  updateStorageSettings,
  uploadLimits,
  verifySignedResource
};
