import {
  Controller,
  Post,
  Get,
  Patch,
  Body,
  Param,
  Query,
  UseGuards,
  HttpCode,
  HttpStatus,
  Logger,
  BadRequestException,
  NotFoundException,
  ForbiddenException,
} from '@nestjs/common';
import {
  ApiTags,
  ApiOperation,
  ApiResponse,
  ApiBearerAuth,
  ApiParam,
  ApiQuery,
} from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { RequestsService, PaginatedResult } from './requests.service';
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
import { CreateRequestDto } from './dto/create-request.dto';
import { UpdateRequestDto } from './dto/update-request.dto';
import { RequestQueryDto } from './dto/request-query.dto';
import { PaginationDto } from './dto/pagination.dto';
import { RequestResponseDto } from './dto/request-response.dto';
import { RequestDetailResponseDto } from './dto/request-detail-response.dto';
import { TimelineEventDto } from './dto/timeline-event.dto';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { CorrelationId } from '../../common/decorators/correlation-id.decorator';
import { Applicant } from '../../database/models/applicant.model';
import { LicenseRequest } from '../../database/models/license-request.model';
import { JwtPayload } from '../../common/interfaces/request-context.interface';
import { UserRole, RequestStatus } from '../../common/enums';
import { UuidValidationPipe } from '../../common/pipes/uuid-validation.pipe';
import { Inject } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';

@ApiTags('Requests')
@Controller('requests')
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
export class RequestsController {
  private readonly logger = new Logger(RequestsController.name);

  constructor(
    private readonly requestsService: RequestsService,
    @Inject(Applicant)
    private readonly applicantModel: typeof Applicant,
  ) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({
    summary: 'Create new license request',
    description: 'Create a new license request in DRAFT status',
  })
  @ApiResponse({
    status: 201,
    description: 'License request created successfully',
    type: RequestResponseDto,
  })
  @ApiResponse({
    status: 400,
    description: 'Invalid request data',
  })
  @ApiResponse({
    status: 401,
    description: 'Unauthorized',
  })
  async create(
    @Body() createRequestDto: CreateRequestDto,
    @CurrentUser() user: JwtPayload,
    @CorrelationId() correlationId: string,
  ): Promise<RequestResponseDto> {
    this.logger.debug(`[${correlationId}] Creating new request for user: ${user.email}`);

    // Only citizens/applicants can create requests (CITIZEN is the database role name)
    if (user.role !== UserRole.APPLICANT && user.role !== ('CITIZEN' as any)) {
      throw new ForbiddenException('Only citizens can create license requests');
    }

    // Look up applicant by email
    const applicant = await this.applicantModel.query().findOne({ email: user.email });
    if (!applicant) {
      throw new BadRequestException(`No applicant profile found for user: ${user.email}`);
    }

    const request = await this.requestsService.create(applicant.id, createRequestDto);
    return this.mapToResponseDto(request);
  }

  @Get()
  @ApiOperation({
    summary: 'List license requests',
    description: 'Get paginated list of license requests with optional filters',
  })
  @ApiQuery({
    name: 'status',
    required: false,
    description: 'Filter by status',
  })
  @ApiQuery({
    name: 'requestType',
    required: false,
    description: 'Filter by request type',
  })
  @ApiQuery({
    name: 'applicantId',
    required: false,
    description: 'Filter by applicant ID',
  })
  @ApiQuery({
    name: 'page',
    required: false,
    type: Number,
    description: 'Page number (default: 1)',
  })
  @ApiQuery({
    name: 'limit',
    required: false,
    type: Number,
    description: 'Items per page (default: 20)',
  })
  @ApiResponse({
    status: 200,
    description: 'List of license requests',
    schema: {
      properties: {
        data: {
          type: 'array',
          items: { $ref: '#/components/schemas/RequestResponseDto' },
        },
        total: { type: 'number' },
        page: { type: 'number' },
        limit: { type: 'number' },
        totalPages: { type: 'number' },
        hasNextPage: { type: 'boolean' },
      },
    },
  })
  async findAll(
    @Query() query: RequestQueryDto,
    @CurrentUser() user: JwtPayload,
    @CorrelationId() correlationId: string,
  ): Promise<any> {
    this.logger.debug(`[${correlationId}] Fetching license requests`);

    // Citizens can only see their own requests
    if (user.role === UserRole.APPLICANT || user.role === ('CITIZEN' as any)) {
      const applicant = await this.applicantModel.query().findOne({ email: user.email });
      if (!applicant) {
        throw new BadRequestException('No applicant profile found for user');
      }
      query.applicantId = applicant.id;
    }

    // Department users can only see requests assigned to their department
    if (user.role === UserRole.DEPARTMENT && user.departmentCode) {
      query.departmentCode = user.departmentCode;
    }

    const { data, total } = await this.requestsService.findAll(query);
    const totalPages = Math.ceil(total / query.limit);
    return {
      data: data.map(r => this.mapToResponseDto(r)),
      total,
      page: query.page,
      limit: query.limit,
      totalPages,
      hasNextPage: query.page < totalPages,
    };
  }

