/**
 * JWT validation for the Mailforge Plugin.
 *
 * Uses `jose` (Web Crypto-based) instead of `jsonwebtoken` so the same code
 * runs in both Node.js (API routes, Worker) and the Edge runtime that
 * Next.js Middleware executes in. `jsonwebtoken` depends on Node's `crypto`
 * module which is unavailable in Edge — using it there silently fails
 * verification, which is a footgun we want to avoid.
 *
 * Algorithm: HS256 (symmetric, fast, no PKI). Swap to ES256/RS256 by
 * changing the algorithm string AND the key-loading helper.
 *
 * Expected payload shape (host-CRM responsibility):
 *   {
 *     userId: string,
 *     workspaceIds: string[],
 *     permissions: string[],
 *     email?: string,
 *     displayName?: string,
 *     iat: number,
 *     exp: number
 *   }
 */

import { jwtVerify, SignJWT, errors as joseErrors } from 'jose';
import type { UserContext } from '@/core/types';

export type JwtErrorKind =
  | 'missing-secret'
  | 'malformed'
  | 'expired'
  | 'invalid-signature'
  | 'invalid-payload';

export class JwtError extends Error {
  readonly kind: JwtErrorKind;
  constructor(kind: JwtErrorKind, message: string) {
    super(message);
    this.kind = kind;
    this.name = 'JwtError';
  }
}

function getSecretBytes(): Uint8Array {
  const secret = process.env.MAILFORGE_JWT_SECRET?.trim();
  if (!secret) {
    throw new JwtError(
      'missing-secret',
      'MAILFORGE_JWT_SECRET is not set. Generate via `openssl rand -hex 64` and put it in .env.local. Must match the host-CRM secret.',
    );
  }
  if (secret.length < 32) {
    throw new JwtError(
      'missing-secret',
      'MAILFORGE_JWT_SECRET is suspiciously short (< 32 chars). Use at least 64 hex characters for HS256.',
    );
  }
  return new TextEncoder().encode(secret);
}

function assertStringArray(value: unknown, field: string): string[] {
  if (!Array.isArray(value)) {
    throw new JwtError('invalid-payload', `JWT payload.${field} must be an array, got ${typeof value}`);
  }
  for (const item of value) {
    if (typeof item !== 'string') {
      throw new JwtError(
        'invalid-payload',
        `JWT payload.${field} must contain only strings, got ${typeof item}`,
      );
    }
  }
  return value as string[];
}

/**
 * Verify a JWT and produce a typed UserContext. Throws `JwtError` on any
 * failure — callers should map error kinds to HTTP status codes (401 for
 * expired/invalid, 500 for missing-secret).
 *
 * Async because jose uses Web Crypto under the hood; the Node `crypto.verify`
 * call was synchronous, so callers (proxy + withAuth) now `await` it.
 */
export async function verifyJwt(token: string): Promise<UserContext> {
  const secret = getSecretBytes();

  let payload: Record<string, unknown>;
  try {
    const { payload: p } = await jwtVerify(token, secret, { algorithms: ['HS256'] });
    payload = p as Record<string, unknown>;
  } catch (err) {
    if (err instanceof joseErrors.JWTExpired) {
      throw new JwtError('expired', 'JWT expired — host CRM should issue a fresh token');
    }
    if (err instanceof joseErrors.JWSSignatureVerificationFailed) {
      throw new JwtError(
        'invalid-signature',
        'JWT signature verification failed. Check MAILFORGE_JWT_SECRET matches the host CRM.',
      );
    }
    if (err instanceof joseErrors.JWTInvalid || err instanceof joseErrors.JWSInvalid) {
      throw new JwtError('malformed', err.message);
    }
    throw new JwtError('malformed', err instanceof Error ? err.message : String(err));
  }

  if (typeof payload.userId !== 'string' || payload.userId.length === 0) {
    throw new JwtError('invalid-payload', 'JWT payload.userId must be a non-empty string');
  }

  return {
    userId: payload.userId,
    workspaceIds: assertStringArray(payload.workspaceIds ?? [], 'workspaceIds'),
    permissions: assertStringArray(payload.permissions ?? [], 'permissions'),
    email: typeof payload.email === 'string' ? payload.email : undefined,
    displayName:
      typeof payload.displayName === 'string' ? payload.displayName : undefined,
  };
}

/**
 * Test-helper: sign a JWT with the local secret. Used in scripts/test-auth.ts
 * and by dev tools — do NOT use this in production code paths. The host CRM
 * is the JWT issuer, not Mailforge.
 */
export async function signJwt(
  payload: Omit<UserContext, 'permissions'> & { permissions?: string[] },
  ttlSeconds = 900,
): Promise<string> {
  const secret = getSecretBytes();
  return await new SignJWT({
    userId: payload.userId,
    workspaceIds: payload.workspaceIds,
    permissions: payload.permissions ?? [],
    email: payload.email,
    displayName: payload.displayName,
  })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime(Math.floor(Date.now() / 1000) + ttlSeconds)
    .sign(secret);
}
