import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
  InternalServerErrorException,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { finalize } from "rxjs/operators";
import type { PrismaClient } from "@prisma/client";
import { PrismaService } from "../../prisma/prisma.service";
import { runWithRlsClient } from "../../prisma/rls-context.provider";
import type { AuthenticatedRequest } from "../types/user.types";

/**
 * Transaction-safe Prisma client type that excludes methods not available on transaction clients.
 * This prevents services from accidentally calling $connect, $disconnect, $transaction, etc.
 * on a transaction client, which would cause runtime errors.
 */
export type TransactionClient = Omit<
  PrismaClient,
  "$connect" | "$disconnect" | "$transaction" | "$on" | "$use"
>;

/**
 * RlsContextInterceptor sets Row-Level Security (RLS) session variables for authenticated requests.
 *
 * This interceptor runs after AuthGuard and WorkspaceGuard, extracting the authenticated user
 * and workspace from the request and setting PostgreSQL session variables within a transaction:
 * - SET LOCAL app.current_user_id = '...'
 * - SET LOCAL app.current_workspace_id = '...'
 *
 * The transaction-scoped Prisma client is then propagated via AsyncLocalStorage, allowing
 * services to access it via getRlsClient() without explicit dependency injection.
 *
 * ## Security Design
 *
 * SET LOCAL is used instead of SET to ensure session variables are transaction-scoped.
 * This is critical for connection pooling safety - without transaction scoping, variables
 * would leak between requests that reuse the same connection from the pool.
 *
 * The entire request handler is executed within the transaction boundary, ensuring all
 * queries inherit the RLS context.
 *
 * ## Usage
 *
 * Registered globally as APP_INTERCEPTOR in AppModule (after TelemetryInterceptor).
 * Services access the RLS client via:
 *
 * ```typescript
 * const client = getRlsClient() ?? this.prisma;
 * return client.task.findMany(); // Filtered by RLS
 * ```
 *
 * ## Unauthenticated Routes
 *
 * Routes without AuthGuard (public endpoints) will not have request.user set.
 * The interceptor gracefully handles this by skipping RLS context setup.
 *
 * @see docs/design/credential-security.md for RLS architecture
 */
@Injectable()
export class RlsContextInterceptor implements NestInterceptor {
  private readonly logger = new Logger(RlsContextInterceptor.name);

  // Transaction timeout configuration
  // Longer timeout to support file uploads, complex queries, and bulk operations
  private readonly TRANSACTION_TIMEOUT_MS = 30000; // 30 seconds
  private readonly TRANSACTION_MAX_WAIT_MS = 10000; // 10 seconds to acquire connection

  constructor(private readonly prisma: PrismaService) {}

  /**
   * Intercept HTTP requests and set RLS context if user is authenticated.
   *
   * @param context - The execution context
   * @param next - The next call handler
   * @returns Observable of the response with RLS context applied
   */
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
    const user = request.user;

    // Skip RLS context setup for unauthenticated requests
    if (!user?.id) {
      this.logger.debug("Skipping RLS context: no authenticated user");
      return next.handle();
    }

    const userId = user.id;
    const workspaceId = request.workspace?.id ?? user.workspaceId;

    this.logger.debug(
      `Setting RLS context: user=${userId}${workspaceId ? `, workspace=${workspaceId}` : ""}`
    );

    // Execute the entire request within a transaction with RLS context set
    return new Observable((subscriber) => {
      this.prisma
        .$transaction(
          async (tx) => {
            // Use set_config(..., true) so values are transaction-local and parameterized safely.
            // Direct SET LOCAL with bind parameters produces invalid SQL on PostgreSQL.
            await tx.$executeRaw`SELECT set_config('app.current_user_id', ${userId}, true)`;

            if (workspaceId) {
              await tx.$executeRaw`SELECT set_config('app.current_workspace_id', ${workspaceId}, true)`;
            }

            // Propagate the transaction client via AsyncLocalStorage
            // This allows services to access it via getRlsClient()
            // Use TransactionClient type to maintain type safety
            return runWithRlsClient(tx as TransactionClient, () => {
              return new Promise((resolve, reject) => {
                next
                  .handle()
                  .pipe(
                    finalize(() => {
                      this.logger.debug("RLS context cleared");
                    })
                  )
                  .subscribe({
                    next: (value) => {
                      subscriber.next(value);
                      resolve(value);
                    },
                    error: (error: unknown) => {
                      const err = error instanceof Error ? error : new Error(String(error));
                      subscriber.error(err);
                      reject(err);
                    },
                    complete: () => {
                      subscriber.complete();
                      resolve(undefined);
                    },
                  });
              });
            });
          },
          {
            timeout: this.TRANSACTION_TIMEOUT_MS,
            maxWait: this.TRANSACTION_MAX_WAIT_MS,
          }
        )
        .catch((error: unknown) => {
          const err = error instanceof Error ? error : new Error(String(error));
          this.logger.error(`Failed to set RLS context: ${err.message}`, err.stack);
          // Sanitize error before sending to client to prevent information disclosure
          // (schema info, internal variable names, connection details, etc.)
          subscriber.error(new InternalServerErrorException("Request processing failed"));
        });
    });
  }
}