  @Get('pending')
  @ApiOperation({
    summary: 'Get pending requests for department',
    description: 'Get paginated list of pending requests for the current department',
  })
  @ApiQuery({
    name: 'page',
    required: false,
    type: Number,
    description: 'Page number (default: 1)',
  })
  @ApiQuery({
    name: 'limit',
    required: false,
    type: Number,
    description: 'Items per page (default: 20)',
  })
  @ApiResponse({
    status: 200,
    description: 'Pending requests',
  })
  async findPending(
    @Query() query: PaginationDto,
    @CorrelationId() correlationId: string,
  ): Promise<any> {
    this.logger.debug(`[${correlationId}] Fetching pending requests`);

    const deptCode = 'FIRE_SAFETY';
    const { data, total } = await this.requestsService.findPendingForDepartment(deptCode, query);
    return {
      data: data.map(r => this.mapToResponseDto(r)),
      meta: {
        total,
        page: query.page,
        limit: query.limit,
        totalPages: Math.ceil(total / query.limit),
      },
    };
  }

  @Get(':id')
  @ApiOperation({
    summary: 'Get request details',
    description: 'Get full details of a license request including documents and approvals',
  })
  @ApiParam({
    name: 'id',
    description: 'Request ID (UUID)',
  })
  @ApiResponse({
    status: 200,
    description: 'Request details',
    type: RequestDetailResponseDto,
  })
  @ApiResponse({
    status: 404,
    description: 'Request not found',
  })
  async findById(
    @Param('id', UuidValidationPipe) id: string,
    @CurrentUser() user: JwtPayload,
    @CorrelationId() correlationId: string,
  ): Promise<RequestDetailResponseDto> {
    this.logger.debug(`[${correlationId}] Fetching request details: ${id}`);

    const request = await this.requestsService.findById(id);

    // Citizens can only view their own requests
    if (user.role === UserRole.APPLICANT || user.role === ('CITIZEN' as any)) {
      const applicant = await this.applicantModel.query().findOne({ email: user.email });
      if (!applicant) {
        throw new ForbiddenException('No applicant profile found for user');
      }
      if (request.applicantId !== applicant.id) {
        throw new ForbiddenException('You can only view your own requests');
      }
    }

    // Department users can only view requests assigned to their department
    if (user.role === UserRole.DEPARTMENT && user.departmentCode) {
      const approvals = (request as any).approvals;
      // Check if any approval is for this department
      // Try department.code first (if relation loaded), fall back to departmentId lookup
      const hasApproval =
        Array.isArray(approvals) &&
        approvals.some((a: any) => {
          // Primary check: department relation loaded with code
          if ((a as any).department?.code === user.departmentCode) {
            return true;
          }
          // Secondary check: compare departmentId with user's department sub (which is the department ID)
          if (a.departmentId && a.departmentId === user.sub) {
            return true;
          }
          return false;
        });
      if (!hasApproval) {
        throw new ForbiddenException('You can only view requests assigned to your department');
      }
    }

    return this.mapToDetailResponseDto(request);
  }

