/**
 * Dev Assets File Routes
 *
 * HTTP routes for serving and uploading files via presigned URLs.
 * These routes validate signatures generated by the dev-assets-mcp server.
 *
 * Routes:
 * - GET /api/dev-assets/:orgId/* - Download a file
 * - PUT /api/dev-assets/:orgId/* - Upload a file
 *
 * Only mounted when DevObjectStorage is the active backend (no S3 configured)
 */

import { Hono } from "hono";
import { createHmac } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import type { StudioContext } from "../../core/studio-context";
import { getSettings } from "../../settings";
import { safeEqual } from "./credential-vault";

// Base directory for dev assets (relative to cwd)
const DEV_ASSETS_BASE_DIR = "./data/assets";

// ============================================================================
// Utility Functions
// ============================================================================

/**
 * Get the base directory for an organization's assets
 */
function getOrgAssetsDir(orgId: string): string {
  // Sanitize org ID to prevent directory traversal
  const sanitizedOrgId = orgId.replace(/[^a-zA-Z0-9_-]/g, "_");
  return join(DEV_ASSETS_BASE_DIR, sanitizedOrgId);
}

/**
 * Sanitize a file key to prevent directory traversal
 */
function sanitizeKey(key: string): string {
  // Remove leading slashes and normalize path
  const normalized = key.replace(/^\/+/, "").replace(/\.\./g, "");
  return normalized;
}

/**
 * Get the full file path for a key within an org's assets
 */
function getFilePath(orgId: string, key: string): string {
  const baseDir = getOrgAssetsDir(orgId);
  const sanitizedKey = sanitizeKey(key);
  return join(baseDir, sanitizedKey);
}

/**
 * Verify a presigned URL signature
 */
export function verifySignature(
  orgId: string,
  key: string,
  expires: number,
  method: "GET" | "PUT",
  signature: string,
): boolean {
  const secret = getSettings().encryptionKey || "dev-secret";
  const data = `${orgId}:${key}:${expires}:${method}`;
  const expectedSignature = createHmac("sha256", secret)
    .update(data)
    .digest("hex");
  // Constant-time compare — a plain `===` leaks timing info an attacker can
  // use to forge a valid signature byte-by-byte, same class of bug the vault
  // service token guards against (see credential-vault.ts's `safeEqual`).
  return safeEqual(signature, expectedSignature);
}

/**
 * Get content type from file extension
 * Exported for use by dev-assets-mcp.ts
 */
export function getContentType(key: string): string {
  const ext = key.split(".").pop()?.toLowerCase() || "";
  const contentTypes: Record<string, string> = {
    // Images
    jpg: "image/jpeg",
    jpeg: "image/jpeg",
    png: "image/png",
    gif: "image/gif",
    webp: "image/webp",
    svg: "image/svg+xml",
    ico: "image/x-icon",
    bmp: "image/bmp",
    // Documents
    pdf: "application/pdf",
    json: "application/json",
    xml: "application/xml",
    // Text
    txt: "text/plain",
    html: "text/html",
    htm: "text/html",
    css: "text/css",
    csv: "text/csv",
    md: "text/markdown",
    // Scripts
    js: "application/javascript",
    mjs: "application/javascript",
    ts: "application/typescript",
    // Video
    mp4: "video/mp4",
    webm: "video/webm",
    avi: "video/x-msvideo",
    mov: "video/quicktime",
    // Audio
    mp3: "audio/mpeg",
    wav: "audio/wav",
    ogg: "audio/ogg",
    // Archives
    zip: "application/zip",
    gz: "application/gzip",
    tar: "application/x-tar",
    // Fonts
    woff: "font/woff",
    woff2: "font/woff2",
    ttf: "font/ttf",
    otf: "font/otf",
  };

  return contentTypes[ext] || "application/octet-stream";
}

// ============================================================================
// Routes
// ============================================================================

type DevAssetsVariables = { studioContext: StudioContext };

interface CreateDevAssetsRoutesOptions {
  /**
   * When `true`, the routes are mounted at `/*` and the org id is read from
   * `studioContext.organization.id` (set by the `resolveOrgFromPath` middleware).
   * When `false`, the routes are mounted at `/:orgId/*` and the org id is read
   * from the path param — preserves the legacy `/api/dev-assets/:orgId/*`
   * behaviour.
   */
  orgFromPath: boolean;
}

