/**
 * The HTTP-facing upload + serve layer: parse multipart requests, validate,
 * store, and serve files back. Built on the store registry (`client.ts`).
 */

import type { Context } from "@wrnexus/core";
import { getStore, hasStorage, type Store } from "./client.ts";
import type { StoredObject } from "./driver.ts";
import { accepts, contentTypeOf, extForType, extOf } from "./mime.ts";

/** Reserved prefix the framework serves PUBLIC local objects from. */
export const UPLOADS_PREFIX = "/__wrnexus/uploads/";

export interface UploadedFile {
  /** Storage key — pass to `getStore().driver.get/delete` or a serve route. */
  key: string;
  /** A servable URL for public objects, or `null` for private stores. */
  url: string | null;
  /** Original (sanitized) client filename, for display. */
  name: string;
  type: string;
  size: number;
}

export interface UploadOptions {
  /** Only read files from this form field (default: every file field). */
  field?: string;
  /** Override the store's `maxBytes`. */
  maxBytes?: number;
  /** Override the store's `accept` list. */
  accept?: string[];
  /** Key prefix, e.g. `"avatars"` → keys become `avatars/<yyyy>/<mm>/<rand>.<ext>`. */
  prefix?: string;
  /** Virus/DLP/content scanner invoked before bytes enter storage. Throw or return unsafe to reject. */
  scan?: (file: UploadScanInput) => UploadScanResult | Promise<UploadScanResult>;
  /** Image/video/indexing hook invoked after storage. Failure removes the just-written object. */
  afterStore?: (file: UploadedFile & { bytes: Uint8Array; store: Store }) => void | Promise<void>;
}

export interface UploadScanInput {
  name: string;
  type: string;
  size: number;
  bytes: Uint8Array;
  store: Store;
}
export interface UploadScanResult {
  safe: boolean;
  reason?: string;
  scanner?: string;
}

/** A 4xx-carrying error so `handleUpload` can map it to a status. */
export class UploadError extends Error {
  constructor(
    message: string,
    readonly status = 400,
  ) {
    super(message);
    this.name = "UploadError";
  }
}

function randomId(): string {
  const b = new Uint8Array(16);
  crypto.getRandomValues(b);
  return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
}

const pad = (n: number): string => (n < 10 ? "0" + n : String(n));

/** Build a collision-proof, path-safe key — NEVER derived from client input. */
function makeKey(file: { name: string; type: string }, prefix?: string): string {
  const now = new Date();
  const ext = extOf(file.name) || extForType(file.type);
  const dated = `${now.getUTCFullYear()}/${pad(now.getUTCMonth() + 1)}/${randomId()}`;
  const withExt = ext ? `${dated}.${ext}` : dated;
  const p = (prefix ?? "").replace(/^\/+|\/+$/g, "");
  if (p && !isSafeKey(p)) throw new UploadError("unsafe upload prefix", 400);
  return p ? `${p}/${withExt}` : withExt;
}

function isSafeKey(key: string): boolean {
  return (
    !!key &&
    !key.includes("\\") &&
    !key.includes("\0") &&
    !key.startsWith("/") &&
    key.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..")
  );
}

/** Strip any path parts from a client filename; keep a friendly display name. */
function displayName(name: string): string {
  return (name.split(/[\\/]/).pop() || "file").slice(0, 255);
}

/** The servable URL for a stored object (public → URL, private → null). */
export function storedUrl(store: Store, key: string): string | null {
  if (store.access !== "public") return null;
  return store.driver.publicUrl(key) ?? `${UPLOADS_PREFIX}${store.name}/${key}`;
}

/**
 * Read multipart file(s) from a request and store them. Throws `UploadError`
 * (4xx) on validation failures. Call it directly, or use `handleUpload`.
 */
