const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
let multer = null;
try {
  multer = require("multer");
} catch {
  multer = null;
}

const {
  INCOMING_DIR,
  deleteResource,
  formatBytes,
  getResource,
  getResourceByExposedToken,
  getStorageStats,
  importUploadedFiles,
  listResources,
  preflightIncomingRequest,
  resourceFilePath,
  serializeResource,
  setResourceAccess,
  supportedExtensions,
  supportedFormats,
  updateResourceName,
  updateStorageSettings,
  uploadLimits,
  verifySignedResource
} = require("./content-library");

function registerPublicContentRoutes(app) {
  app.get("/media/:token/:filename", (req, res) => {
    const resource = getResourceByExposedToken(req.params.token);
    if (!resource) return res.status(404).send("Not found.");
    return serveResource(req, res, resource, {
      cache_control: "public, max-age=0, must-revalidate",
      cors: true,
      disposition: req.query.download === "1" ? "attachment" : "inline"
    });
  });

  app.get("/internal/media/:id/:filename", (req, res) => {
    const resource = verifySignedResource(req.params.id, req.query.expires, req.query.sig);
    if (!resource) return res.status(404).send("Not found.");
    return serveResource(req, res, resource, {
      cache_control: "private, no-store",
      cors: true,
      disposition: req.query.download === "1" ? "attachment" : "inline"
    });
  });
}

function registerContentAdminRoutes(app, options = {}) {
  const requireAdmin = options.requireAdmin;
  if (typeof requireAdmin !== "function") {
    throw new Error("Content-library admin routes require an admin authorization middleware.");
  }
  const uploadMany = buildUploadMiddleware();

  app.get("/admin/resources", requireAdmin, (req, res) => {
    const baseUrl = requestBaseUrl(req);
    const resources = listResources().map((resource) => serializeResource(resource, { base_url: baseUrl }));
    const storage = getStorageStats();
    res.render("admin-resources", {
      title: "Resources",
      resources,
      storage,
      settings: storageSettingsForView(storage),
      formatGroups: groupFormats(supportedFormats()),
      formatBytes
    });
  });

  app.get("/api/admin/resources", requireAdmin, (req, res) => {
    const baseUrl = requestBaseUrl(req);
    res.set("Cache-Control", "no-store");
    res.json({
      ok: true,
      resources: listResources(req.query).map((resource) => serializeResource(resource, { base_url: baseUrl })),
      storage: getStorageStats()
    });
  });

  app.get("/admin/resources/:id/raw/:filename", requireAdmin, (req, res) => {
    const resource = getResource(req.params.id);
    if (!resource) return res.status(404).send("Not found.");
    return serveResource(req, res, resource, {
      cache_control: "private, no-store",
      cors: false,
      disposition: req.query.download === "1" ? "attachment" : "inline"
    });
  });

  app.get("/admin/resources/:id/download/:filename", requireAdmin, (req, res) => {
    const resource = getResource(req.params.id);
    if (!resource) return res.status(404).send("Not found.");
    return serveResource(req, res, resource, {
      cache_control: "private, no-store",
      cors: false,
      disposition: "attachment"
    });
  });

  app.post(
    "/admin/resources/upload",
    requireAdmin,
    guardUploadCapacity,
    uploadMany,
    async (req, res) => {
      try {
        if (req.contentUploadError) throw req.contentUploadError;
        const imported = await importUploadedFiles(req.files, {
          access_level: req.body.access_level,
          uploaded_by: req.session.user?.id || null
        });
        const baseUrl = requestBaseUrl(req);
        return mutationSuccess(req, res, {
          message: `${imported.length} resource${imported.length === 1 ? "" : "s"} uploaded.`,
          resources: imported.map((resource) => serializeResource(resource, { base_url: baseUrl })),
          storage: getStorageStats()
        }, "/admin/resources");
      } catch (error) {
        cleanupRequestFiles(req.files);
        return mutationError(req, res, error, "/admin/resources");
      }
    }
  );

  app.post("/admin/resources/settings", requireAdmin, (req, res) => {
    try {
      const storage = updateStorageSettings(req.body);
      return mutationSuccess(req, res, {
        message: "Resource storage settings saved.",
        storage,
        settings: storageSettingsForView(storage)
      }, "/admin/resources");
    } catch (error) {
      return mutationError(req, res, error, "/admin/resources");
    }
  });

  app.post("/admin/resources/:id/rename", requireAdmin, (req, res) => {
    try {
      const resource = updateResourceName(req.params.id, req.body.display_name);
      return mutationSuccess(req, res, {
        message: "Resource name updated.",
        resource: serializeResource(resource, { base_url: requestBaseUrl(req) })
      }, "/admin/resources");
    } catch (error) {
      return mutationError(req, res, error, "/admin/resources");
    }
  });

  app.post("/admin/resources/:id/access", requireAdmin, (req, res) => {
    try {
      const resource = setResourceAccess(req.params.id, "exposed");
      return mutationSuccess(req, res, {
        message: "Resource exposed with a new read-only URL.",
        resource: serializeResource(resource, { base_url: requestBaseUrl(req) })
      }, "/admin/resources");
    } catch (error) {
      return mutationError(req, res, error, "/admin/resources");
    }
  });

  app.post("/admin/resources/:id/revoke", requireAdmin, (req, res) => {
    try {
      const resource = setResourceAccess(req.params.id, "locked");
      return mutationSuccess(req, res, {
        message: "Resource locked. Its previous exposed URL no longer works.",
        resource: serializeResource(resource, { base_url: requestBaseUrl(req) })
      }, "/admin/resources");
    } catch (error) {
      return mutationError(req, res, error, "/admin/resources");
    }
  });

  app.post("/admin/resources/:id/delete", requireAdmin, (req, res) => {
    try {
      const deleted = deleteResource(req.params.id);
      return mutationSuccess(req, res, {
        message: `${deleted.display_name} deleted.`,
        deleted_id: deleted.id,
        storage: getStorageStats()
      }, "/admin/resources");
    } catch (error) {
      return mutationError(req, res, error, "/admin/resources");
    }
  });
}

