import { Injectable } from '@nestjs/common';

import {
  Prisma,
  ProgressReport,
  ProgressReportDeadlineSlot,
  ProgressReportDeadlineStatus,
  ProgressReportMeetingSlot,
  ProgressReportMeetingStatus,
  ProgressReportSemester,
  ProgressReportStatus,
} from '@/generated/prisma/client';
import { PrismaService } from '@/prisma/prisma.service';

// FIRST sorts before SECOND because that is the order of the enum values.
const meetingOrderBy = {
  slot: 'asc',
} satisfies Prisma.ProgressReportMeetingOrderByWithRelationInput;

// Slot order matches the enum, so FIRST, then SECOND, then FINAL.
const deadlineOrderBy = {
  slot: 'asc',
} satisfies Prisma.ProgressReportDeadlineOrderByWithRelationInput;

const deadlineFields = {
  id: true,
  slot: true,
  deadline: true,
  status: true,
  progressReportId: true,
} satisfies Prisma.ProgressReportDeadlineSelect;

export type ProgressReportDeadlineRecord =
  Prisma.ProgressReportDeadlineGetPayload<{
    select: typeof deadlineFields;
  }>;

// The list response only needs enough to label and place a meeting. Heavier
// fields (description, location, document) stay in the meeting endpoints.
const listMeetingFields = {
  id: true,
  slot: true,
  title: true,
  meetingDate: true,
  startTime: true,
  endTime: true,
  status: true,
} satisfies Prisma.ProgressReportMeetingSelect;

export type ProgressReportListMeetingRecord =
  Prisma.ProgressReportMeetingGetPayload<{
    select: typeof listMeetingFields;
  }>;

// The fields the frontend actually needs. Leaving out userId, deletedAt,
// createdAt, and updatedAt keeps internal data out of API responses.
const meetingFields = {
  id: true,
  slot: true,
  progressReportId: true,
  title: true,
  description: true,
  meetingDate: true,
  startTime: true,
  endTime: true,
  location: true,
  documentReference: true,
  status: true,
} satisfies Prisma.ProgressReportMeetingSelect;

export type ProgressReportMeetingRecord =
  Prisma.ProgressReportMeetingGetPayload<{
    select: typeof meetingFields;
  }>;

const progressReportRelations = {
  deadlines: {
    where: { deletedAt: null },
    orderBy: deadlineOrderBy,
    select: deadlineFields,
  },
  meetings: {
    where: { deletedAt: null },
    orderBy: meetingOrderBy,
    select: listMeetingFields,
  },
  ministries: {
    where: { deletedAt: null },
    select: {
      id: true,
      ministryId: true,
      issues: true,
      attachment: true,
      status: true,
      updatedAt: true,
    },
  },
} satisfies Prisma.ProgressReportInclude;