export const createDevAssetsRoutes = (opts: CreateDevAssetsRoutesOptions) => {
  const app = new Hono<{ Variables: DevAssetsVariables }>();

  const path = opts.orgFromPath ? "/*" : "/:orgId/*";

  /**
   * GET /api/dev-assets/:orgId/* (or /api/:org/dev-assets/* in org-scoped mode)
   * Download a file.
   *
   * Query params:
   * - expires: Unix timestamp when URL expires
   * - signature: HMAC signature
   * - method: Must be "GET"
   */
  app.get(path, async (c) => {
    const orgId = opts.orgFromPath
      ? c.get("studioContext")?.organization?.id
      : c.req.param("orgId");

    if (!orgId) {
      return c.json({ error: "Missing organization context" }, 500);
    }

    // Get the path after the org segment.
    const prefix = opts.orgFromPath
      ? `/api/${c.req.param("org") ?? ""}/dev-assets/`
      : `/api/dev-assets/${orgId}/`;
    const key = c.req.path.replace(prefix, "");

    if (!key) {
      return c.json({ error: "Missing key" }, 400);
    }

    // Validate query params
    const expiresStr = c.req.query("expires");
    const signature = c.req.query("signature");
    const method = c.req.query("method");

    if (!expiresStr || !signature || method !== "GET") {
      return c.json({ error: "Invalid or missing signature parameters" }, 400);
    }

    const expires = parseInt(expiresStr, 10);
    if (!Number.isFinite(expires)) {
      return c.json({ error: "Invalid expires parameter" }, 400);
    }

    const now = Math.floor(Date.now() / 1000);

    // Check expiration
    if (expires < now) {
      return c.json({ error: "URL has expired" }, 403);
    }

    // Verify signature
    if (!verifySignature(orgId, key, expires, "GET", signature)) {
      return c.json({ error: "Invalid signature" }, 403);
    }

    // Get the file
    const filePath = getFilePath(orgId, key);

    try {
      const file = Bun.file(filePath);
      const exists = await file.exists();

      if (!exists) {
        return c.json({ error: "File not found" }, 404);
      }

      const contentType = getContentType(key);

      return new Response(file.stream(), {
        headers: {
          "Content-Type": contentType,
          "Content-Length": file.size.toString(),
          "Cache-Control": "private, max-age=3600",
        },
      });
    } catch (err) {
      console.error("Error serving file:", err);
      return c.json({ error: "Failed to read file" }, 500);
    }
  });

  /**
   * PUT /api/dev-assets/:orgId/* (or /api/:org/dev-assets/* in org-scoped mode)
   * Upload a file.
   *
   * Query params:
   * - expires: Unix timestamp when URL expires
   * - signature: HMAC signature
   * - method: Must be "PUT"
   */
  app.put(path, async (c) => {
    const orgId = opts.orgFromPath
      ? c.get("studioContext")?.organization?.id
      : c.req.param("orgId");

    if (!orgId) {
      return c.json({ error: "Missing organization context" }, 500);
    }

    // Get the path after the org segment.
    const prefix = opts.orgFromPath
      ? `/api/${c.req.param("org") ?? ""}/dev-assets/`
      : `/api/dev-assets/${orgId}/`;
    const key = c.req.path.replace(prefix, "");

    if (!key) {
      return c.json({ error: "Missing key" }, 400);
    }

    // Validate query params
    const expiresStr = c.req.query("expires");
    const signature = c.req.query("signature");
    const method = c.req.query("method");

    if (!expiresStr || !signature || method !== "PUT") {
      return c.json({ error: "Invalid or missing signature parameters" }, 400);
    }

    const expires = parseInt(expiresStr, 10);
    if (!Number.isFinite(expires)) {
      return c.json({ error: "Invalid expires parameter" }, 400);
    }

    const now = Math.floor(Date.now() / 1000);

    // Check expiration
    if (expires < now) {
      return c.json({ error: "URL has expired" }, 403);
    }

    // Verify signature
    if (!verifySignature(orgId, key, expires, "PUT", signature)) {
      return c.json({ error: "Invalid signature" }, 403);
    }

    // Get the file path and ensure directory exists
    const filePath = getFilePath(orgId, key);
    const dir = dirname(filePath);

    try {
      // Create directory if it doesn't exist
      await mkdir(dir, { recursive: true });

      // Read the request body
      const body = await c.req.arrayBuffer();

      // Write the file
      await writeFile(filePath, Buffer.from(body));

      return c.json({ success: true, key });
    } catch (err) {
      console.error("Error saving file:", err);
      return c.json({ error: "Failed to save file" }, 500);
    }
  });

  return app;
};