export async function upload(
  storeName: string | undefined,
  req: Request,
  opts: UploadOptions = {},
): Promise<{ files: UploadedFile[] }> {
  const store = getStore(storeName);
  const maxBytes = opts.maxBytes ?? store.config.maxBytes;
  const accept = opts.accept ?? store.config.accept;

  let form: FormData;
  try {
    form = await req.formData();
  } catch {
    throw new UploadError("expected a multipart/form-data upload", 400);
  }

  const blobs: File[] = [];
  for (const [name, value] of form.entries()) {
    if (typeof value === "string") continue;
    if (opts.field && name !== opts.field) continue;
    blobs.push(value as File);
  }
  if (blobs.length === 0) throw new UploadError("no files in upload", 400);

  const files: UploadedFile[] = [];
  for (const file of blobs) {
    const type = file.type || contentTypeOf(file.name);
    if (maxBytes && file.size > maxBytes) {
      throw new UploadError(`file too large (max ${maxBytes} bytes)`, 413);
    }
    if (!accepts(accept, { type, name: file.name })) {
      throw new UploadError(`file type not allowed: ${type || extOf(file.name) || "unknown"}`, 415);
    }
    const key = makeKey({ name: file.name, type }, opts.prefix);
    const data = new Uint8Array(await file.arrayBuffer());
    if (opts.scan) {
      const result = await opts.scan({
        name: displayName(file.name),
        type,
        size: file.size,
        bytes: data,
        store,
      });
      if (!result.safe)
        throw new UploadError(
          `file rejected by ${result.scanner ?? "security scanner"}${result.reason ? `: ${result.reason}` : ""}`,
          422,
        );
    }
    await store.driver.put(key, data, { contentType: type, filename: displayName(file.name) });
    const uploaded: UploadedFile = {
      key,
      url: storedUrl(store, key),
      name: displayName(file.name),
      type,
      size: file.size,
    };
    try {
      await opts.afterStore?.({ ...uploaded, bytes: data, store });
    } catch (error) {
      await store.driver.delete(key);
      throw new UploadError(
        `post-upload processing failed: ${error instanceof Error ? error.message : String(error)}`,
        422,
      );
    }
    files.push(uploaded);
  }
  return { files };
}

/**
 * Ready-made POST handler:
 *
 *   // app/api/upload.ts
 *   export const POST = handleUpload({ store: "public" });
 *
 * Returns `{ ok:true, files:[…] }` on success, or `{ ok:false, error }` with a
 * 4xx/5xx status.
 */
export function handleUpload(opts: UploadOptions & { store?: string } = {}) {
  return async (ctx: Context): Promise<Response> => {
    try {
      const { files } = await upload(opts.store, ctx.req, opts);
      return Response.json({ ok: true, files });
    } catch (err) {
      const status = err instanceof UploadError ? err.status : 500;
      const error = err instanceof Error ? err.message : "upload failed";
      return Response.json({ ok: false, error }, { status });
    }
  };
}

function fileResponse(obj: StoredObject, cache: string): Response {
  const headers: Record<string, string> = {
    "content-type": obj.contentType,
    "cache-control": cache,
    "x-content-type-options": "nosniff",
  };
  if (
    /^(?:text\/html|image\/svg\+xml|application\/(?:xhtml\+xml|javascript))\b/i.test(
      obj.contentType,
    )
  ) {
    headers["content-disposition"] = 'attachment; filename="download"';
  }
  if (typeof obj.size === "number") headers["content-length"] = String(obj.size);
  return new Response(obj.body as BodyInit, { headers });
}

/**
 * Serve an object from a store as a route handler — mount it behind your auth
 * middleware to gate PRIVATE files:
 *
 *   // app/api/files/[key].ts
 *   export const GET = serveFromStore("docs");
 *
 * Reads the key from `ctx.params.key` (or `ctx.params.path`); it may contain `/`.
 */
export function serveFromStore(storeName?: string, opts: { param?: string } = {}) {
  return async (ctx: Context): Promise<Response> => {
    const store = getStore(storeName);
    const key = ctx.params[opts.param ?? "key"] ?? ctx.params.path ?? "";
    if (!isSafeKey(key)) return new Response("Not Found", { status: 404 });
    const obj = await store.driver.get(key);
    if (!obj) return new Response("Not Found", { status: 404 });
    const cache =
      store.access === "public" ? "public, max-age=31536000, immutable" : "private, no-store";
    return fileResponse(obj, cache);
  };
}

/**
 * Framework asset hook: serve PUBLIC local objects at
 * `/__wrnexus/uploads/<store>/<key>`. Returns `null` for anything it doesn't
 * own (unknown/private/S3-backed store) so the caller falls through. Wired into
 * the dev + prod asset servers.
 */
export async function serveStoredFile(pathname: string): Promise<Response | null> {
  if (!pathname.startsWith(UPLOADS_PREFIX)) return null;
  const rest = pathname.slice(UPLOADS_PREFIX.length);
  const slash = rest.indexOf("/");
  if (slash <= 0) return null;

  let storeName: string;
  let key: string;
  try {
    storeName = decodeURIComponent(rest.slice(0, slash));
    key = decodeURIComponent(rest.slice(slash + 1));
  } catch {
    return null;
  }
  if (!isSafeKey(key)) return null;
  if (!hasStorage(storeName)) return null;

  const store = getStore(storeName);
  if (store.access !== "public") return null; // private → served via the app route
  const obj = await store.driver.get(key);
  if (!obj) return new Response("Not Found", { status: 404 });
  return fileResponse(obj, "public, max-age=31536000, immutable");
}
