import {
  CallHandler,
  ExecutionContext,
  ForbiddenException,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { ClsService } from 'nestjs-cls';
import type { Request } from 'express';
import { CLS_READ_ONLY } from '../cls/cls.constants';

const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);

/**
 * Routes a read-only ("View as user") session may still call with a
 * mutating verb: exiting the view-as session, logging out, and rotating
 * the session. Matched as a suffix of the request path.
 */
const READ_ONLY_ALLOWLIST = ['/auth/impersonate/exit', '/auth/logout', '/auth/refresh'];

/**
 * ReadOnlyInterceptor — the hard "can't change anything" wall for the
 * read-only impersonation feature. While a session is a view-as session
 * (CLS_READ_ONLY = true, set by JwtStrategy from the token's read_only
 * claim), every state-changing request (POST/PUT/PATCH/DELETE) is
 * rejected 403 — regardless of what the impersonated user is normally
 * allowed to do — except the small allowlist above.
 *
 * Runs as a global APP_INTERCEPTOR, i.e. AFTER the auth guard has
 * populated CLS, so the read_only flag is reliably present. Public
 * routes (no JWT) never set the flag and are unaffected.
 */
@Injectable()
export class ReadOnlyInterceptor implements NestInterceptor {
  constructor(private readonly cls: ClsService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    if (context.getType() !== 'http') return next.handle();

    const req = context.switchToHttp().getRequest<Request>();
    const readOnly = this.cls.get<boolean>(CLS_READ_ONLY) === true;

    if (readOnly && MUTATING_METHODS.has(req.method)) {
      const path = req.path || req.url || '';
      const allowed = READ_ONLY_ALLOWLIST.some((suffix) => path.endsWith(suffix));
      if (!allowed) {
        throw new ForbiddenException(
          'You are viewing as another user (read-only). Exit the view-as session to make changes.',
        );
      }
    }

    return next.handle();
  }
}
