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

import { Prisma } from '@/generated/prisma/client';
import { PrismaService } from '@/prisma/prisma.service';

import { SaveProgressReportEvaluationsDto } from './dto/save-progress-report-evaluations.dto';

@Injectable()
export class ProgressReportEvaluationsService {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * =====================================================
   * PSWG - GET EVALUATION FORM
   * =====================================================
   *
   * FIX:
   * 1. Get issues from this exact Progress Report assignment.
   * 2. Do not use ministry-only issues when linked Progress
   *    Report Update Issues exist.
   * 3. Return the latest saved evaluation for each issue.
   *
   * Using latest-per-issue prevents the browser from showing
   * a different old user's values when Postman/browser tokens
   * are not the same.
   */
  async getEvaluationForm(assignmentId: number, userId: number) {
    const assignment = await this.ensureAssignmentExists(assignmentId);

    await this.ensureUserExists(userId);

    const issues = await this.getProgressReportIssues(
      assignment.progressReportId,
      assignment.ministryId,
    );

    const issueIds = issues.map((issue) => issue.id);

    /**
     * Fetch all active evaluations for these exact issues,
     * newest first.
     *
     * We then keep only the first row for each issueId.
     */
    const savedEvaluations =
      issueIds.length > 0
        ? await this.prisma.progressReportIssueEvaluation.findMany({
            where: {
              progressReportMinistryId: assignmentId,

              deletedAt: null,

              issueId: {
                in: issueIds,
              },
            },

            select: {
              id: true,

              issueId: true,

              isResolved: true,

              rating: true,

              feedback: true,

              userId: true,

              createdAt: true,

              updatedAt: true,
            },

            orderBy: [
              {
                updatedAt: 'desc',
              },
              {
                id: 'desc',
              },
            ],
          })
        : [];

    /**
     * First record for an issue wins because findMany is
     * ordered newest -> oldest.
     */
    const evaluationMap = new Map<number, (typeof savedEvaluations)[number]>();

    for (const evaluation of savedEvaluations) {
      if (!evaluationMap.has(evaluation.issueId)) {
        evaluationMap.set(evaluation.issueId, evaluation);
      }
    }

    return {
      assignment: {
        id: assignment.id,

        progressReportId: assignment.progressReportId,

        ministryId: assignment.ministryId,

        status: assignment.status,

        ministry: assignment.ministry,

        progressReport: assignment.progressReport,
      },

      currentUserId: userId,

      totalIssues: issues.length,

      items: issues.map((issue) => ({
        issueId: issue.id,

        title: issue.title,

        description: issue.description,

        recommendation: issue.recommendation,

        status: issue.issueStatus,

        category: issue.category,

        workingGroup: issue.stakeholder,

        governmentAgencies: issue.governmentAgencies,

        evaluation: evaluationMap.get(issue.id) ?? null,
      })),
    };
  }

  /**
   * =====================================================
   * ADMIN - GET SUBMITTED EVALUATION
   * =====================================================
   */
  async getSubmittedEvaluationForm(assignmentId: number) {
    const assignment = await this.ensureAssignmentExists(assignmentId);

    const issues = await this.getProgressReportIssues(
      assignment.progressReportId,
      assignment.ministryId,
    );

    const issueIds = issues.map((issue) => issue.id);

    if (issueIds.length === 0) {
      return {
        assignment: {
          id: assignment.id,

          progressReportId: assignment.progressReportId,

          ministryId: assignment.ministryId,

          status: assignment.status,

          ministry: assignment.ministry,

          progressReport: assignment.progressReport,
        },

        submittedByUserId: null,

        totalIssues: 0,

        items: [],
      };
    }

    /**
     * Latest PSWG user who submitted/updated an evaluation.
     */
    const latestEvaluation =
      await this.prisma.progressReportIssueEvaluation.findFirst({
        where: {
          progressReportMinistryId: assignmentId,

          deletedAt: null,

          issueId: {
            in: issueIds,
          },
        },

        select: {
          id: true,

          userId: true,

          updatedAt: true,
        },

        orderBy: [
          {
            updatedAt: 'desc',
          },
          {
            id: 'desc',
          },
        ],
      });

    const submittedByUserId = latestEvaluation?.userId ?? null;

    const submittedEvaluations = submittedByUserId
      ? await this.prisma.progressReportIssueEvaluation.findMany({
          where: {
            progressReportMinistryId: assignmentId,

            userId: submittedByUserId,

            deletedAt: null,

            issueId: {
              in: issueIds,
            },
          },

          select: {
            id: true,

            issueId: true,

            isResolved: true,

            rating: true,

            feedback: true,

            userId: true,

            createdAt: true,

            updatedAt: true,
          },

          orderBy: {
            id: 'asc',
          },
        })
      : [];

    const evaluationMap = new Map(
      submittedEvaluations.map((evaluation) => [
        evaluation.issueId,
        evaluation,
      ]),
    );

    return {
      assignment: {
        id: assignment.id,

        progressReportId: assignment.progressReportId,

        ministryId: assignment.ministryId,

        status: assignment.status,

        ministry: assignment.ministry,

        progressReport: assignment.progressReport,
      },

      submittedByUserId,

      totalIssues: issues.length,

      items: issues.map((issue) => ({
        issueId: issue.id,

        title: issue.title,

        description: issue.description,

        recommendation: issue.recommendation,

        status: issue.issueStatus,

        category: issue.category,

        workingGroup: issue.stakeholder,

        governmentAgencies: issue.governmentAgencies,

        evaluation: evaluationMap.get(issue.id) ?? null,
      })),
    };
  }