function buildUploadMiddleware() {
  if (!multer) {
    return (req, _res, next) => {
      req.contentUploadError = withStatus(new Error("File uploads require the multer dependency. Run npm install, then restart Lumi."), 503);
      next();
    };
  }
  const storage = multer.diskStorage({
    destination: (_req, _file, callback) => callback(null, INCOMING_DIR),
    filename: (_req, _file, callback) => callback(null, `${crypto.randomUUID()}.upload`)
  });
  return (req, res, next) => {
    const configured = uploadLimits();
    const uploader = multer({
      storage,
      limits: {
        fileSize: configured.max_file_bytes,
        files: configured.max_files,
        fields: 10,
        parts: configured.max_files + 12
      },
      fileFilter: (_req, file, callback) => {
        const extension = path.extname(file.originalname || "").toLowerCase();
        if (supportedExtensions().includes(extension)) return callback(null, true);
        return callback(new Error(`${extension || "That file type"} is not supported by Lumi's media library.`));
      }
    });
    uploader.array("resources", configured.max_files)(req, res, (error) => {
      if (!error) return next();
      cleanupRequestFiles(req.files);
      const message = error.code === "LIMIT_FILE_SIZE"
        ? `A file exceeded the configured per-file limit of ${formatBytes(configured.max_file_bytes)}.`
        : error.code === "LIMIT_FILE_COUNT"
          ? `Upload no more than ${configured.max_files} files at once.`
          : error.message;
      req.contentUploadError = withStatus(new Error(message), error.code?.startsWith("LIMIT_") ? 413 : 400);
      next();
    });
  };
}

function guardUploadCapacity(req, res, next) {
  const result = preflightIncomingRequest(req.get("content-length"));
  if (result.ok) return next();
  return mutationError(req, res, withStatus(new Error(result.reason), result.status || 507), "/admin/resources");
}

