import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';

import {
  Prisma,
  ProgressReportMinistryStatus,
  ProgressReportStatus,
  RgcDecisionStatus,
} from '@/generated/prisma/client';
import { SystemNotificationsService } from '@/modules/system-notifications/system-notifications.service';
import { UploadService } from '@/modules/upload/upload.service';

import { QueryProgressReportMinistriesDto } from './dto/query-progress-report-ministries.dto';
import { QueryPswgProgressReportsDto } from './dto/query-pswg-progress-reports.dto';
import { ReviewProgressReportMinistryDto } from './dto/review-progress-report-ministry.dto';
import { SaveProgressReportIssueCommentDto } from './dto/save-progress-report-issue-comment.dto';
import { UpdateProgressReportIssueStatusDto } from './dto/update-progress-report-issue-status.dto';
import { UpdateMyProgressReportMinistryDto } from './dto/update-my-progress-report-ministry.dto';
import { UpsertMyProgressReportIssueDto } from './dto/upsert-my-progress-report-issue.dto';
import { UpsertMyProgressReportRgcDecisionDto } from './dto/upsert-my-progress-report-rgc-decision.dto';
import { UpdateProgressReportRgcDecisionStatusDto } from './dto/update-progress-report-rgc-decision-status.dto';
import {
  MinistryProgressReportsRepository,
  type MinistryOpenIssue,
  type PswgRgcDecision,
  type ProgressReportMinistryListItem,
  type ProgressReportMinistryWithRelations,
  type ProgressReportIssueCommentWithRelations,
  type ProgressReportUpdateWithRelations,
} from './ministry-progress-reports.repository';

const CDC_GPSF_ROLE_NAME = 'cdc_g-psf';
const MINISTRY_ROLE_NAME = 'ministry';
const PRIVATE_SECTOR_ROLE_NAME = 'private_sector';
const CDC_VISIBLE_STATUSES: ProgressReportMinistryStatus[] = [
  ProgressReportMinistryStatus.SUBMITTED,
  ProgressReportMinistryStatus.CDC_UNDER_REVIEW,
  ProgressReportMinistryStatus.COMPLETED,
];

const ministryTransitions: Partial<
  Record<ProgressReportMinistryStatus, ProgressReportMinistryStatus[]>
> = {
  DRAFT: [ProgressReportMinistryStatus.SHARED_WITH_PSWG],
};

// A Ministry may upload or replace its report PDF while it is a draft or
// after PSWG has reviewed it (before submitting to CDC).
const MINISTRY_UPLOADABLE_STATUSES: ProgressReportMinistryStatus[] = [
  ProgressReportMinistryStatus.DRAFT,
  ProgressReportMinistryStatus.PSWG_REVIEWED,
];

// A report can be submitted to CDC once PSWG has reviewed it.
const MINISTRY_SUBMITTABLE_STATUSES: ProgressReportMinistryStatus[] = [
  ProgressReportMinistryStatus.PSWG_REVIEWED,
];

const MINISTRY_ISSUE_UPDATE_STATUSES: ProgressReportMinistryStatus[] = [
  ProgressReportMinistryStatus.DRAFT,
  ProgressReportMinistryStatus.PSWG_REVIEWED,
];

const CDC_ISSUE_UPDATE_STATUSES: ProgressReportMinistryStatus[] = [
  ProgressReportMinistryStatus.SUBMITTED,
  ProgressReportMinistryStatus.CDC_UNDER_REVIEW,
];

const cdcTransitions: Partial<
  Record<ProgressReportMinistryStatus, ProgressReportMinistryStatus[]>
> = {
  SUBMITTED: [ProgressReportMinistryStatus.CDC_UNDER_REVIEW],
  CDC_UNDER_REVIEW: [ProgressReportMinistryStatus.COMPLETED],
};

@Injectable()
export class MinistryProgressReportsService {
  private readonly logger = new Logger(MinistryProgressReportsService.name);

  constructor(
    private readonly repository: MinistryProgressReportsRepository,
    private readonly uploads: UploadService,
    private readonly notifications: SystemNotificationsService,
  ) {}

  async buildAssignmentsForSentReport(): Promise<
    Prisma.ProgressReportMinistryCreateManyProgressReportInput[]
  > {
    const ministryIds = await this.repository.findActiveMinistryIds();

    return ministryIds.map((ministryId) => ({
      ministryId,
      issues: 0,
      status: ProgressReportMinistryStatus.DRAFT,
    }));
  }

  async getListMinistryId(userId: number): Promise<number | undefined> {
    const context = await this.repository.findUserAccessContext(userId);
    const roles =
      context?.roles.map((item) => item.role.name.toLowerCase()) ?? [];

    if (!roles.includes(MINISTRY_ROLE_NAME)) {
      return undefined;
    }

    return this.getSingleMinistryId(
      context?.stakeholders.map((item) => item.stakeholderId) ?? [],
    );
  }

  async isPswgUser(userId: number): Promise<boolean> {
    const context = await this.repository.findUserAccessContext(userId);
    const roles =
      context?.roles.map((item) => item.role.name.toLowerCase()) ?? [];

    return roles.includes(PRIVATE_SECTOR_ROLE_NAME);
  }

