import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  BadRequestException,
  InternalServerErrorException,
  Logger,
} from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { PrismaService } from "../../prisma/prisma.service";
import type { AuthenticatedRequest } from "../types/user.types";

/**
 * WorkspaceGuard ensures that:
 * 1. A workspace is specified in the request (header, param, or body)
 * 2. The authenticated user is a member of that workspace
 *
 * This guard should be used in combination with AuthGuard:
 *
 * @example
 * ```typescript
 * @Controller('tasks')
 * @UseGuards(AuthGuard, WorkspaceGuard)
 * export class TasksController {
 *   @Get()
 *   async getTasks(@Workspace() workspaceId: string) {
 *     // workspaceId is verified and available
 *     // Service layer must use withUserContext() for RLS
 *   }
 * }
 * ```
 *
 * The workspace ID can be provided via:
 * - Header: `X-Workspace-Id` (recommended)
 * - URL parameter: `:workspaceId`
 * - Request body: `workspaceId` field
 * - Query parameter: `?workspaceId=xxx` (backward compatibility)
 *
 * Priority: Header > Param > Body > Query
 *
 * Note: RLS context must be set at the service layer using withUserContext()
 * or withUserTransaction() to ensure proper transaction scoping with connection pooling.
 */
@Injectable()
export class WorkspaceGuard implements CanActivate {
  private readonly logger = new Logger(WorkspaceGuard.name);

  constructor(private readonly prisma: PrismaService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
    const user = request.user;

    if (!user?.id) {
      throw new ForbiddenException("User not authenticated");
    }

    // Extract workspace ID from request
    const workspaceId = this.extractWorkspaceId(request);

    if (!workspaceId) {
      throw new BadRequestException(
        "Workspace ID is required (via header X-Workspace-Id, URL parameter, request body, or query string)"
      );
    }

    // Verify user is a member of the workspace
    const isMember = await this.verifyWorkspaceMembership(user.id, workspaceId);

    if (!isMember) {
      this.logger.warn(
        `Access denied: User ${user.id} is not a member of workspace ${workspaceId}`
      );
      throw new ForbiddenException("You do not have access to this workspace");
    }

    // Attach workspace info to request for convenience
    request.workspace = {
      id: workspaceId,
    };

    // Also attach workspaceId to user object for backward compatibility
    if (request.user) {
      request.user.workspaceId = workspaceId;
    }

    this.logger.debug(`Workspace access granted: User ${user.id} → Workspace ${workspaceId}`);

    return true;
  }

  /**
   * Extracts workspace ID from request in order of priority:
   * 1. X-Workspace-Id header (recommended)
   * 2. :workspaceId URL parameter
   * 3. workspaceId in request body
   * 4. workspaceId query parameter (for backward compatibility)
   */
  private extractWorkspaceId(request: AuthenticatedRequest): string | undefined {
    // 1. Check header (recommended approach)
    const headerWorkspaceId = request.headers["x-workspace-id"];
    if (typeof headerWorkspaceId === "string") {
      return headerWorkspaceId;
    }

    // 2. Check URL params (:workspaceId in route)
    const paramWorkspaceId = request.params.workspaceId;
    if (paramWorkspaceId) {
      return paramWorkspaceId;
    }

    // 3. Check request body (body may be undefined for GET requests despite Express typings)
    const body = request.body as Record<string, unknown> | undefined;
    if (body && typeof body.workspaceId === "string") {
      return body.workspaceId;
    }

    // 4. Check query string (backward compatibility for existing clients)
    // Access query property if it exists (may not be in all request types)
    const requestWithQuery = request as typeof request & { query?: Record<string, unknown> };
    const queryWorkspaceId = requestWithQuery.query?.workspaceId;
    if (typeof queryWorkspaceId === "string") {
      return queryWorkspaceId;
    }

    return undefined;
  }

  /**
   * Verifies that a user is a member of the specified workspace
   *
   * SEC-API-2 FIX: Database errors are no longer swallowed as "access denied".
   * Connection timeouts, pool exhaustion, and other infrastructure errors
   * are propagated as 500 errors to avoid masking operational issues.
   */
  private async verifyWorkspaceMembership(userId: string, workspaceId: string): Promise<boolean> {
    try {
      const member = await this.prisma.workspaceMember.findUnique({
        where: {
          workspaceId_userId: {
            workspaceId,
            userId,
          },
        },
      });

      return member !== null;
    } catch (error) {
      // Only handle Prisma "not found" errors (P2025) as expected cases
      // All other database errors (connection, timeout, pool) should propagate
      if (
        error instanceof Prisma.PrismaClientKnownRequestError &&
        error.code === "P2025" // Record not found
      ) {
        return false;
      }

      // Log the error before propagating
      this.logger.error(
        `Database error during workspace membership check: ${error instanceof Error ? error.message : "Unknown error"}`,
        error instanceof Error ? error.stack : undefined
      );

      // Propagate infrastructure errors as 500s, not access denied
      throw new InternalServerErrorException("Failed to verify workspace access");
    }
  }
}