export type ProgressReportWithRelations = Prisma.ProgressReportGetPayload<{
  include: typeof progressReportRelations;
}>;

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

  // The repository contains the direct Prisma database calls.
  create(data: Prisma.ProgressReportCreateInput) {
    return this.prisma.progressReport.create({
      data,
      include: progressReportRelations,
    });
  }

  // Create the report and its FIRST deadline together. Prisma runs a nested
  // create in one transaction, so a failed deadline rolls the report back.
  createWithFirstDeadline(
    data: Omit<Prisma.ProgressReportCreateInput, 'deadlines'>,
    firstDeadline: Date,
  ) {
    return this.prisma.progressReport.create({
      data: {
        ...data,
        deadlines: {
          create: {
            slot: ProgressReportDeadlineSlot.FIRST,
            deadline: firstDeadline,
          },
        },
      },
      include: progressReportRelations,
    });
  }

  findManyAndCount(options: {
    where: Prisma.ProgressReportWhereInput;
    skip: number;
    take: number;
  }) {
    // Run both queries in one transaction so the rows and total stay consistent.
    return this.prisma.$transaction([
      this.prisma.progressReport.findMany({
        ...options,
        orderBy: {
          createdAt: 'desc',
        },
        include: progressReportRelations,
      }),
      this.prisma.progressReport.count({
        where: options.where,
      }),
    ]);
  }

  // Only active reports can be read, updated, or deleted.
  findActiveById(id: number) {
    return this.prisma.progressReport.findFirst({
      where: {
        id,
        deletedAt: null,
      },
      include: progressReportRelations,
    });
  }

  // Find another active report that already uses this year and semester.
  findActiveByYearAndSemester(
    year: number,
    semester: ProgressReportSemester,
    excludeId?: number,
  ) {
    return this.prisma.progressReport.findFirst({
      where: {
        year,
        semester,
        deletedAt: null,
        ...(excludeId !== undefined ? { id: { not: excludeId } } : {}),
      },
    });
  }

  update(id: number, data: Prisma.ProgressReportUpdateInput) {
    return this.prisma.progressReport.update({
      where: { id },
      data,
      include: progressReportRelations,
    });
  }

  updateStatus(id: number, status: ProgressReportStatus) {
    return this.update(id, { status });
  }

  createDeadline(data: Prisma.ProgressReportDeadlineCreateInput) {
    return this.prisma.progressReportDeadline.create({
      data,
      select: deadlineFields,
    });
  }

  findActiveDeadlineById(deadlineId: number) {
    return this.prisma.progressReportDeadline.findFirst({
      where: {
        id: deadlineId,
        deletedAt: null,
      },
      select: deadlineFields,
    });
  }

  findActiveDeadlinesByProgressReportId(progressReportId: number) {
    return this.prisma.progressReportDeadline.findMany({
      where: {
        progressReportId,
        deletedAt: null,
      },
      orderBy: deadlineOrderBy,
      select: deadlineFields,
    });
  }

  findActiveDeadlineBySlot(
    progressReportId: number,
    slot: ProgressReportDeadlineSlot,
  ) {
    return this.prisma.progressReportDeadline.findFirst({
      where: {
        progressReportId,
        slot,
        deletedAt: null,
      },
      select: deadlineFields,
    });
  }

  // Includes soft-deleted rows on purpose. The unique constraint covers the
  // whole table, so a deleted deadline still owns its slot and has to be
  // found before a new deadline can take that slot.
  findAnyDeadlineBySlot(
    progressReportId: number,
    slot: ProgressReportDeadlineSlot,
  ) {
    return this.prisma.progressReportDeadline.findUnique({
      where: {
        progressReportId_slot: { progressReportId, slot },
      },
      select: { ...deadlineFields, deletedAt: true },
    });
  }

  updateDeadline(
    deadlineId: number,
    data: Prisma.ProgressReportDeadlineUpdateInput,
  ) {
    return this.prisma.progressReportDeadline.update({
      where: { id: deadlineId },
      data,
      select: deadlineFields,
    });
  }

  // Brings a soft-deleted deadline back so its slot can be reused.
  restoreAndUpdateDeadline(deadlineId: number, deadline: Date) {
    return this.updateDeadline(deadlineId, {
      deadline,
      status: ProgressReportDeadlineStatus.DRAFT,
      deletedAt: null,
    });
  }

  softDeleteDeadline(deadlineId: number) {
    return this.updateDeadline(deadlineId, { deletedAt: new Date() });
  }

  createMeeting(data: Prisma.ProgressReportMeetingCreateInput) {
    return this.prisma.progressReportMeeting.create({
      data,
      select: meetingFields,
    });
  }

  findMeetingById(meetingId: number) {
    return this.prisma.progressReportMeeting.findFirst({
      where: {
        id: meetingId,
        deletedAt: null,
      },
      select: meetingFields,
    });
  }

  findMeetingsByProgressReportId(progressReportId: number) {
    return this.prisma.progressReportMeeting.findMany({
      where: {
        progressReportId,
        deletedAt: null,
      },
      orderBy: meetingOrderBy,
      select: meetingFields,
    });
  }

  findMeetingByProgressReportIdAndSlot(
    progressReportId: number,
    slot: ProgressReportMeetingSlot,
  ) {
    return this.prisma.progressReportMeeting.findFirst({
      where: {
        progressReportId,
        slot,
        deletedAt: null,
      },
      select: meetingFields,
    });
  }

  // Includes soft-deleted rows on purpose. The unique constraint covers the
  // whole table, so a deleted meeting still owns its slot and has to be found
  // before a new meeting can take that slot.
  findAnyMeetingByProgressReportIdAndSlot(
    progressReportId: number,
    slot: ProgressReportMeetingSlot,
  ) {
    return this.prisma.progressReportMeeting.findUnique({
      where: {
        progressReportId_slot: { progressReportId, slot },
      },
      select: { ...meetingFields, deletedAt: true },
    });
  }

  updateMeeting(
    meetingId: number,
    data: Prisma.ProgressReportMeetingUpdateInput,
  ) {
    return this.prisma.progressReportMeeting.update({
      where: { id: meetingId },
      data,
      select: meetingFields,
    });
  }

  updateMeetingStatus(meetingId: number, status: ProgressReportMeetingStatus) {
    return this.updateMeeting(meetingId, { status });
  }

  softDeleteMeeting(meetingId: number) {
    return this.updateMeeting(meetingId, { deletedAt: new Date() });
  }

  // A soft delete records the deletion time instead of removing the row.
  softDelete(id: number): Promise<ProgressReport> {
    return this.update(id, { deletedAt: new Date() });
  }

  // Ids of every active ministry stakeholder, used to notify all ministries
  // when a progress report is sent.
  async findActiveMinistryStakeholderIds(): Promise<number[]> {
    const stakeholders = await this.prisma.stakeholder.findMany({
      where: {
        active: true,
        deletedAt: null,
        stakeholderType: {
          name: { equals: 'Ministry', mode: 'insensitive' },
          deletedAt: null,
        },
      },
      select: { id: true },
    });

    return stakeholders.map((stakeholder) => stakeholder.id);
  }
}