  @Post(':id/submit')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'Submit request for approval',
    description: 'Submit a draft request for review',
  })
  @ApiParam({
    name: 'id',
    description: 'Request ID (UUID)',
  })
  @ApiResponse({
    status: 200,
    description: 'Request submitted successfully',
    type: RequestResponseDto,
  })
  @ApiResponse({
    status: 400,
    description: 'Cannot submit request (invalid status or missing documents)',
  })
  @ApiResponse({
    status: 404,
    description: 'Request not found',
  })
  async submit(
    @Param('id', UuidValidationPipe) id: string,
    @CurrentUser() user: JwtPayload,
    @CorrelationId() correlationId: string,
  ): Promise<RequestResponseDto> {
    this.logger.debug(`[${correlationId}] Submitting request: ${id}`);

    // Verify ownership: only the applicant who created the request can submit it
    const request = await this.requestsService.findById(id);

    // Look up applicant by email
    const applicant = await this.applicantModel.query().findOne({ email: user.email });
    if (!applicant) {
      throw new ForbiddenException('No applicant profile found for user');
    }

    if (request.applicantId !== applicant.id) {
      throw new ForbiddenException('You can only submit your own requests');
    }

    const submittedRequest = await this.requestsService.submit(id);
    return this.mapToResponseDto(submittedRequest);
  }

  @Post(':id/cancel')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'Cancel request',
    description: 'Cancel a license request',
  })
  @ApiParam({
    name: 'id',
    description: 'Request ID (UUID)',
  })
  @ApiResponse({
    status: 200,
    description: 'Request cancelled successfully',
    type: RequestResponseDto,
  })
  @ApiResponse({
    status: 400,
    description: 'Cannot cancel request (invalid status)',
  })
  @ApiResponse({
    status: 404,
    description: 'Request not found',
  })
  async cancel(
    @Param('id', UuidValidationPipe) id: string,
    @Body('reason') reason: string,
    @CurrentUser() user: JwtPayload,
    @CorrelationId() correlationId: string,
  ): Promise<any> {
    this.logger.debug(`[${correlationId}] Cancelling request: ${id}`);

    if (!reason || reason.trim().length === 0) {
      throw new BadRequestException('Cancellation reason is required');
    }

    if (reason.trim().length < 3) {
      throw new BadRequestException('Cancellation reason must be at least 3 characters');
    }

    // Verify ownership: only the applicant who created the request can cancel it (unless admin)
    const existingRequest = await this.requestsService.findById(id);

    // Admin can cancel any request
    if (user.role !== UserRole.ADMIN) {
      // Look up applicant by email
      const applicant = await this.applicantModel.query().findOne({ email: user.email });
      if (!applicant) {
        throw new ForbiddenException('No applicant profile found for user');
      }

      if (existingRequest.applicantId !== applicant.id) {
        throw new ForbiddenException('You can only cancel your own requests');
      }
    }

    const request = await this.requestsService.cancel(id, reason, user.sub);
    return {
      ...this.mapToResponseDto(request),
      message: 'Request cancelled successfully',
      notificationSent: true,
    };
  }

  @Patch(':id')
  @ApiOperation({
    summary: 'Update request metadata',
    description: 'Update additional metadata for a request',
  })
  @ApiParam({
    name: 'id',
    description: 'Request ID (UUID)',
  })
  @ApiResponse({
    status: 200,
    description: 'Request updated successfully',
    type: RequestResponseDto,
  })
  @ApiResponse({
    status: 404,
    description: 'Request not found',
  })
  async update(
    @Param('id', UuidValidationPipe) id: string,
    @Body() updateDto: UpdateRequestDto,
    @CorrelationId() correlationId: string,
  ): Promise<RequestResponseDto> {
    this.logger.debug(`[${correlationId}] Updating request: ${id}`);

    const request = await this.requestsService.update(id, updateDto);
    return this.mapToResponseDto(request);
  }

  @Get(':id/timeline')
  @ApiOperation({
    summary: 'Get request timeline',
    description: 'Get timeline of events for a license request',
  })
  @ApiParam({
    name: 'id',
    description: 'Request ID (UUID)',
  })
  @ApiResponse({
    status: 200,
    description: 'Request timeline',
    type: [TimelineEventDto],
  })
  @ApiResponse({
    status: 404,
    description: 'Request not found',
  })
  async getTimeline(
    @Param('id', UuidValidationPipe) id: string,
    @CorrelationId() correlationId: string,
  ): Promise<TimelineEventDto[]> {
    this.logger.debug(`[${correlationId}] Fetching timeline for request: ${id}`);

    return this.requestsService.getTimeline(id);
  }

  private mapToResponseDto(request: LicenseRequest): RequestResponseDto {
    const metadata = (request.metadata || {}) as Record<string, any>;
    const workflow = (request as any).workflow;

    // Determine current step index from completed approvals
    let currentStepIndex = 0;
    if (workflow) {
      const definition = workflow.definition as any;
      const approvals = (request as any).approvals || [];

      if (definition?.stages && definition.stages.length > 0) {
        for (let i = 0; i < definition.stages.length; i++) {
          const stage = definition.stages[i];
          const stageDeptCodes = stage.requiredApprovals?.map((ra: any) => ra.departmentCode) || [];

          const stageApprovals = approvals.filter((a: any) => {
            const deptCode = (a as any).department?.code;
            return stageDeptCodes.includes(deptCode) && !a.invalidatedAt;
          });

          const stageComplete =
            stageApprovals.length === stage.requiredApprovals?.length &&
            stageApprovals.every((a: any) => a.status === 'APPROVED');

          if (stageComplete) {
            currentStepIndex = i + 1;
          } else {
            break;
          }
        }
      }
    }

    // Determine assigned department from current stage or first pending approval
    let assignedDepartment = undefined;
    const approvals = (request as any).approvals;
    if (Array.isArray(approvals)) {
      const pendingApproval = approvals.find((a: any) => a.status === 'PENDING');
      if (pendingApproval) {
        assignedDepartment =
          (pendingApproval as any).department?.code || pendingApproval.departmentId;
      }
    }

    return {
      id: request.id,
      requestNumber: request.requestNumber,
      applicantId: request.applicantId,
      requestType: request.requestType as any,
      status: request.status as any,
      currentStageId: request.currentStageId,
      workflowId: request.workflowId,
      workflowCode: workflow?.workflowType || undefined,
      currentStepIndex,
      assignedDepartment,
      metadata: metadata,
      formData: metadata, // Alias for backward compatibility
      blockchainTxHash: request.blockchainTxHash,
      tokenId: request.tokenId,
      createdAt: request.createdAt,
      updatedAt: request.updatedAt,
      submittedAt: request.submittedAt,
      approvedAt: request.approvedAt,
      // Flatten commonly accessed metadata fields to top level
      applicantName: metadata.applicantName as string | undefined,
      businessName: metadata.businessName as string | undefined,
      // Spread any other metadata fields
      ...metadata,
    };
  }

  private mapToDetailResponseDto(request: LicenseRequest): RequestDetailResponseDto {
    const workflow = (request as any).workflow;
    const applicant = (request as any).applicant;
    const workflowState = (request as any).workflowState;
    const metadata = (request.metadata || {}) as Record<string, any>;

    // Determine current step index from completed approvals
    let currentStepIndex = 0;
    if (workflow) {
      const definition = workflow.definition as any;
      const approvals = (request as any).approvals || [];

      if (definition?.stages && definition.stages.length > 0) {
        // Calculate completed stages by checking approval status
        for (let i = 0; i < definition.stages.length; i++) {
          const stage = definition.stages[i];
          const stageDeptCodes = stage.requiredApprovals?.map((ra: any) => ra.departmentCode) || [];

          // Find approvals for this stage
          const stageApprovals = approvals.filter((a: any) => {
            const deptCode = (a as any).department?.code;
            return stageDeptCodes.includes(deptCode) && !a.invalidatedAt;
          });

          // Check if stage is complete (all approvals approved)
          const stageComplete =
            stageApprovals.length === stage.requiredApprovals?.length &&
            stageApprovals.every((a: any) => a.status === 'APPROVED');

          if (stageComplete) {
            currentStepIndex = i + 1;
          } else {
            break;
          }
        }
      }
    }

    // Determine assigned department from current stage or first pending approval
    let assignedDepartment = undefined;
    const approvals = (request as any).approvals;
    if (Array.isArray(approvals)) {
      const pendingApproval = approvals.find((a: any) => a.status === 'PENDING');
      if (pendingApproval) {
        assignedDepartment =
          (pendingApproval as any).department?.code || pendingApproval.departmentId;
      }
    }

    this.logger.debug(`Mapping request ${request.id} with status: ${request.status}`);

    const result: RequestDetailResponseDto = {
      id: request.id,
      requestNumber: request.requestNumber,
      applicantId: request.applicantId,
      requestType: request.requestType as any,
      status: (request.status || 'DRAFT') as RequestStatus,
      currentStageId: request.currentStageId,
      workflowId: request.workflowId,
      workflowCode: workflow?.workflowType || undefined,
      currentStepIndex,
      assignedDepartment,
      metadata: metadata,
      formData: metadata,
      blockchainTxHash: request.blockchainTxHash,
      tokenId: request.tokenId,
      documents:
        (request.documents as any)?.map((d: any) => ({
          id: d.id,
          docType: d.docType,
          originalFilename: d.originalFilename,
          currentVersion: d.currentVersion,
          currentHash: d.currentHash,
          minioBucket: d.minioBucket,
          isActive: d.isActive,
          createdAt: d.createdAt,
          updatedAt: d.updatedAt,
        })) || [],
      approvals:
        (request.approvals as any)?.map((a: any) => ({
          id: a.id,
          departmentId: a.departmentId,
          status: a.status,
          remarks: a.remarks,
          reviewedDocuments: a.reviewedDocuments,
          createdAt: a.createdAt,
          updatedAt: a.updatedAt,
          invalidatedAt: a.invalidatedAt,
          invalidationReason: a.invalidationReason,
        })) || [],
      createdAt: request.createdAt,
      updatedAt: request.updatedAt,
      submittedAt: request.submittedAt,
      approvedAt: request.approvedAt,
      workflow: workflow
        ? {
            id: workflow.id,
            code: workflow.workflowType,
            name: workflow.name,
            steps: workflow.definition?.steps || [],
          }
        : undefined,
      applicant: applicant
        ? {
            id: applicant.id,
            email: applicant.email,
            name: applicant.name,
            walletAddress: applicant.walletAddress || '',
          }
        : undefined,
    };

    return plainToInstance(RequestDetailResponseDto, result, { excludeExtraneousValues: false });
  }
}