  /**
   * =====================================================
   * ADMIN - UPDATE SUBMITTED EVALUATION
   * =====================================================
   */
  async updateSubmittedEvaluationForm(
    assignmentId: number,
    dto: SaveProgressReportEvaluationsDto,
  ) {
    const assignment = await this.ensureAssignmentExists(assignmentId);

    const allowedIssueIds = await this.getAllowedIssueIds(
      assignment.progressReportId,
      assignment.ministryId,
    );

    this.validateRequestedIssueIds(dto, allowedIssueIds);

    const latestEvaluation =
      await this.prisma.progressReportIssueEvaluation.findFirst({
        where: {
          progressReportMinistryId: assignmentId,

          deletedAt: null,

          issueId: {
            in: allowedIssueIds,
          },
        },

        select: {
          id: true,

          userId: true,
        },

        orderBy: [
          {
            updatedAt: 'desc',
          },
          {
            id: 'desc',
          },
        ],
      });

    if (!latestEvaluation) {
      throw new NotFoundException(
        'Submitted Progress Report evaluation not found.',
      );
    }

    const submittedByUserId = latestEvaluation.userId;

    await this.prisma.$transaction(async (tx) => {
      for (const evaluation of dto.evaluations) {
        const feedback = this.toPrismaJson(evaluation.feedback);

        await tx.progressReportIssueEvaluation.upsert({
          where: {
            progressReportMinistryId_issueId_userId: {
              progressReportMinistryId: assignmentId,

              issueId: evaluation.issueId,

              userId: submittedByUserId,
            },
          },

          create: {
            progressReportMinistryId: assignmentId,

            issueId: evaluation.issueId,

            userId: submittedByUserId,

            isResolved: evaluation.isResolved,

            rating: evaluation.rating,

            feedback,
          },

          update: {
            isResolved: evaluation.isResolved,

            rating: evaluation.rating,

            feedback,

            deletedAt: null,
          },
        });
      }
    });

    return this.getSubmittedEvaluationForm(assignmentId);
  }

  /**
   * =====================================================
   * CURRENT USER - GET OWN SAVED EVALUATIONS
   * =====================================================
   */
  async findAll(assignmentId: number, userId: number) {
    const assignment = await this.ensureAssignmentExists(assignmentId);

    await this.ensureUserExists(userId);

    const allowedIssueIds = await this.getAllowedIssueIds(
      assignment.progressReportId,
      assignment.ministryId,
    );

    const items =
      allowedIssueIds.length > 0
        ? await this.prisma.progressReportIssueEvaluation.findMany({
            where: {
              progressReportMinistryId: assignmentId,

              userId,

              deletedAt: null,

              issueId: {
                in: allowedIssueIds,
              },
            },

            select: {
              id: true,

              progressReportMinistryId: true,

              issueId: true,

              isResolved: true,

              rating: true,

              feedback: true,

              userId: true,

              issue: {
                select: {
                  id: true,

                  title: true,

                  description: true,

                  recommendation: true,

                  issueStatus: {
                    select: {
                      id: true,

                      code: true,

                      name: true,
                    },
                  },

                  category: {
                    select: {
                      id: true,

                      name: true,
                    },
                  },
                },
              },

              createdAt: true,

              updatedAt: true,
            },

            orderBy: {
              id: 'asc',
            },
          })
        : [];

    return {
      assignmentId,

      progressReportId: assignment.progressReportId,

      ministryId: assignment.ministryId,

      total: items.length,

      items,
    };
  }

