import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { ApiCode } from '@pac/types';
import type { Request, Response } from 'express';
import { Observable, map } from 'rxjs';

/**
 * Universal response wrapper. Runs as the LAST interceptor in the chain so
 * inner data has already been validated by ZodSerializerInterceptor.
 *
 * Every successful (non-SSE, non-health) response is forced to HTTP 200 and
 * wrapped:
 *   { code: 0, msg: 'ok', data: <controller return value> }
 *
 * Skipped for:
 *   - text/event-stream (SSE — agent script streaming)
 *   - GET /health  (probe shape must stay simple)
 *   - GET /api/openapi.json + /api/docs (Scalar tooling)
 */
@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
  intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    const httpCtx = ctx.switchToHttp();
    const req = httpCtx.getRequest<Request>();
    const res = httpCtx.getResponse<Response>();

    if (shouldSkip(req, res)) return next.handle();

    return next.handle().pipe(
      map((data) => {
        // Don't wrap if the handler already returned an envelope (defensive).
        if (
          data &&
          typeof data === 'object' &&
          'code' in (data as Record<string, unknown>) &&
          'msg' in (data as Record<string, unknown>) &&
          'data' in (data as Record<string, unknown>)
        ) {
          return data;
        }
        // Force HTTP 200 — original status (e.g. 201/202) becomes irrelevant in this contract.
        res.status(200);
        return { code: ApiCode.OK, msg: 'ok', data: data ?? null };
      }),
    );
  }
}

function shouldSkip(req: Request, res: Response): boolean {
  const url = req.url ?? '';
  if (url === '/health') return true;
  if (url.startsWith('/api/openapi.json') || url.startsWith('/api/docs')) return true;
  // Twilio voice webhook 返回裸 TwiML(XML),不能包成 {code,msg,data} JSON
  if (url.startsWith('/pac/v1/twilio/voice')) return true;
  const ct = res.getHeader('content-type');
  if (typeof ct === 'string' && ct.startsWith('text/event-stream')) return true;
  return false;
}