function serveResource(req, res, resource, options = {}) {
  let filePath;
  let stat;
  try {
    filePath = resourceFilePath(resource);
    stat = fs.statSync(filePath);
  } catch {
    return res.status(404).send("Not found.");
  }

  const etag = `"sha256-${resource.checksum_sha256}"`;
  res.set({
    "Accept-Ranges": "bytes",
    "Cache-Control": options.cache_control || "private, no-store",
    "Content-Type": resource.mime,
    "Content-Disposition": contentDisposition(options.disposition || "inline", resource.original_name),
    "ETag": etag,
    "X-Content-Type-Options": "nosniff",
    "Cross-Origin-Resource-Policy": options.cors ? "cross-origin" : "same-origin"
  });
  if (options.cors) res.set("Access-Control-Allow-Origin", "*");
  if (String(resource.mime || "").startsWith("image/svg+xml")) {
    res.set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:; sandbox");
  }

  if (!req.headers.range && req.headers["if-none-match"] === etag) {
    return res.status(304).end();
  }

  const range = parseByteRange(req.headers.range, stat.size);
  if (range?.invalid) {
    res.set("Content-Range", `bytes */${stat.size}`);
    return res.status(416).end();
  }
  if (range) {
    const length = range.end - range.start + 1;
    res.status(206);
    res.set({
      "Content-Range": `bytes ${range.start}-${range.end}/${stat.size}`,
      "Content-Length": String(length)
    });
    if (req.method === "HEAD") return res.end();
    return pipeFile(res, filePath, { start: range.start, end: range.end });
  }

  res.set("Content-Length", String(stat.size));
  if (req.method === "HEAD") return res.end();
  return pipeFile(res, filePath);
}

function parseByteRange(header, size) {
  if (!header) return null;
  const match = /^bytes=(\d*)-(\d*)$/.exec(String(header).trim());
  if (!match) return { invalid: true };
  let start;
  let end;
  if (!match[1]) {
    const suffixLength = Number(match[2]);
    if (!Number.isFinite(suffixLength) || suffixLength <= 0) return { invalid: true };
    start = Math.max(0, size - suffixLength);
    end = size - 1;
  } else {
    start = Number(match[1]);
    end = match[2] ? Number(match[2]) : size - 1;
  }
  if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || start >= size) {
    return { invalid: true };
  }
  return { start, end: Math.min(end, size - 1) };
}

function pipeFile(res, filePath, options = undefined) {
  const stream = fs.createReadStream(filePath, options);
  stream.on("error", (error) => {
    if (!res.headersSent) res.status(500).send("Unable to read resource.");
    else res.destroy(error);
  });
  stream.pipe(res);
  return stream;
}

function contentDisposition(type, filename) {
  const safeAscii = String(filename || "resource")
    .replace(/[^\x20-\x7e]/g, "_")
    .replace(/["\\]/g, "_");
  return `${type}; filename="${safeAscii}"; filename*=UTF-8''${encodeURIComponent(filename || "resource")}`;
}

function requestBaseUrl(req) {
  return `${req.protocol}://${req.get("host")}`;
}

function groupFormats(formats) {
  const groups = {};
  formats.forEach((format) => {
    if (!groups[format.category]) groups[format.category] = [];
    groups[format.category].push(format.extension);
  });
  return Object.entries(groups)
    .map(([category, extensions]) => ({ category, extensions: extensions.sort() }))
    .sort((left, right) => left.category.localeCompare(right.category));
}

function storageSettingsForView(storage) {
  return {
    limit_gib: storage.limit_bytes > 0 ? round(storage.limit_bytes / (1024 ** 3), 2) : 0,
    reserve_gib: round(storage.reserve_bytes / (1024 ** 3), 2),
    max_file_mib: Math.round(storage.max_file_bytes / (1024 ** 2)),
    max_files: storage.max_files
  };
}

function mutationSuccess(req, res, payload, fallbackPath) {
  if (wantsJson(req)) return res.json({ ok: true, ...payload });
  req.session.flash = { type: "success", message: payload.message || "Saved." };
  return res.redirect(fallbackPath);
}

function mutationError(req, res, error, fallbackPath) {
  const status = Number(error?.status) || 400;
  const message = error?.message || "Unable to update the resource library.";
  if (wantsJson(req)) return res.status(status).json({ ok: false, error: message });
  req.session.flash = { type: "error", message };
  return res.redirect(fallbackPath);
}

function wantsJson(req) {
  return req.xhr || String(req.get("accept") || "").toLowerCase().includes("application/json");
}

function cleanupRequestFiles(files) {
  for (const file of Array.isArray(files) ? files : []) {
    if (!file?.path) continue;
    try { fs.rmSync(file.path, { force: true }); } catch {}
  }
}

function withStatus(error, status) {
  error.status = status;
  return error;
}

function round(value, places) {
  const factor = 10 ** places;
  return Math.round(Number(value) * factor) / factor;
}

module.exports = {
  parseByteRange,
  registerContentAdminRoutes,
  registerPublicContentRoutes,
  serveResource
};