  /**
   * =====================================================
   * PSWG - SAVE EVALUATIONS
   * =====================================================
   */
  async save(
    assignmentId: number,
    userId: number,
    dto: SaveProgressReportEvaluationsDto,
  ) {
    const assignment = await this.ensureAssignmentExists(assignmentId);

    await this.ensureUserExists(userId);

    const allowedIssueIds = await this.getAllowedIssueIds(
      assignment.progressReportId,
      assignment.ministryId,
    );

    this.validateRequestedIssueIds(dto, allowedIssueIds);

    await this.prisma.$transaction(async (tx) => {
      for (const evaluation of dto.evaluations) {
        const feedback = this.toPrismaJson(evaluation.feedback);

        await tx.progressReportIssueEvaluation.upsert({
          where: {
            progressReportMinistryId_issueId_userId: {
              progressReportMinistryId: assignmentId,

              issueId: evaluation.issueId,

              userId,
            },
          },

          create: {
            progressReportMinistryId: assignmentId,

            issueId: evaluation.issueId,

            isResolved: evaluation.isResolved,

            rating: evaluation.rating,

            feedback,

            userId,
          },

          update: {
            isResolved: evaluation.isResolved,

            rating: evaluation.rating,

            feedback,

            deletedAt: null,
          },
        });
      }
    });

    const data = await this.findAll(assignmentId, userId);

    return {
      success: true,

      message: 'Progress Report evaluation saved successfully.',

      ...data,
    };
  }

  /**
   * =====================================================
   * DELETE OWN EVALUATION
   * =====================================================
   */
  async remove(assignmentId: number, evaluationId: number, userId: number) {
    await this.ensureAssignmentExists(assignmentId);

    await this.ensureUserExists(userId);

    const evaluation =
      await this.prisma.progressReportIssueEvaluation.findFirst({
        where: {
          id: evaluationId,

          progressReportMinistryId: assignmentId,

          userId,

          deletedAt: null,
        },

        select: {
          id: true,
        },
      });

    if (!evaluation) {
      throw new NotFoundException('Progress Report evaluation not found.');
    }

    await this.prisma.progressReportIssueEvaluation.update({
      where: {
        id: evaluation.id,
      },

      data: {
        deletedAt: new Date(),
      },
    });

    return {
      success: true,

      message: 'Progress Report evaluation deleted successfully.',
    };
  }

  /**
   * =====================================================
   * EXACT PROGRESS REPORT ISSUES
   * =====================================================
   *
   * First try ProgressReportUpdateIssue because that table
   * links the issue to a specific progressReportId + ministryId.
   *
   * If there are no linked rows (old data), fall back to the
   * old ministry/status rule so existing records still work.
   */
  private async getProgressReportIssues(
    progressReportId: number,
    ministryId: number,
  ) {
    const linkedRows = await this.prisma.progressReportUpdateIssue.findMany({
      where: {
        ministryId,

        progressReportUpdate: {
          progressReportId,

          ministryId,

          deletedAt: null,
        },
      },

      select: {
        id: true,

        issueId: true,
      },

      orderBy: {
        id: 'asc',
      },
    });

    const linkedIssueIds = [...new Set(linkedRows.map((row) => row.issueId))];

    if (linkedIssueIds.length > 0) {
      const issues = await this.findIssuesByIds(linkedIssueIds);

      /**
       * Preserve ProgressReportUpdateIssue order.
       */
      const issueMap = new Map(issues.map((issue) => [issue.id, issue]));

      return linkedIssueIds
        .map((issueId) => issueMap.get(issueId))
        .filter((issue): issue is NonNullable<typeof issue> => Boolean(issue));
    }

    /**
     * Backward-compatible fallback for older records that
     * do not yet have ProgressReportUpdateIssue rows.
     */
    return this.prisma.issues.findMany({
      where: {
        deletedAt: null,

        governmentAgencies: {
          some: {
            stakeholderId: ministryId,
          },
        },

        OR: [
          {
            issueStatus: {
              code: {
                in: [
                  'IN_PROGRESS',
                  'NOT_ADDRESSED',
                  'in_progress',
                  'not_addressed',
                ],

                mode: 'insensitive',
              },
            },
          },

          {
            issueStatus: {
              name: {
                in: [
                  'In Progress',
                  'Not Addressed',
                  'in progress',
                  'not addressed',
                ],

                mode: 'insensitive',
              },
            },
          },
        ],
      },

      select: this.issueSelect(),

      orderBy: {
        id: 'desc',
      },
    });
  }

