/**
 * app-harness-api.ts — Token-auth middleware for workspace-app headless browser access (Task 3).
 *
 * Exported interfaces:
 *   appHarnessAuthMiddleware() — verifies __appToken / x-app-token, injects a
 *                                scoped synthetic viewer into req.user so existing
 *                                viewerOf() + canEditInSpace() checks pass.
 *   mountAppHarnessApi()       — wires the middleware; Task 5 will add the static
 *                                harness route here.
 *
 * Security contract:
 *   - Valid token + matching space → req.user set to synthetic owner principal
 *   - Token scope spaceId ≠ requested spaceId → 403, no viewer injected (fail-closed)
 *   - Invalid/garbage token (present but unrecognised) → 403 (fail-closed)
 *   - Token absent → next() with req.user untouched (normal auth chain applies)
 *   - Cannot extract space id from URL → 403 (fail-closed)
 *   - Synthetic viewer has role='user', status='active' — NOT admin, no elevation
 */

import type { Express } from 'express';
import type { RequestHandler } from 'express';
import { verifyAppHarnessToken } from './app-harness-token.js';

// Regex to extract the space id from paths like:
//   /api/local/spaces/<id>/files
//   /api/local/spaces/<id>/files/content
//   /api/local/spaces/<id>/files/raw
// Anchored at ^ to the real API prefix and requires a boundary (/, ?, or $) after /files
// to prevent crafted paths like /x/spaces/evil/filesapi/spaces/real/files from matching.
const SPACE_FILES_RE = /^\/api\/local\/spaces\/([^/]+)\/files(?:\/|$|\?)/;

/**
 * Build a synthetic Express.User that represents the token's userId as an
 * ordinary (non-admin) active user.  The shape must match the Express.User
 * interface declared in src/bridge/auth.ts:
 *   id, email, name, avatarUrl, role, status, orgIds,
 *   defaultVisibility, defaultVisibilityOrgId
 *
 * viewerOf() reads req.user directly when authActive && req.user is set.
 * canManageSpace() passes when space.ownerId === user.id, so the caller must
 * ensure the space is owned by this userId — that is the app-harness contract.
 */
function buildSyntheticViewer(userId: string): Express.User {
  return {
    id: userId,
    email: '',
    name: null,
    avatarUrl: null,
    role: 'user',                  // ordinary principal — never admin
    status: 'active',
    orgIds: [],
    defaultVisibility: 'private',
    defaultVisibilityOrgId: null,
  } as Express.User;
}

/**
 * Middleware factory — call once and pass the result to app.use().
 *
 * Behaviour per request:
 *  1. Extract token from x-app-token header or __appToken query param.
 *  2. If no token → next() immediately (normal auth handles the request).
 *  3. Token present:
 *     a. Extract space id from URL.  Fail-closed (403) if not extractable.
 *     b. Verify token.  Fail-closed (403) if invalid/expired.
 *     c. Compare token.spaceId with URL space id.  403 on mismatch.
 *     d. Set req.user to synthetic viewer for token.userId, then next().
 */
export function appHarnessAuthMiddleware(): RequestHandler {
  return (req, res, next) => {
    // ── 1. Token extraction ─────────────────────────────────────────────────
    const rawToken =
      (req.headers['x-app-token'] as string | undefined) ??
      (req.query['__appToken'] as string | undefined);

    if (!rawToken) {
      // No token: pass through untouched — normal auth chain applies.
      return next();
    }

    // ── 2. Space id extraction ──────────────────────────────────────────────
    // Use originalUrl so this works at any router-mount depth.
    const urlPath = req.originalUrl ?? req.url;
    const match = SPACE_FILES_RE.exec(urlPath);
    if (!match) {
      // Token present but URL doesn't match a space-files path → fail-closed.
      res.status(403).json({ error: 'app-harness: token present but path is not a space files route' });
      return;
    }
    const requestedSpaceId = match[1];

    // ── 3. Token verification ───────────────────────────────────────────────
    const payload = verifyAppHarnessToken(rawToken);
    if (!payload) {
      res.status(403).json({ error: 'app-harness: invalid or expired token' });
      return;
    }

    // ── 4. Scope check ──────────────────────────────────────────────────────
    if (payload.spaceId !== requestedSpaceId) {
      res.status(403).json({ error: 'app-harness: token scope does not match requested space' });
      return;
    }

    // ── 5. Inject synthetic viewer ──────────────────────────────────────────
    (req as any).user = buildSyntheticViewer(payload.userId);
    return next();
  };
}

/**
 * Wire the app-harness middleware into the Express app.
 * Must be registered BEFORE mountSpaceApi / createSpaceApi so that
 * req.user is populated before the per-route viewerOf() reads it.
 *
 * Task 5 will add the static harness HTML route here.
 */
export function mountAppHarnessApi(
  app: Express,
  // deps reserved for Task 5 (static harness page, token-mint endpoint, etc.)
  _deps?: Record<string, unknown>,
): void {
  app.use(appHarnessAuthMiddleware());
  // Task 5: static harness route goes here
}
