import { Injectable } from '@nestjs/common';
import {
  MeetingRequestStatus,
  MeetingStatus,
  Prisma,
} from '@/generated/prisma/client';

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

import { ListMeetingsDto } from './dto/list-meetings.dto';

// Stakeholder type id for the seeded Ministry stakeholder type.
const MINISTRY_STAKEHOLDER_TYPE_ID = 1;

export class MeetingRequestNotReadyToScheduleError extends Error {
  constructor() {
    super(
      'A meeting can be created only when the request is ready and has no unfinished meeting.',
    );
  }
}

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

  create(
    data: Prisma.MeetingCreateInput,
    meetingRequestId?: number,
    scheduleMeetingRequest = false,
  ) {
    if (meetingRequestId) {
      return this.prisma.$transaction(async (tx) => {
        const meetingRequest = await tx.meetingRequests.findFirst({
          where: {
            id: meetingRequestId,
            deletedAt: null,
          },
          select: {
            status: true,
          },
        });
        const linkedMeetings = await tx.meeting.findMany({
          where: {
            meetingRequestId,
            deletedAt: null,
          },
          select: {
            status: true,
          },
        });
        const hasUnfinishedMeeting = linkedMeetings.some(
          (meeting) => meeting.status !== MeetingStatus.COMPLETED,
        );
        const isFirstMeetingReady =
          meetingRequest?.status === MeetingRequestStatus.UNDER_REVIEW &&
          linkedMeetings.length === 0;
        const arePreviousMeetingsCompleted =
          meetingRequest?.status === MeetingRequestStatus.COMPLETED &&
          linkedMeetings.length > 0 &&
          !hasUnfinishedMeeting;

        if (!isFirstMeetingReady && !arePreviousMeetingsCompleted) {
          throw new MeetingRequestNotReadyToScheduleError();
        }

        if (scheduleMeetingRequest) {
          const updatedRequest = await tx.meetingRequests.updateMany({
            where: {
              id: meetingRequestId,
              deletedAt: null,
              status: meetingRequest?.status,
            },
            data: {
              status: MeetingRequestStatus.SCHEDULED,
            },
          });

          if (updatedRequest.count !== 1) {
            throw new MeetingRequestNotReadyToScheduleError();
          }
        }

        return tx.meeting.create({
          data,
          include: this.includeRelations(),
        });
      });
    }

    return this.prisma.meeting.create({
      data,
      include: this.includeRelations(),
    });
  }

  findMany(query: ListMeetingsDto = {}, scope?: Prisma.MeetingWhereInput) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 10;
    const skip = (page - 1) * limit;

    const where: Prisma.MeetingWhereInput = {
      deletedAt: null,
      ...(scope ?? {}),
      userId: query.userId,
      meetingRequestId: query.meetingRequestId,
      status: query.status,
    };

    return this.prisma.$transaction(async (tx) => {
      const [data, total] = await Promise.all([
        tx.meeting.findMany({
          where,
          include: this.includeRelations(),
          orderBy: {
            id: 'desc',
          },
          skip,
          take: limit,
        }),
        tx.meeting.count({
          where,
        }),
      ]);

      return {
        data,
        total,
      };
    });
  }

  findById(id: number, scope?: Prisma.MeetingWhereInput) {
    return this.prisma.meeting.findFirst({
      where: {
        id,
        deletedAt: null,
        ...(scope ?? {}),
      },
      include: this.includeRelations(),
    });
  }

  update(
    id: number,
    data: Prisma.MeetingUpdateInput,
    meetingRequestIdToSchedule?: number,
  ) {
    if (meetingRequestIdToSchedule) {
      return this.prisma.$transaction(async (tx) => {
        const meetingRequest = await tx.meetingRequests.findFirst({
          where: {
            id: meetingRequestIdToSchedule,
            deletedAt: null,
          },
          select: {
            status: true,
          },
        });
        const otherMeetings = await tx.meeting.findMany({
          where: {
            meetingRequestId: meetingRequestIdToSchedule,
            deletedAt: null,
            id: {
              not: id,
            },
          },
          select: {
            status: true,
          },
        });
        const hasOtherUnfinishedMeeting = otherMeetings.some(
          (meeting) => meeting.status !== MeetingStatus.COMPLETED,
        );

        if (
          !meetingRequest ||
          (meetingRequest.status !== MeetingRequestStatus.UNDER_REVIEW &&
            meetingRequest.status !== MeetingRequestStatus.SCHEDULED &&
            meetingRequest.status !== MeetingRequestStatus.COMPLETED) ||
          hasOtherUnfinishedMeeting
        ) {
          throw new MeetingRequestNotReadyToScheduleError();
        }

        if (meetingRequest.status !== MeetingRequestStatus.SCHEDULED) {
          const updatedRequest = await tx.meetingRequests.updateMany({
            where: {
              id: meetingRequestIdToSchedule,
              deletedAt: null,
              status: meetingRequest.status,
            },
            data: {
              status: MeetingRequestStatus.SCHEDULED,
            },
          });

          if (updatedRequest.count !== 1) {
            throw new MeetingRequestNotReadyToScheduleError();
          }
        }

        return tx.meeting.update({
          where: {
            id,
          },
          data,
          include: this.includeRelations(),
        });
      });
    }

    return this.prisma.meeting.update({
      where: {
        id,
      },
      data,
      include: this.includeRelations(),
    });
  }

  softDelete(id: number) {
    return this.prisma.meeting.update({
      where: {
        id,
      },
      data: {
        deletedAt: new Date(),
      },
    });
  }

  findUserById(id: number) {
    return this.prisma.user.findFirst({
      where: {
        id,
        deletedAt: null,
        isActive: true,
      },
      select: {
        id: true,
      },
    });
  }

  findMeetingRequestById(id: number) {
    return this.prisma.meetingRequests.findFirst({
      where: {
        id,
        deletedAt: null,
      },
      select: {
        id: true,
      },
    });
  }

  async findMinistryStakeholderIdsByUserId(userId: number) {
    const links = await this.prisma.stakeholderUser.findMany({
      where: {
        userId,
        stakeholder: {
          deletedAt: null,
          stakeholderTypeId: MINISTRY_STAKEHOLDER_TYPE_ID,
        },
      },
      select: { stakeholderId: true },
    });

    return Array.from(new Set(links.map((link) => link.stakeholderId)));
  }

  findMeetingRequestForStakeholderIds(
    meetingRequestId: number,
    stakeholderIds: number[],
  ) {
    return this.prisma.meetingRequests.findFirst({
      where: {
        id: meetingRequestId,
        deletedAt: null,
        governmentAgencies: {
          some: {
            stakeholderId: {
              in: stakeholderIds,
            },
          },
        },
      },
      select: {
        id: true,
        status: true,
        meetings: {
          where: {
            deletedAt: null,
          },
          select: {
            status: true,
          },
        },
        governmentAgencies: {
          select: {
            stakeholderId: true,
          },
        },
      },
    });
  }

  async findWorkingGroupUsersByMinistryStakeholderIds(
    ministryStakeholderIds: number[],
  ) {
    if (ministryStakeholderIds.length === 0) {
      return [];
    }

    const ministries = await this.prisma.stakeholder.findMany({
      where: {
        id: {
          in: ministryStakeholderIds,
        },
        deletedAt: null,
        active: true,
      },
      select: {
        id: true,
        relatedStakeholderId: true,
      },
    });

    const directlyLinkedWorkingGroupIds = ministries
      .map((ministry) => ministry.relatedStakeholderId)
      .filter(
        (stakeholderId): stakeholderId is number =>
          typeof stakeholderId === 'number' && stakeholderId > 0,
      );

    const workingGroups = await this.prisma.stakeholder.findMany({
      where: {
        deletedAt: null,
        active: true,
        stakeholderTypeId: 2,
        OR: [
          {
            id: {
              in: directlyLinkedWorkingGroupIds,
            },
          },
          {
            relatedStakeholderId: {
              in: ministryStakeholderIds,
            },
          },
        ],
      },
      select: {
        id: true,
      },
    });

    const workingGroupIds = Array.from(
      new Set(workingGroups.map((workingGroup) => workingGroup.id)),
    );

    if (workingGroupIds.length === 0) {
      return [];
    }

    return this.prisma.stakeholderUser.findMany({
      where: {
        stakeholderId: {
          in: workingGroupIds,
        },
      },
      orderBy: {
        createdAt: 'asc',
      },
      select: {
        user: {
          select: {
            id: true,
            name: true,
            isActive: true,
            deletedAt: true,
          },
        },
      },
    });
  }

  findMeetingRequestAgencyIds(id: number) {
    return this.prisma.meetingRequests.findFirst({
      where: {
        id,
        deletedAt: null,
      },
      select: {
        id: true,
        governmentAgencies: {
          select: {
            stakeholderId: true,
          },
        },
      },
    });
  }

  findUsersByStakeholderIds(stakeholderIds: number[]) {
    return this.prisma.stakeholderUser.findMany({
      where: {
        stakeholderId: {
          in: stakeholderIds,
        },
      },
      orderBy: {
        createdAt: 'asc',
      },
      select: {
        user: {
          select: {
            id: true,
            name: true,
            email: true,
            avatar: true,
            position: true,
            isActive: true,
            deletedAt: true,
          },
        },
      },
    });
  }

  findActiveUsersByRoleName(roleName: string) {
    return this.prisma.user.findMany({
      where: {
        isActive: true,
        deletedAt: null,
        roles: {
          some: {
            role: {
              name: roleName,
              deletedAt: null,
            },
          },
        },
      },
      select: {
        id: true,
        name: true,
        email: true,
        avatar: true,
        position: true,
      },
    });
  }

  private includeRelations() {
    return {
      user: {
        select: {
          id: true,
          name: true,
          email: true,
          position: true,
        },
      },
      meetingRequest: {
        include: {
          user: {
            select: {
              id: true,
              name: true,
              email: true,
              position: true,
              // The stakeholder(s) the requesting user belongs to, so the
              // meeting request can report its working group / ministry name.
              stakeholders: {
                select: {
                  stakeholder: {
                    select: { id: true, name: true, coChair: true },
                  },
                },
              },
            },
          },
          governmentAgencies: {
            include: {
              stakeholder: {
                select: {
                  id: true,
                  name: true,
                  logo: true,
                  stakeholderTypeId: true,
                  relatedStakeholderId: true,
                  stakeholderType: {
                    select: {
                      name: true,
                    },
                  },
                },
              },
            },
          },
          issues: {
            where: {
              deletedAt: null,
            },
            orderBy: {
              id: 'asc',
            },
            select: {
              id: true,
              title: true,
              description: true,
              recommendation: true,
              attachment: true,
              createdAt: true,
              updatedAt: true,
              issueStatus: {
                select: {
                  id: true,
                  code: true,
                  name: true,
                },
              },
              category: {
                select: {
                  id: true,
                  name: true,
                },
              },
              governmentAgencies: {
                orderBy: {
                  agencyOrder: 'asc',
                },
                select: {
                  agencyOrder: true,
                  stakeholder: {
                    select: {
                      id: true,
                      name: true,
                      logo: true,
                      stakeholderTypeId: true,
                      relatedStakeholderId: true,
                      stakeholderType: {
                        select: {
                          name: true,
                        },
                      },
                    },
                  },
                },
              },
            },
          },
        },
      },
      guests: {
        where: {
          deletedAt: null,
        },
        include: {
          user: {
            select: {
              id: true,
              name: true,
              email: true,
              position: true,
            },
          },
        },
      },
      meetingSummaries: {
        where: {
          deletedAt: null,
        },
        select: {
          id: true,
        },
        take: 1,
      },
    } as const;
  }

  async findRelatedStakeholders(
    agencyIds: number[],
    relatedStakeholderIds: number[],
  ) {
    const filters: Prisma.StakeholderWhereInput[] = [];

    if (relatedStakeholderIds.length > 0) {
      filters.push({
        id: {
          in: relatedStakeholderIds,
        },
      });
    }

    if (agencyIds.length > 0) {
      filters.push({
        relatedStakeholderId: {
          in: agencyIds,
        },
      });
    }

    if (filters.length === 0) {
      return [];
    }

    return this.prisma.stakeholder.findMany({
      where: {
        deletedAt: null,
        active: true,
        OR: filters,
      },
      select: {
        id: true,
        name: true,
        logo: true,
        stakeholderTypeId: true,
        relatedStakeholderId: true,
        stakeholderType: {
          select: {
            name: true,
          },
        },
      },
    });
  }
}