  async findAll(
    progressReportId: number,
    query: QueryProgressReportMinistriesDto,
    userId: number,
  ) {
    await this.ensureCdcGpsfUser(userId);
    await this.ensureActiveReport(progressReportId);

    const page = query.page ?? 1;
    const limit = query.limit ?? 10;
    const [items, total] = await this.repository.findManyAndCount({
      progressReportId,
      skip: (page - 1) * limit,
      take: limit,
      status: query.submittedToCdc
        ? { in: CDC_VISIBLE_STATUSES }
        : query.status
          ? { equals: query.status }
          : undefined,
      hasBeenSubmitted: query.submittedToCdc,
      search: query.search?.trim(),
    });
    const issueCounts =
      await this.repository.countOpenPrimaryIssuesByMinistryIds(
        items.map((item) => item.ministryId),
      );
    const issueCountByMinistryId = new Map<number, number>(
      issueCounts.map(
        (item) => [item.stakeholderId, item._count._all] as const,
      ),
    );

    return {
      data: await Promise.all(
        items.map(async (item) => ({
          ...(await this.serializeListItem(item)),
          issues: issueCountByMinistryId.get(item.ministryId) ?? 0,
        })),
      ),
      meta: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  async findMine(progressReportId: number, userId: number) {
    const ministryId = await this.resolveCurrentMinistryId(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );

    // Nobody has submitted yet, so the signed-in Ministry user is the one
    // preparing this report. Show them instead of an empty "Prepared by".
    const currentUser = await this.repository.findUserProfile(userId);

    return this.serializeDetail(assignment, currentUser);
  }

  async findOne(progressReportId: number, ministryId: number, userId: number) {
    await this.ensureCdcGpsfUser(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );

    if (!CDC_VISIBLE_STATUSES.includes(assignment.status)) {
      throw new NotFoundException(
        `Submitted Ministry assignment was not found for progress report ${progressReportId}`,
      );
    }

    return this.serializeCdcDetail(assignment);
  }

  async findAllForPswg(query: QueryPswgProgressReportsDto, userId: number) {
    const pswgIds = await this.resolveCurrentPswgIds(userId);
    const page = query.page ?? 1;
    const limit = query.limit ?? 10;
    const [items, total] = await this.repository.findPswgAssignmentsAndCount({
      pswgIds,
      skip: (page - 1) * limit,
      take: limit,
    });

    return {
      data: await Promise.all(items.map((item) => this.serialize(item))),
      meta: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  async findOneForPswg(assignmentId: number, userId: number) {
    const pswgIds = await this.resolveCurrentPswgIds(userId);
    const assignment = await this.repository.findPswgAssignmentById(
      assignmentId,
      pswgIds,
    );

    if (!assignment) {
      throw new NotFoundException(
        `Shared progress report assignment with ID ${assignmentId} was not found`,
      );
    }

    return this.serializePswgDetail(assignment, pswgIds);
  }

  async reviewForPswg(assignmentId: number, userId: number) {
    const pswgIds = await this.resolveCurrentPswgIds(userId);
    const assignment = await this.repository.findPswgAssignmentById(
      assignmentId,
      pswgIds,
    );

    if (!assignment) {
      throw new NotFoundException(
        `Shared progress report assignment with ID ${assignmentId} was not found`,
      );
    }

    if (assignment.status !== ProgressReportMinistryStatus.SHARED_WITH_PSWG) {
      throw new ConflictException(
        `Only a SHARED_WITH_PSWG report can be reviewed. Current status is ${assignment.status}.`,
      );
    }

    const updated = await this.repository.updateAssignment(assignment.id, {
      status: ProgressReportMinistryStatus.PSWG_REVIEWED,
    });

    await this.notifyStatusChange(updated, userId);

    return {
      message: 'Ministry progress report reviewed by PSWG successfully.',
      ...(await this.serializePswgDetail(updated, pswgIds)),
    };
  }

  async updateMine(
    progressReportId: number,
    userId: number,
    dto: UpdateMyProgressReportMinistryDto,
    attachment?: Express.Multer.File,
  ) {
    const ministryId = await this.resolveCurrentMinistryId(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );

    const nextStatus = dto.status ?? assignment.status;

    this.ensureTransition(assignment.status, nextStatus, ministryTransitions);

    const canUpload = MINISTRY_UPLOADABLE_STATUSES.includes(assignment.status);

    if (attachment && !canUpload) {
      throw new ConflictException(
        `A PDF cannot be changed while the report status is ${assignment.status}.`,
      );
    }

    if (
      nextStatus === ProgressReportMinistryStatus.SHARED_WITH_PSWG &&
      !assignment.attachment &&
      !attachment
    ) {
      throw new BadRequestException(
        'A Ministry progress report PDF is required before sharing with PSWG.',
      );
    }

    const updated = await this.updateWithOptionalAttachment(
      assignment,
      nextStatus,
      attachment,
    );

    if (assignment.status !== updated.status) {
      await this.notifyStatusChange(updated, userId);
    }

    return {
      message: 'Ministry progress report updated successfully.',
      ...(await this.serialize(updated)),
    };
  }

  async submitMine(progressReportId: number, userId: number) {
    const ministryId = await this.resolveCurrentMinistryId(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );

    if (!MINISTRY_SUBMITTABLE_STATUSES.includes(assignment.status)) {
      throw new ConflictException(
        `Only a PSWG_REVIEWED report can be submitted. Current status is ${assignment.status}.`,
      );
    }

    if (!assignment.attachment) {
      throw new BadRequestException(
        'A Ministry progress report PDF is required before submitting.',
      );
    }

    const updated = await this.repository.updateAssignment(assignment.id, {
      status: ProgressReportMinistryStatus.SUBMITTED,
      user: { connect: { id: userId } },
      submittedAt: new Date(),
    });

    await this.notifyStatusChange(updated, userId);

    return {
      message: 'Ministry progress report submitted to CDC-GPSF successfully.',
      ...(await this.serializeDetail(updated)),
    };
  }

  async upsertMyIssue(
    progressReportId: number,
    issueId: number,
    userId: number,
    dto: UpsertMyProgressReportIssueDto,
    attachment?: Express.Multer.File,
  ) {
    const ministryId = await this.resolveCurrentMinistryId(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );

    if (!MINISTRY_ISSUE_UPDATE_STATUSES.includes(assignment.status)) {
      throw new ConflictException(
        `Issue updates are unavailable while the report status is ${assignment.status}.`,
      );
    }

    const [issue, issueStatus, issueResolve, existingUpdate] =
      await Promise.all([
        this.repository.findOpenPrimaryIssue(issueId, ministryId),
        this.repository.findProgressUpdateStatus(dto.issueStatusId),
        this.repository.findLatestIssueResolve(issueId),
        this.repository.findProgressReportUpdate(
          progressReportId,
          ministryId,
          issueId,
        ),
      ]);

    if (!issue) {
      throw new NotFoundException(
        `Open primary-agency Issue with ID ${issueId} was not found for this Ministry.`,
      );
    }

    if (!issueStatus) {
      throw new BadRequestException(
        `Issue status with ID ${dto.issueStatusId} is not available for progress updates.`,
      );
    }

    if (!issueResolve) {
      throw new NotFoundException(
        `Active Issue resolution for Issue ID ${issueId} was not found.`,
      );
    }

    let newAttachment: string | undefined;

    try {
      if (attachment) {
        newAttachment = (
          await this.uploads.save(attachment, 'progress-report-updates', {
            allowedMimeTypes: ['application/pdf'],
          })
        ).url;
      }

      const updated = await this.repository.upsertProgressReportUpdate({
        progressReportId,
        ministryId,
        issueId,
        issueStatusId: issueStatus.id,
        issueResolveId: issueResolve.id,
        indicators: this.toNullableJson(dto.indicators),
        progressSolution: this.toNullableJson(dto.progressSolution),
        implementationChallenges: this.toNullableJson(
          dto.implementationChallenges,
        ),
        requests: this.toNullableJson(dto.requests),
        sourceOfVerification: dto.sourceOfVerification ?? null,
        linkToVerificationSource: dto.linkToVerificationSource ?? null,
        nextStep: this.toNullableJson(dto.nextStep),
        dateOfIssueSolution: dto.dateOfIssueSolution
          ? new Date(`${dto.dateOfIssueSolution}T00:00:00.000Z`)
          : null,
        attachement: newAttachment ?? existingUpdate?.attachement ?? null,
        userId,
      });

      if (
        newAttachment &&
        existingUpdate?.attachement &&
        existingUpdate.attachement !== newAttachment
      ) {
        await this.uploads.remove(existingUpdate.attachement);
      }

      return {
        message: 'Progress report issue updated successfully.',
        ...(await this.serializeProgressUpdate(updated)),
      };
    } catch (error) {
      if (newAttachment) {
        await this.uploads.remove(newAttachment);
      }

      throw error;
    }
  }

  async upsertMyRgcDecision(
    progressReportId: number,
    plenaryDecisionId: number,
    userId: number,
    dto: UpsertMyProgressReportRgcDecisionDto,
    attachment?: Express.Multer.File,
  ) {
    const ministryId = await this.resolveCurrentMinistryId(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );
    const decisionStatus = dto.status as RgcDecisionStatus;

    if (!MINISTRY_ISSUE_UPDATE_STATUSES.includes(assignment.status)) {
      throw new ConflictException(
        `RGC Decision updates are unavailable while the report status is ${assignment.status}.`,
      );
    }

    const [decision, issueStatus, existingRelation] = await Promise.all([
      this.repository.findOpenMinistryRgcDecision(
        plenaryDecisionId,
        ministryId,
      ),
      this.repository.findProgressUpdateStatusByCode(decisionStatus),
      this.repository.findProgressReportRgcDecisionUpdate(
        progressReportId,
        ministryId,
        plenaryDecisionId,
      ),
    ]);

    if (!decision) {
      throw new NotFoundException(
        `Open RGC Decision with ID ${plenaryDecisionId} was not found for this Ministry.`,
      );
    }

    if (!issueStatus) {
      throw new BadRequestException(
        `RGC Decision status ${decisionStatus} is not available for progress updates.`,
      );
    }

    const existingUpdate = existingRelation?.progressReportUpdate ?? null;
    let newAttachment: string | undefined;

    try {
      if (attachment) {
        newAttachment = (
          await this.uploads.save(attachment, 'progress-report-updates', {
            allowedMimeTypes: ['application/pdf'],
          })
        ).url;
      }

      const updated =
        await this.repository.upsertProgressReportRgcDecisionUpdate({
          progressReportId,
          ministryId,
          plenaryDecisionId,
          issueStatusId: issueStatus.id,
          status: decisionStatus,
          categoryId: dto.categoryId,
          category: dto.category,
          indicatorId: dto.indicatorId,
          meetingDate: dto.meetingDate
            ? new Date(`${dto.meetingDate}T00:00:00.000Z`)
            : undefined,
          focalPerson: dto.focalPerson,
          decision: dto.decision,
          verificationSource: dto.verificationSource,
          verificationLink: dto.verificationLink,
          indicators: this.toNullableJson(dto.indicators),
          progressSolution: this.toNullableJson(dto.progressSolution),
          implementationChallenges: this.toNullableJson(
            dto.implementationChallenges,
          ),
          requests: this.toNullableJson(dto.requests),
          sourceOfVerification: dto.sourceOfVerification ?? null,
          linkToVerificationSource: dto.linkToVerificationSource ?? null,
          nextStep: this.toNullableJson(dto.nextStep),
          dateOfIssueSolution: dto.dateOfIssueSolution
            ? new Date(`${dto.dateOfIssueSolution}T00:00:00.000Z`)
            : null,
          attachement: newAttachment ?? existingUpdate?.attachement ?? null,
          userId,
        });

      if (
        newAttachment &&
        existingUpdate?.attachement &&
        existingUpdate.attachement !== newAttachment
      ) {
        await this.uploads.remove(existingUpdate.attachement);
      }

      const savedDecision = await this.repository.findMinistryRgcDecisionById(
        plenaryDecisionId,
        ministryId,
      );

      if (!savedDecision) {
        throw new NotFoundException(
          `RGC Decision with ID ${plenaryDecisionId} was not found after saving.`,
        );
      }

      return {
        message: 'Progress report RGC Decision updated successfully.',
        plenaryDecisionId,
        status: savedDecision.status,
        category: savedDecision.category,
        indicator: savedDecision.indicator,
        meetingDate: savedDecision.meetingDate.toISOString().slice(0, 10),
        focalPerson: savedDecision.focalPerson,
        decision: savedDecision.decision,
        verificationSource: savedDecision.verificationSource,
        verificationLink: savedDecision.verificationLink,
        submittedToCdcAt: savedDecision.submittedToCdcAt,
        ...(await this.serializeRgcProgressUpdate(updated)),
      };
    } catch (error) {
      if (newAttachment) {
        await this.uploads.remove(newAttachment);
      }

      throw error;
    }
  }

  async review(
    progressReportId: number,
    ministryId: number,
    userId: number,
    dto: ReviewProgressReportMinistryDto,
  ) {
    await this.ensureCdcGpsfUser(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );

    this.ensureTransition(assignment.status, dto.status, cdcTransitions);

    const updated =
      assignment.status === dto.status
        ? assignment
        : await this.repository.updateAssignment(assignment.id, {
            status: dto.status,
            // Record the CDC-GPSF user who reviewed this Ministry report.
            reviewedBy: { connect: { id: userId } },
            reviewedAt: new Date(),
          });

    if (assignment.status !== updated.status) {
      await this.notifyStatusChange(updated, userId);
    }

    return {
      message: 'Ministry progress report review status updated successfully.',
      ...(await this.serialize(updated)),
    };
  }

  async updateIssueStatus(
    progressReportId: number,
    ministryId: number,
    issueId: number,
    userId: number,
    dto: UpdateProgressReportIssueStatusDto,
  ) {
    await this.ensureCdcGpsfUser(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );

    if (!CDC_ISSUE_UPDATE_STATUSES.includes(assignment.status)) {
      throw new ConflictException(
        `Issue status cannot be changed while the report status is ${assignment.status}.`,
      );
    }

    const [issue, issueStatus] = await Promise.all([
      this.repository.findOpenPrimaryIssue(issueId, ministryId),
      this.repository.findProgressUpdateStatus(dto.issueStatusId),
    ]);

    if (!issue) {
      throw new NotFoundException(
        `Open primary-agency Issue with ID ${issueId} was not found for this Ministry.`,
      );
    }

    if (!issueStatus) {
      throw new BadRequestException(
        `Issue status with ID ${dto.issueStatusId} must be IN_PROGRESS, SOLVED, or NOT_ADDRESSED.`,
      );
    }

    const updated = await this.repository.updateIssueStatus(
      issueId,
      issueStatus.id,
    );

    return {
      message: 'Progress report issue status updated successfully.',
      issueId: updated.id,
      issueStatus: updated.issueStatus,
    };
  }

  async createIssueComment(
    progressReportId: number,
    ministryId: number,
    issueId: number,
    userId: number,
    dto: SaveProgressReportIssueCommentDto,
  ) {
    const { updateIssue } = await this.getIssueCommentContext(
      progressReportId,
      ministryId,
      issueId,
      userId,
    );

    const commentType = await this.repository.findActiveCommentType(
      dto.commentTypeId,
    );

    if (!commentType) {
      throw new BadRequestException(
        `Comment type with ID ${dto.commentTypeId} was not found.`,
      );
    }

    if (updateIssue.progressReportIssueComments.length > 0) {
      throw new ConflictException(
        'This Progress Report Issue already has an active CDC comment.',
      );
    }

    const comment = await this.repository.createIssueComment({
      progressReportUpdateIssueId: updateIssue.id,
      commentTypeId: commentType.id,
      comment: dto.comment.trim(),
      userId,
    });

    return {
      message: 'Progress report issue comment created successfully.',
      ...this.serializeIssueComment(comment),
    };
  }

  async updateIssueComment(
    progressReportId: number,
    ministryId: number,
    issueId: number,
    commentId: number,
    userId: number,
    dto: SaveProgressReportIssueCommentDto,
  ) {
    const { updateIssue } = await this.getIssueCommentContext(
      progressReportId,
      ministryId,
      issueId,
      userId,
    );

    const [commentType, existingComment] = await Promise.all([
      this.repository.findActiveCommentType(dto.commentTypeId),
      this.repository.findActiveIssueComment(commentId, updateIssue.id),
    ]);

    if (!commentType) {
      throw new BadRequestException(
        `Comment type with ID ${dto.commentTypeId} was not found.`,
      );
    }

    if (!existingComment) {
      throw new NotFoundException(
        `Progress Report Issue comment with ID ${commentId} was not found.`,
      );
    }

    const comment = await this.repository.updateIssueComment(commentId, {
      commentTypeId: commentType.id,
      comment: dto.comment.trim(),
      userId,
    });

    return {
      message: 'Progress report issue comment updated successfully.',
      ...this.serializeIssueComment(comment),
    };
  }

  async deleteIssueComment(
    progressReportId: number,
    ministryId: number,
    issueId: number,
    commentId: number,
    userId: number,
  ) {
    const { updateIssue } = await this.getIssueCommentContext(
      progressReportId,
      ministryId,
      issueId,
      userId,
    );

    const existingComment = await this.repository.findActiveIssueComment(
      commentId,
      updateIssue.id,
    );

    if (!existingComment) {
      throw new NotFoundException(
        `Progress Report Issue comment with ID ${commentId} was not found.`,
      );
    }

    await this.repository.softDeleteIssueComment(commentId);

    return {
      message: 'Progress report issue comment deleted successfully.',
      id: commentId,
    };
  }

  async createRgcDecisionComment(
    progressReportId: number,
    ministryId: number,
    plenaryDecisionId: number,
    userId: number,
    dto: SaveProgressReportIssueCommentDto,
  ) {
    const { updateDecision } = await this.getRgcDecisionCommentContext(
      progressReportId,
      ministryId,
      plenaryDecisionId,
      userId,
    );

    const commentType = await this.repository.findActiveCommentType(
      dto.commentTypeId,
    );

    if (!commentType) {
      throw new BadRequestException(
        `Comment type with ID ${dto.commentTypeId} was not found.`,
      );
    }

    if (updateDecision.progressReportIssueComments.length > 0) {
      throw new ConflictException(
        'This Progress Report RGC Decision already has an active CDC comment.',
      );
    }

    const comment = await this.repository.createRgcDecisionComment({
      progressReportUpdatePlenaryDecisionId: updateDecision.id,
      commentTypeId: commentType.id,
      comment: dto.comment.trim(),
      userId,
    });

    return {
      message: 'Progress report RGC Decision comment created successfully.',
      ...this.serializeRgcDecisionComment(comment),
    };
  }

  async updateRgcDecisionComment(
    progressReportId: number,
    ministryId: number,
    plenaryDecisionId: number,
    commentId: number,
    userId: number,
    dto: SaveProgressReportIssueCommentDto,
  ) {
    const { updateDecision } = await this.getRgcDecisionCommentContext(
      progressReportId,
      ministryId,
      plenaryDecisionId,
      userId,
    );

    const [commentType, existingComment] = await Promise.all([
      this.repository.findActiveCommentType(dto.commentTypeId),
      this.repository.findActiveRgcDecisionComment(
        commentId,
        updateDecision.id,
      ),
    ]);

    if (!commentType) {
      throw new BadRequestException(
        `Comment type with ID ${dto.commentTypeId} was not found.`,
      );
    }

    if (!existingComment) {
      throw new NotFoundException(
        `Progress Report RGC Decision comment with ID ${commentId} was not found.`,
      );
    }

    const comment = await this.repository.updateRgcDecisionComment(commentId, {
      commentTypeId: commentType.id,
      comment: dto.comment.trim(),
      userId,
    });

    return {
      message: 'Progress report RGC Decision comment updated successfully.',
      ...this.serializeRgcDecisionComment(comment),
    };
  }

  async deleteRgcDecisionComment(
    progressReportId: number,
    ministryId: number,
    plenaryDecisionId: number,
    commentId: number,
    userId: number,
  ) {
    const { updateDecision } = await this.getRgcDecisionCommentContext(
      progressReportId,
      ministryId,
      plenaryDecisionId,
      userId,
    );

    const existingComment = await this.repository.findActiveRgcDecisionComment(
      commentId,
      updateDecision.id,
    );

    if (!existingComment) {
      throw new NotFoundException(
        `Progress Report RGC Decision comment with ID ${commentId} was not found.`,
      );
    }

    await this.repository.softDeleteRgcDecisionComment(commentId);

    return {
      message: 'Progress report RGC Decision comment deleted successfully.',
      id: commentId,
    };
  }

  async updateRgcDecisionStatus(
    progressReportId: number,
    ministryId: number,
    plenaryDecisionId: number,
    userId: number,
    dto: UpdateProgressReportRgcDecisionStatusDto,
  ) {
    await this.ensureCdcGpsfUser(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );

    if (!CDC_ISSUE_UPDATE_STATUSES.includes(assignment.status)) {
      throw new ConflictException(
        `RGC Decision status cannot be changed while the report status is ${assignment.status}.`,
      );
    }

    const decision = await this.repository.findMinistryRgcDecisionById(
      plenaryDecisionId,
      ministryId,
    );

    if (!decision) {
      throw new NotFoundException(
        `RGC Decision with ID ${plenaryDecisionId} was not found for this Ministry.`,
      );
    }

    const updated = await this.repository.updateRgcDecisionStatus(
      plenaryDecisionId,
      dto.status as RgcDecisionStatus,
    );

    return {
      message: 'Progress report RGC Decision status updated successfully.',
      plenaryDecisionId: updated.id,
      status: updated.status,
    };
  }

  private async updateWithOptionalAttachment(
    assignment: ProgressReportMinistryWithRelations,
    status: ProgressReportMinistryStatus,
    attachment?: Express.Multer.File,
  ) {
    let newAttachment: string | undefined;

    try {
      if (attachment) {
        newAttachment = (
          await this.uploads.save(attachment, 'ministry-progress-reports', {
            allowedMimeTypes: ['application/pdf'],
          })
        ).url;
      }

      const updated =
        assignment.status === status && !newAttachment
          ? assignment
          : await this.repository.updateAssignment(assignment.id, {
              status,
              ...(newAttachment ? { attachment: newAttachment } : {}),
            });

      if (newAttachment && assignment.attachment) {
        await this.uploads.remove(assignment.attachment);
      }

      return updated;
    } catch (error) {
      if (newAttachment) {
        await this.uploads.remove(newAttachment);
      }

      throw error;
    }
  }

  private ensureTransition(
    current: ProgressReportMinistryStatus,
    next: ProgressReportMinistryStatus,
    transitions: Partial<
      Record<ProgressReportMinistryStatus, ProgressReportMinistryStatus[]>
    >,
  ) {
    if (current === next) return;

    if (!transitions[current]?.includes(next)) {
      throw new ConflictException(
        `Progress report ministry status cannot change from ${current} to ${next}.`,
      );
    }
  }

  private async resolveCurrentMinistryId(userId: number) {
    const context = await this.repository.findUserAccessContext(userId);
    const roles =
      context?.roles.map((item) => item.role.name.toLowerCase()) ?? [];

    if (!roles.includes(MINISTRY_ROLE_NAME)) {
      throw new ForbiddenException(
        'Your account does not have the Ministry role.',
      );
    }

    return this.getSingleMinistryId(
      context?.stakeholders.map((item) => item.stakeholderId) ?? [],
    );
  }

  private async resolveCurrentPswgIds(userId: number) {
    const context = await this.repository.findPswgUserAccessContext(userId);
    const roles =
      context?.roles.map((item) => item.role.name.toLowerCase()) ?? [];

    if (!roles.includes(PRIVATE_SECTOR_ROLE_NAME)) {
      throw new ForbiddenException(
        'Your account does not have the Private Sector role.',
      );
    }

    const pswgIds = Array.from(
      new Set(context?.stakeholders.map((item) => item.stakeholderId) ?? []),
    );

    if (pswgIds.length === 0) {
      throw new ForbiddenException(
        'Your account is not assigned to an active PSWG stakeholder.',
      );
    }

    return pswgIds;
  }

  private async ensureCdcGpsfUser(userId: number) {
    const context = await this.repository.findUserAccessContext(userId);
    const roles =
      context?.roles.map((item) => item.role.name.toLowerCase()) ?? [];

    if (!roles.includes(CDC_GPSF_ROLE_NAME)) {
      throw new ForbiddenException(
        'Only CDC-GPSF users can access Ministry review operations.',
      );
    }
  }

  private getSingleMinistryId(ministryIds: number[]) {
    const uniqueIds = Array.from(new Set(ministryIds));

    if (uniqueIds.length === 0) {
      throw new ForbiddenException(
        'Your account is not assigned to an active Ministry stakeholder.',
      );
    }

    if (uniqueIds.length > 1) {
      throw new ConflictException(
        'Your account is assigned to more than one active Ministry stakeholder.',
      );
    }

    return uniqueIds[0];
  }

  private async ensureActiveReport(progressReportId: number) {
    const report = await this.repository.findActiveReport(progressReportId);

    if (!report) {
      throw new NotFoundException(
        `Progress report with ID ${progressReportId} was not found`,
      );
    }

    return report;
  }

  private async ensureActiveAssignment(
    progressReportId: number,
    ministryId: number,
  ) {
    const report = await this.ensureActiveReport(progressReportId);

    if (report.status !== ProgressReportStatus.SENT) {
      throw new NotFoundException(
        `Progress report with ID ${progressReportId} was not sent to Ministries`,
      );
    }

    const assignment = await this.repository.findActiveAssignment(
      progressReportId,
      ministryId,
    );

    if (!assignment) {
      throw new NotFoundException(
        `Ministry assignment was not found for progress report ${progressReportId}`,
      );
    }

    return assignment;
  }

  private async serialize(item: ProgressReportMinistryWithRelations) {
    const latestMeeting = item.progressReport.meetings[0];
    const chronologicalMeetings = [...item.progressReport.meetings].sort(
      (first, second) =>
        first.meetingDate.getTime() - second.meetingDate.getTime() ||
        first.startTime.getTime() - second.startTime.getTime() ||
        first.createdAt.getTime() - second.createdAt.getTime(),
    );
    const [
      attachment,
      requestDocument,
      draftSemesterReport,
      finalSemesterReport,
      ...meetingDocuments
    ] = await Promise.all([
      this.uploads.getMetadata(item.attachment),
      this.uploads.getMetadata(item.progressReport.attachment),
      this.uploads.getMetadata(item.progressReport.draftSemesterReport),
      this.uploads.getMetadata(item.progressReport.finalSemesterReport),
      ...chronologicalMeetings.map((meeting) =>
        this.uploads.getMetadata(meeting.documentReference),
      ),
    ]);
    const meetings = chronologicalMeetings.map((meeting, index) => ({
      ...meeting,
      meetingDate: meeting.meetingDate.toISOString().slice(0, 10),
      startTime: meeting.startTime.toISOString().slice(11, 19),
      endTime: meeting.endTime.toISOString().slice(11, 19),
      documentReference: meetingDocuments[index] ?? null,
    }));
    const serializedLatestMeeting = latestMeeting
      ? (meetings.find((meeting) => meeting.id === latestMeeting.id) ?? null)
      : null;

    return {
      id: item.id,
      progressReportId: item.progressReportId,
      ministryId: item.ministryId,
      issues: item.issues,
      attachment,
      status: item.status,
      userId: item.userId,
      ministry: item.ministry,
      preparedBy: item.user,
      reviewedBy: item.reviewedBy,
      submittedAt: item.submittedAt,
      progressReport: {
        id: item.progressReport.id,
        title: item.progressReport.title,
        description: item.progressReport.description,
        year: item.progressReport.year,
        semester: item.progressReport.semester,
        status: item.progressReport.status,
        requestDocument,
        draftSemesterReport,
        finalSemesterReport,
        deadlines: item.progressReport.deadlines.map((deadline) => ({
          ...deadline,
          deadline: deadline.deadline.toISOString().slice(0, 10),
        })),
        meetings,
        latestMeeting: serializedLatestMeeting,
      },
      createdAt: item.createdAt,
      updatedAt: item.updatedAt,
      deletedAt: item.deletedAt,
    };
  }

  private async serializeListItem(item: ProgressReportMinistryListItem) {
    return {
      id: item.id,
      ministryId: item.ministryId,
      ministry: item.ministry,
      attachment: await this.uploads.getMetadata(item.attachment),
    };
  }

  private async serializeDetail(
    item: ProgressReportMinistryWithRelations,
    currentUser?: {
      id: number;
      name: string | null;
      position: string | null;
    } | null,
  ) {
    const [assignment, openIssues, rgcDecisions] = await Promise.all([
      this.serialize(item),
      this.repository.findOpenPrimaryIssues(
        item.progressReportId,
        item.ministryId,
      ),
      this.repository.findMinistryRgcDecisions(item.ministryId),
    ]);

    const hasBeenSubmitted =
      item.submittedAt !== null ||
      item.status === ProgressReportMinistryStatus.SUBMITTED ||
      item.status === ProgressReportMinistryStatus.CDC_UNDER_REVIEW ||
      item.status === ProgressReportMinistryStatus.COMPLETED;

    const preparedBy = item.user ?? currentUser ?? null;

    return {
      id: item.id,
      progressReportId: item.progressReportId,
      status: item.status,
      progressReport: {
        id: assignment.progressReport.id,
        title: assignment.progressReport.title,
        year: assignment.progressReport.year,
        semester: assignment.progressReport.semester,
      },
      ministry: {
        id: item.ministry.id,
        name: item.ministry.name,
        description: item.ministry.description,
        logo: item.ministry.logo,
      },
      ministryInformation: {
        submittedAt: item.submittedAt,
        // Once the report is submitted we keep showing whoever submitted it.
        // Before that we fall back to the user who is viewing the report.
        preparedBy: preparedBy
          ? {
              id: preparedBy.id,
              name: preparedBy.name,
              position: preparedBy.position,
            }
          : null,
        approvalProgressReport: assignment.attachment,
      },
      cdcInformation: {
        status: hasBeenSubmitted ? item.status : null,
        reviewedBy: item.reviewedBy
          ? {
              id: item.reviewedBy.id,
              name: item.reviewedBy.name,
              position: item.reviewedBy.position,
            }
          : null,
        latestUpdatedAt: hasBeenSubmitted ? item.updatedAt : null,
        requestDocument: assignment.progressReport.requestDocument,
        meeting: assignment.progressReport.latestMeeting
          ? {
              meetingDate: assignment.progressReport.latestMeeting.meetingDate,
              startTime: assignment.progressReport.latestMeeting.startTime,
              endTime: assignment.progressReport.latestMeeting.endTime,
              location: assignment.progressReport.latestMeeting.location,
            }
          : null,
      },
      description: assignment.progressReport.description,
      issues: openIssues.length,
      openIssues: await Promise.all(
        openIssues.map((issue) => this.serializeMinistryOpenIssue(issue)),
      ),
      rgcDecisions: await this.serializeMinistryRgcDecisions(
        item.progressReportId,
        item.ministryId,
        rgcDecisions,
      ),
    };
  }

  private async serializeCdcDetail(item: ProgressReportMinistryWithRelations) {
    const [assignment, openIssues, rgcDecisions] = await Promise.all([
      this.serialize(item),
      this.repository.findOpenPrimaryIssues(
        item.progressReportId,
        item.ministryId,
      ),
      this.repository.findMinistryRgcDecisions(item.ministryId),
    ]);

    return {
      ...assignment,
      issues: openIssues.length,
      openIssues: await Promise.all(
        openIssues.map((issue) => this.serializeOpenIssue(issue, true)),
      ),
      rgcDecisions: await this.serializeRgcDecisions(
        item.progressReportId,
        item.ministryId,
        rgcDecisions,
        true,
      ),
    };
  }

  private async serializePswgDetail(
    item: ProgressReportMinistryWithRelations,
    pswgIds: number[],
  ) {
    const [assignment, openIssues, rgcDecisions] = await Promise.all([
      this.serialize(item),
      this.repository.findOpenPrimaryIssues(
        item.progressReportId,
        item.ministryId,
        pswgIds,
      ),
      this.repository.findPswgRgcDecisions(item.ministryId, pswgIds),
    ]);

    return {
      ...assignment,
      issues: openIssues.length,
      openIssues: await Promise.all(
        openIssues.map((issue) => this.serializeOpenIssue(issue)),
      ),
      rgcDecisions: await this.serializeRgcDecisions(
        item.progressReportId,
        item.ministryId,
        rgcDecisions,
      ),
    };
  }

  private async serializeRgcDecisions(
    progressReportId: number,
    ministryId: number,
    decisions: PswgRgcDecision[],
    includeCdcComment = false,
  ) {
    const updateRelations =
      await this.repository.findProgressReportRgcDecisionUpdates(
        progressReportId,
        ministryId,
        decisions.map((decision) => decision.id),
      );
    const updateByDecisionId = new Map(
      updateRelations.map((relation) => [
        relation.plenaryDecisionId,
        relation.progressReportUpdate,
      ]),
    );

    return Promise.all(
      decisions.map((decision) =>
        this.serializeRgcDecision(
          decision,
          updateByDecisionId.get(decision.id) ?? null,
          includeCdcComment,
        ),
      ),
    );
  }

  private async serializeRgcDecision(
    decision: PswgRgcDecision,
    progressUpdate: ProgressReportUpdateWithRelations | null,
    includeCdcComment: boolean,
  ) {
    const cdcComment = this.getProgressUpdateRgcDecisionComment(
      progressUpdate,
      decision.id,
    );

    return {
      id: decision.id,
      meetingDate: decision.meetingDate.toISOString().slice(0, 10),
      status: decision.status,
      focalPerson: decision.focalPerson,
      decision: decision.decision,
      category: decision.category,
      verificationSource: decision.verificationSource,
      verificationLink: decision.verificationLink,
      submittedToCdcAt: decision.submittedToCdcAt,
      issues: decision.decisionIssues.map(({ issue }) => ({
        id: issue.id,
        title: issue.title,
        workingGroup: issue.stakeholder,
      })),
      hasProgressUpdate: Boolean(progressUpdate),
      progressUpdate: progressUpdate
        ? await this.serializeRgcProgressUpdate(progressUpdate)
        : null,
      ...(includeCdcComment
        ? {
            cdcComment: cdcComment
              ? this.serializeRgcDecisionComment(cdcComment)
              : null,
          }
        : {}),
      createdAt: decision.createdAt,
      updatedAt: decision.updatedAt,
    };
  }

  // The Ministry's own report view renders a flat decision row: the linked
  // progress-update fields are lifted onto the decision (null when there is no
  // update yet), category is a plain name, and PSWG/issue and audit timestamps
  // are dropped. CDC and PSWG keep the richer nested shape above.
  private async serializeMinistryRgcDecisions(
    progressReportId: number,
    ministryId: number,
    decisions: PswgRgcDecision[],
  ) {
    const updateRelations =
      await this.repository.findProgressReportRgcDecisionUpdates(
        progressReportId,
        ministryId,
        decisions.map((decision) => decision.id),
      );
    const updateByDecisionId = new Map(
      updateRelations.map((relation) => [
        relation.plenaryDecisionId,
        relation.progressReportUpdate,
      ]),
    );

    return Promise.all(
      decisions.map((decision) =>
        this.serializeMinistryRgcDecision(
          decision,
          updateByDecisionId.get(decision.id) ?? null,
        ),
      ),
    );
  }

  private async serializeMinistryRgcDecision(
    decision: PswgRgcDecision,
    progressUpdate: ProgressReportUpdateWithRelations | null,
  ) {
    const cdcComment = this.getProgressUpdateRgcDecisionComment(
      progressUpdate,
      decision.id,
    );

    return {
      id: decision.id,
      meetingDate: decision.meetingDate.toISOString().slice(0, 10),
      status: decision.status,
      focalPerson: decision.focalPerson,
      decision: decision.decision,
      category: decision.category.name,
      verificationSource: decision.verificationSource,
      verificationLink: decision.verificationLink,
      submittedToCdcAt: decision.submittedToCdcAt,
      hasProgressUpdate: Boolean(progressUpdate),
      indicators: progressUpdate?.indicators ?? null,
      progressSolution: progressUpdate?.progressSolution ?? null,
      implementationChallenges:
        progressUpdate?.implementationChallenges ?? null,
      requests: progressUpdate?.requests ?? null,
      nextStep: progressUpdate?.next_step ?? null,
      dateOfIssueSolution: progressUpdate?.dateOfIssueSolution
        ? progressUpdate.dateOfIssueSolution.toISOString().slice(0, 10)
        : null,
      attachment: await this.uploads.getMetadata(
        progressUpdate?.attachement ?? null,
      ),
      cdcComment: cdcComment
        ? this.serializeRgcDecisionComment(cdcComment)
        : null,
    };
  }

  private async serializeProgressUpdate(
    update: ProgressReportUpdateWithRelations,
    includeIssueStatus = true,
  ) {
    return {
      id: update.id,
      progressReportId: update.progressReportId,
      ministryId: update.ministryId,
      issueId: update.issueId,
      issueResolveId: update.issueResolveId,
      ...(includeIssueStatus && { issueStatus: update.issueStatus }),
      indicators: update.indicators,
      progressSolution: update.progressSolution,
      implementationChallenges: update.implementationChallenges,
      requests: update.requests,
      sourceOfVerification: update.sourceOfVerification,
      linkToVerificationSource: update.linkToVerificationSource,
      rgcDecision: update.issueResolve?.rgcDecision ?? null,
      nextStep: update.issueResolve?.nextStep ?? update.next_step,
      dateOfIssueSolution: update.dateOfIssueSolution
        ? update.dateOfIssueSolution.toISOString().slice(0, 10)
        : null,
      attachment: await this.uploads.getMetadata(update.attachement),
      relatedIssues: update.progressReportUpdateIssues,
      relatedPlenaryDecisions: this.serializeRelatedPlenaryDecisions(update),
      updatedBy: update.user,
      createdAt: update.createdAt,
      updatedAt: update.updatedAt,
    };
  }

  private async serializeRgcProgressUpdate(
    update: ProgressReportUpdateWithRelations,
  ) {
    // A decision progress update mirrors the Issue one but drops the Issue-only
    // fields: issueStatus, plus sourceOfVerification / linkToVerificationSource
    // (those live on the master PlenaryRgcDecision) and rgcDecision (which comes
    // from an issueResolve a decision never has).
    return {
      id: update.id,
      progressReportId: update.progressReportId,
      ministryId: update.ministryId,
      issueId: update.issueId,
      issueResolveId: update.issueResolveId,
      indicators: update.indicators,
      progressSolution: update.progressSolution,
      implementationChallenges: update.implementationChallenges,
      requests: update.requests,
      nextStep: update.issueResolve?.nextStep ?? update.next_step,
      dateOfIssueSolution: update.dateOfIssueSolution
        ? update.dateOfIssueSolution.toISOString().slice(0, 10)
        : null,
      attachment: await this.uploads.getMetadata(update.attachement),
      relatedIssues: update.progressReportUpdateIssues,
      relatedPlenaryDecisions: this.serializeRelatedPlenaryDecisions(update),
      updatedBy: update.user,
      createdAt: update.createdAt,
      updatedAt: update.updatedAt,
    };
  }

  private async serializeOpenIssue(
    issue: MinistryOpenIssue,
    includeCdcComment = false,
  ) {
    const progressUpdate = issue.progressReportUpdates[0] ?? null;
    const cdcComment = this.getProgressUpdateIssueComment(progressUpdate);

    return {
      id: issue.id,
      title: issue.title,
      description: issue.description,
      recommendation: issue.recommendation,
      status: issue.issueStatus,
      category: issue.category,
      workingGroup: issue.stakeholder,
      governmentAgencies: issue.governmentAgencies,
      createdAt: issue.createdAt,
      updatedAt: issue.updatedAt,
      progressUpdate: progressUpdate
        ? await this.serializeProgressUpdate(progressUpdate)
        : null,
      ...(includeCdcComment
        ? {
            cdcComment: cdcComment
              ? this.serializeIssueComment(cdcComment)
              : null,
          }
        : {}),
    };
  }

  // The Ministry's own report view renders a flat issue row: the progress-update
  // fields are lifted onto the issue (null when there is no update yet), status /
  // category / working group collapse to their id/name, government agencies keep
  // only { agencyOrder, name, logo }, and the audit metadata is dropped. CDC and
  // PSWG keep the richer nested shape above (serializeOpenIssue).
  private async serializeMinistryOpenIssue(issue: MinistryOpenIssue) {
    const progressUpdate = issue.progressReportUpdates[0] ?? null;
    const cdcComment = this.getProgressUpdateIssueComment(progressUpdate);
    // Issue status lives on the master Issues table (updated by both the
    // Ministry save and the CDC status change), so read it directly.
    const status = issue.issueStatus;

    return {
      id: issue.id,
      title: issue.title,
      description: issue.description,
      recommendation: issue.recommendation,
      hasProgressUpdate: Boolean(progressUpdate),
      issueStatusId: status.id,
      status: status.code,
      category: issue.category.name,
      workingGroup: issue.stakeholder.name,
      governmentAgencies: issue.governmentAgencies.map((agency) => ({
        agencyOrder: agency.agencyOrder,
        name: agency.stakeholder.name,
        logo: agency.stakeholder.logo,
      })),
      indicators: progressUpdate?.indicators ?? null,
      progressSolution: progressUpdate?.progressSolution ?? null,
      implementationChallenges:
        progressUpdate?.implementationChallenges ?? null,
      requests: progressUpdate?.requests ?? null,
      sourceOfVerification: progressUpdate?.sourceOfVerification ?? null,
      linkToVerificationSource:
        progressUpdate?.linkToVerificationSource ?? null,
      rgcDecision: progressUpdate?.issueResolve?.rgcDecision ?? null,
      nextStep: progressUpdate
        ? (progressUpdate.issueResolve?.nextStep ?? progressUpdate.next_step)
        : null,
      dateOfIssueSolution: progressUpdate?.dateOfIssueSolution
        ? progressUpdate.dateOfIssueSolution.toISOString().slice(0, 10)
        : null,
      attachment: await this.uploads.getMetadata(
        progressUpdate?.attachement ?? null,
      ),
      cdcComment: cdcComment ? this.serializeIssueComment(cdcComment) : null,
    };
  }

  private async getIssueCommentContext(
    progressReportId: number,
    ministryId: number,
    issueId: number,
    userId: number,
  ) {
    await this.ensureCdcGpsfUser(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );

    if (!CDC_ISSUE_UPDATE_STATUSES.includes(assignment.status)) {
      throw new ConflictException(
        `Issue comments cannot be changed while the report status is ${assignment.status}.`,
      );
    }

    const [issue, updateIssue] = await Promise.all([
      this.repository.findOpenPrimaryIssue(issueId, ministryId),
      this.repository.findProgressReportUpdateIssue(
        progressReportId,
        ministryId,
        issueId,
      ),
    ]);

    if (!issue) {
      throw new NotFoundException(
        `Open primary-agency Issue with ID ${issueId} was not found for this Ministry.`,
      );
    }

    if (!updateIssue) {
      throw new ConflictException(
        'The Ministry must save this Issue progress update before CDC can add a comment.',
      );
    }

    return { assignment, updateIssue };
  }

  private async getRgcDecisionCommentContext(
    progressReportId: number,
    ministryId: number,
    plenaryDecisionId: number,
    userId: number,
  ) {
    await this.ensureCdcGpsfUser(userId);
    const assignment = await this.ensureActiveAssignment(
      progressReportId,
      ministryId,
    );

    if (!CDC_ISSUE_UPDATE_STATUSES.includes(assignment.status)) {
      throw new ConflictException(
        `RGC Decision comments cannot be changed while the report status is ${assignment.status}.`,
      );
    }

    const [decision, updateDecision] = await Promise.all([
      this.repository.findOpenMinistryRgcDecision(
        plenaryDecisionId,
        ministryId,
      ),
      this.repository.findProgressReportRgcDecisionUpdate(
        progressReportId,
        ministryId,
        plenaryDecisionId,
      ),
    ]);

    if (!decision) {
      throw new NotFoundException(
        `Open RGC Decision with ID ${plenaryDecisionId} was not found for this Ministry.`,
      );
    }

    if (!updateDecision) {
      throw new ConflictException(
        'The Ministry must save this RGC Decision progress update before CDC can add a comment.',
      );
    }

    return { assignment, updateDecision };
  }

  private getProgressUpdateIssueComment(
    update: ProgressReportUpdateWithRelations | null,
  ): ProgressReportIssueCommentWithRelations | null {
    if (!update) return null;

    return (
      update.progressReportUpdateIssues
        .flatMap((relation) => relation.progressReportIssueComments)
        .at(0) ?? null
    );
  }

  private serializeIssueComment(
    comment: ProgressReportIssueCommentWithRelations,
  ) {
    return {
      id: comment.id,
      commentTypeId: comment.commentTypeId,
      commentType: comment.commentType,
      comment:
        typeof comment.comment === 'string'
          ? comment.comment
          : JSON.stringify(comment.comment),
      author: comment.user,
      createdAt: comment.createdAt,
      updatedAt: comment.updatedAt,
    };
  }

  private getProgressUpdateRgcDecisionComment(
    update: ProgressReportUpdateWithRelations | null,
    plenaryDecisionId: number,
  ): ProgressReportIssueCommentWithRelations | null {
    if (!update) return null;

    const updateDecision = update.progressReportUpdatePlenaryDecisions.find(
      (relation) => relation.plenaryDecisionId === plenaryDecisionId,
    );

    return updateDecision?.progressReportIssueComments.at(0) ?? null;
  }

  private serializeRelatedPlenaryDecisions(
    update: ProgressReportUpdateWithRelations,
  ) {
    return (update.progressReportUpdatePlenaryDecisions ?? []).map(
      (relation) => ({
        id: relation.id,
        progressReportId: relation.progressReportId,
        ministryId: relation.ministryId,
        plenaryDecisionId: relation.plenaryDecisionId,
      }),
    );
  }

  private serializeRgcDecisionComment(
    comment: ProgressReportIssueCommentWithRelations,
  ) {
    return {
      id: comment.id,
      commentTypeId: comment.commentTypeId,
      commentType: comment.commentType,
      comment:
        typeof comment.comment === 'string'
          ? comment.comment
          : JSON.stringify(comment.comment),
      author: comment.user,
      createdAt: comment.createdAt,
      updatedAt: comment.updatedAt,
    };
  }

  private toNullableJson(
    value: string | undefined,
  ): Prisma.InputJsonValue | Prisma.NullableJsonNullValueInput {
    return value ?? Prisma.DbNull;
  }

  private async notifyStatusChange(
    item: ProgressReportMinistryWithRelations,
    senderUserId: number,
  ) {
    try {
      if (item.status === ProgressReportMinistryStatus.SHARED_WITH_PSWG) {
        if (!item.ministry.relatedStakeholderId) return;

        await this.notifications.create({
          title: item.ministry.name,
          message: `Progress report shared: ${item.progressReport.title}`,
          type: 'PROGRESS_REPORT_SHARED',
          senderUserId,
          receiverStakeholderId: item.ministry.relatedStakeholderId,
          data: {
            progressReportId: item.progressReportId,
            ministryId: item.ministryId,
            url: `/pswg/progress-report/${item.id}`,
          },
        });
        return;
      }

      if (item.status === ProgressReportMinistryStatus.SUBMITTED) {
        const receiverUserIds =
          await this.repository.findActiveUserIdsByRoleName(CDC_GPSF_ROLE_NAME);

        await Promise.all(
          receiverUserIds.map((receiverUserId) =>
            this.notifications.create({
              title: item.ministry.name,
              message: `Ministry progress report submitted: ${item.progressReport.title}`,
              type: 'PROGRESS_REPORT_SUBMITTED',
              senderUserId,
              receiverUserId,
              data: {
                progressReportId: item.progressReportId,
                ministryId: item.ministryId,
                url: `/cdc-gpsf/progress-reports/${item.progressReportId}/ministries/${item.ministryId}`,
              },
            }),
          ),
        );
        return;
      }

      if (item.status === ProgressReportMinistryStatus.PSWG_REVIEWED) {
        await this.notifications.create({
          title: item.ministry.name,
          message: `PSWG reviewed: ${item.progressReport.title}`,
          type: 'PROGRESS_REPORT_PSWG_REVIEWED',
          senderUserId,
          receiverStakeholderId: item.ministryId,
          data: {
            progressReportId: item.progressReportId,
            ministryId: item.ministryId,
            url: `/ministry/progress-reports/${item.progressReportId}`,
          },
        });
        return;
      }

      if (item.status === ProgressReportMinistryStatus.COMPLETED) {
        await this.notifications.create({
          title: 'CDC-GPSF',
          message: `Progress report reviewed: ${item.progressReport.title}`,
          type: 'PROGRESS_REPORT_COMPLETED',
          senderUserId,
          receiverStakeholderId: item.ministryId,
          data: {
            progressReportId: item.progressReportId,
            ministryId: item.ministryId,
            url: `/ministry/progress-reports/${item.progressReportId}`,
          },
        });
      }
    } catch (error) {
      this.logger.error(
        `Failed to notify status ${item.status} for progress report ${item.progressReportId}`,
        error instanceof Error ? error.stack : String(error),
      );
    }
  }
}