  /**
   * Fetch issue objects by exact IDs.
   */
  private async findIssuesByIds(issueIds: number[]) {
    return this.prisma.issues.findMany({
      where: {
        id: {
          in: issueIds,
        },

        deletedAt: null,
      },

      select: this.issueSelect(),
    });
  }

  /**
   * Shared Issue select.
   */
  private issueSelect() {
    return {
      id: true,

      title: true,

      description: true,

      recommendation: true,

      issueStatus: {
        select: {
          id: true,

          code: true,

          name: true,
        },
      },

      category: {
        select: {
          id: true,

          name: true,
        },
      },

      stakeholder: {
        select: {
          id: true,

          name: true,
        },
      },

      governmentAgencies: {
        select: {
          agencyOrder: true,

          stakeholder: {
            select: {
              id: true,

              name: true,
            },
          },
        },

        orderBy: {
          agencyOrder: 'asc' as const,
        },
      },
    } satisfies Prisma.IssuesSelect;
  }

  private async getAllowedIssueIds(
    progressReportId: number,
    ministryId: number,
  ): Promise<number[]> {
    const issues = await this.getProgressReportIssues(
      progressReportId,
      ministryId,
    );

    return issues.map((issue) => issue.id);
  }

  /**
   * Validate duplicate IDs + make sure every submitted issue
   * belongs to this exact Progress Report assignment.
   */
  private validateRequestedIssueIds(
    dto: SaveProgressReportEvaluationsDto,
    allowedIssueIds: number[],
  ) {
    const requestedIssueIds = dto.evaluations.map(
      (evaluation) => evaluation.issueId,
    );

    const uniqueIssueIds = [...new Set(requestedIssueIds)];

    if (uniqueIssueIds.length !== requestedIssueIds.length) {
      throw new BadRequestException('Duplicate issueId found in evaluations.');
    }

    const allowedIssueIdSet = new Set(allowedIssueIds);

    const invalidIssueIds = uniqueIssueIds.filter(
      (issueId) => !allowedIssueIdSet.has(issueId),
    );

    if (invalidIssueIds.length > 0) {
      throw new BadRequestException(
        `The following issue IDs do not belong to this Progress Report Ministry assignment: ${invalidIssueIds.join(
          ', ',
        )}`,
      );
    }
  }

  /**
   * =====================================================
   * ASSIGNMENT
   * =====================================================
   */
  private async ensureAssignmentExists(assignmentId: number) {
    const assignment = await this.prisma.progressReportMinistry.findFirst({
      where: {
        id: assignmentId,

        deletedAt: null,
      },

      select: {
        id: true,

        progressReportId: true,

        ministryId: true,

        status: true,

        ministry: {
          select: {
            id: true,

            name: true,

            description: true,

            logo: true,
          },
        },

        progressReport: {
          select: {
            id: true,

            title: true,

            description: true,

            year: true,

            semester: true,

            status: true,
          },
        },
      },
    });

    if (!assignment) {
      throw new NotFoundException(
        'Progress Report Ministry assignment not found.',
      );
    }

    return assignment;
  }

  /**
   * =====================================================
   * USER
   * =====================================================
   */
  private async ensureUserExists(userId: number) {
    const user = await this.prisma.user.findFirst({
      where: {
        id: userId,

        isActive: true,

        deletedAt: null,
      },

      select: {
        id: true,
      },
    });

    if (!user) {
      throw new BadRequestException(
        'Authenticated user does not exist or is inactive.',
      );
    }

    return user;
  }

  /**
   * =====================================================
   * JSON
   * =====================================================
   */
  private toPrismaJson(
    value: string | Record<string, unknown> | null | undefined,
  ): Prisma.InputJsonValue | Prisma.NullableJsonNullValueInput {
    if (value === undefined || value === null) {
      return Prisma.DbNull;
    }

    return value as Prisma.InputJsonValue;
  }
}
