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

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

import { ListMeetingSummariesDto } from './dto/list-meeting-summaries.dto';

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

export class MeetingRequestNotReadyToCompleteError extends Error {
  constructor() {
    super('Only a scheduled meeting request can be completed.');
  }
}

// Fields the repository needs to create a meeting summary row.
export type MeetingSummaryParticipantData = {
  pswg_reporter?: string | null;
  pswg_reporter_position?: string | null;
  pswg_representative?: string | null;
  pswg_representative_position?: string | null;
  ministry_reporter?: string | null;
  ministry_reporter_position?: string | null;
  ministry_representative?: string | null;
  ministry_representative_position?: string | null;
};

export type CreateMeetingSummaryData = MeetingSummaryParticipantData & {
  meetingRequestId: number;
  meetingId: number;
  documentReference: string | null;
  meetingSummaryStatusId: number;
  userId: number;
};

// One issue's resolution to create or update. Optional fields are only written
// when provided, so a partial update (the dialog) never wipes existing values.
export type UpsertIssueResolveData = {
  meetingSummaryId: number;
  issueId: number;
  userId: number;
  issueStatusId?: number | null;
  escalateId?: number | null;
  rgcDecision?: unknown;
  nextStep?: unknown;
  remark?: unknown;
  documentReference?: string | null;
  agencyStakeholderIds?: number[];
  governmentAgencies?: IssueGovernmentAgencyInput[];
};

export type IssueGovernmentAgencyInput = {
  stakeholderId: number;
  agencyOrder: number;
};

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

  // Load the meeting a summary will be built from. We only need enough fields
  // to derive the linked meeting request.
  findMeetingForSummary(
    meetingId: number,
    ministryStakeholderIds: number[] = [],
  ) {
    const ministryScope: Prisma.MeetingWhereInput =
      ministryStakeholderIds.length > 0
        ? {
            meetingRequest: {
              governmentAgencies: {
                some: {
                  stakeholderId: { in: ministryStakeholderIds },
                },
              },
            },
          }
        : {};

    return this.prisma.meeting.findFirst({
      where: {
        id: meetingId,
        deletedAt: null,
        ...ministryScope,
      },
      select: {
        id: true,
        title: true,
        meetingDate: true,
        status: true,
        meetingRequestId: true,
      },
    });
  }

  findActiveSummaryByMeetingId(meetingId: number) {
    return this.prisma.meetingSummary.findFirst({
      where: { meetingId, deletedAt: null },
      select: { id: true },
    });
  }

  // Resolve a meeting-summary status code (e.g. "DRAFT") to its row id.
  findStatusIdByCode(code: string) {
    return this.prisma.meetingSummaryStatus.findFirst({
      where: { code, deletedAt: null },
      select: { id: true },
    });
  }

  createSummary(data: CreateMeetingSummaryData) {
    return this.prisma.$transaction(async (tx) => {
      const summary = await tx.meetingSummary.create({
        data: {
          meetingRequestId: data.meetingRequestId,
          meetingId: data.meetingId,
          documentReference: data.documentReference,
          meetingSummaryStatusId: data.meetingSummaryStatusId,
          userId: data.userId,
          pswg_reporter: data.pswg_reporter ?? null,
          pswg_reporter_position: data.pswg_reporter_position ?? null,
          pswg_representative: data.pswg_representative ?? null,
          pswg_representative_position:
            data.pswg_representative_position ?? null,
          ministry_reporter: data.ministry_reporter ?? null,
          ministry_reporter_position: data.ministry_reporter_position ?? null,
          ministry_representative: data.ministry_representative ?? null,
          ministry_representative_position:
            data.ministry_representative_position ?? null,
        },
        include: this.includeRelations(),
      });

      // A draft summary means the meeting has happened and is waiting for its
      // final submission. Only move a scheduled meeting forward; this keeps
      // already submitted or completed meetings unchanged.
      await tx.meeting.updateMany({
        where: {
          id: data.meetingId,
          deletedAt: null,
          status: MeetingStatus.SCHEDULED,
        },
        data: {
          status: MeetingStatus.SUBMITTED,
        },
      });

      return summary;
    });
  }

  // Every (non-deleted) summary the caller may see, without pagination.
  // Used by the Excel export so the file contains the whole table.
  findAllSummariesForExport(scope?: Prisma.MeetingSummaryWhereInput) {
    return this.prisma.meetingSummary.findMany({
      where: {
        deletedAt: null,
        ...(scope ?? {}),
      },
      include: this.includeRelations(),
      orderBy: { id: 'desc' },
    });
  }

  async findManySummaries(
    query: ListMeetingSummariesDto = {},
    scope?: Prisma.MeetingSummaryWhereInput,
  ) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 10;
    const skip = (page - 1) * limit;
    const search = query.search?.trim();

    const where: Prisma.MeetingSummaryWhereInput = {
      deletedAt: null,
      // Optional caller-visibility scope (e.g. only summaries shared with the
      // requesting PSWG user's working group).
      ...(scope ?? {}),
      ...(search
        ? {
            OR: [
              { meeting: { title: { contains: search, mode: 'insensitive' } } },
              {
                meetingRequest: {
                  title: { contains: search, mode: 'insensitive' },
                },
              },
            ],
          }
        : {}),
    };

    const [data, total] = await this.prisma.$transaction([
      this.prisma.meetingSummary.findMany({
        where,
        include: this.includeRelations(),
        orderBy: { id: 'desc' },
        skip,
        take: limit,
      }),
      this.prisma.meetingSummary.count({ where }),
    ]);

    return { data, total };
  }

  findSummaryById(id: number, scope?: Prisma.MeetingSummaryWhereInput) {
    return this.prisma.meetingSummary.findFirst({
      where: { id, deletedAt: null, ...(scope ?? {}) },
      include: this.includeRelations(),
    });
  }

  // Mark a summary as shared with the PSWG working group.
  shareSummary(id: number) {
    return this.prisma.meetingSummary.update({
      where: { id },
      data: { share: true, sharedAt: new Date() },
      include: this.includeRelations(),
    });
  }

  // Reverse a share — hide the summary from the PSWG again.
  unshareSummary(id: number) {
    return this.prisma.meetingSummary.update({
      where: { id },
      data: { share: false, sharedAt: null },
      include: this.includeRelations(),
    });
  }

  // The Private Sector (working group) stakeholder ids the user belongs to.
  // Empty when the user is not a private-sector member (e.g. a ministry user).
  async findPrivateSectorStakeholderIdsByUserId(
    userId: number,
  ): Promise<number[]> {
    const links = await this.prisma.stakeholderUser.findMany({
      where: { userId },
      select: {
        stakeholder: {
          select: { id: true, stakeholderTypeId: true, deletedAt: true },
        },
      },
    });

    return links
      .map((link) => link.stakeholder)
      .filter(
        (stakeholder) =>
          stakeholder !== null &&
          stakeholder.deletedAt === null &&
          // stakeholderTypeId 2 === Private Sector / working group.
          stakeholder.stakeholderTypeId === 2,
      )
      .map((stakeholder) => stakeholder.id);
  }

  // Ministry stakeholder ids assigned to the current user. An empty result
  // means the caller is not assigned to a Ministry stakeholder.
  async findMinistryStakeholderIdsByUserId(userId: number): Promise<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)));
  }

  // Active user ids holding the given role (e.g. "cdc"). Used to notify every
  // CDC user when the ministry escalates an issue to CDC.
  async findActiveUserIdsByRoleName(roleName: string): Promise<number[]> {
    const links = await this.prisma.userHasRoles.findMany({
      where: {
        role: { name: roleName, deletedAt: null },
        user: { isActive: true, deletedAt: null },
      },
      select: { userId: true },
    });

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

  // The title of a working-group issue (used in the escalation notification).
  findIssueTitleById(issueId: number) {
    return this.prisma.issues.findFirst({
      where: { id: issueId },
      select: { title: true },
    });
  }

  // A user's display name (used as the notification sender name).
  findUserNameById(userId: number) {
    return this.prisma.user.findFirst({
      where: { id: userId },
      select: { name: true },
    });
  }

  // Active users belonging to the given working-group stakeholder(s).
  // Used to notify a PSWG when a summary is shared with them.
  findUsersByStakeholderIds(stakeholderIds: number[]) {
    return this.prisma.stakeholderUser.findMany({
      where: {
        stakeholderId: { in: stakeholderIds },
        user: { isActive: true, deletedAt: null },
      },
      select: { userId: true },
    });
  }

  updateSummary(
    id: number,
    data: MeetingSummaryParticipantData & {
      documentReference?: string | null;
      meetingSummaryStatusId?: number;
    },
  ) {
    return this.prisma.meetingSummary.update({
      where: { id },
      data,
      include: this.includeRelations(),
    });
  }

  submitSummaryAndCompleteMeetingRequest(
    id: number,
    meetingRequestId: number,
    meetingId: number,
    meetingSummaryStatusId: number,
    documentReference?: string | null,
  ) {
    return this.prisma.$transaction(async (tx) => {
      const meetingRequest = await tx.meetingRequests.findFirst({
        where: {
          id: meetingRequestId,
          deletedAt: null,
        },
        select: {
          status: true,
        },
      });

      if (!meetingRequest) {
        throw new MeetingRequestNotReadyToCompleteError();
      }

      if (
        meetingRequest.status !== MeetingRequestStatus.SCHEDULED &&
        meetingRequest.status !== MeetingRequestStatus.COMPLETED
      ) {
        throw new MeetingRequestNotReadyToCompleteError();
      }

      await tx.meeting.update({
        where: { id: meetingId },
        data: { status: MeetingStatus.COMPLETED },
      });

      const unfinishedMeetingCount = await tx.meeting.count({
        where: {
          meetingRequestId,
          deletedAt: null,
          status: {
            not: MeetingStatus.COMPLETED,
          },
        },
      });
      const nextRequestStatus =
        unfinishedMeetingCount === 0
          ? MeetingRequestStatus.COMPLETED
          : MeetingRequestStatus.SCHEDULED;

      if (meetingRequest.status !== nextRequestStatus) {
        await tx.meetingRequests.update({
          where: {
            id: meetingRequestId,
          },
          data: {
            status: nextRequestStatus,
          },
        });
      }

      return tx.meetingSummary.update({
        where: { id },
        data: {
          meetingSummaryStatusId,
          ...(documentReference !== undefined && { documentReference }),
        },
        include: this.includeRelations(),
      });
    });
  }

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

  findEscalateByName(name: string) {
    return this.prisma.escalate.findFirst({
      where: { name, deletedAt: null },
      select: { id: true },
    });
  }

  // Government agencies are the active ministry-type stakeholders. Used to fill
  // the agency dropdowns when resolving an issue.
  findGovernmentAgencies() {
    return this.prisma.stakeholder.findMany({
      where: { deletedAt: null, active: true, stakeholderTypeId: 1 },
      orderBy: { name: 'asc' },
      select: { id: true, name: true, logo: true },
    });
  }

  findGovernmentAgenciesByIds(ids: number[]) {
    return this.prisma.stakeholder.findMany({
      where: {
        id: { in: ids },
        deletedAt: null,
        active: true,
        stakeholderTypeId: 1,
      },
      select: { id: true },
    });
  }

  // Resolve an issue status by its code (e.g. "IN_PROGRESS") so the resolution
  // status can point at the issue_statuses table.
  findIssueStatusByCode(code: string) {
    return this.prisma.issueStatuses.findFirst({
      where: { code, deletedAt: null },
      select: { id: true },
    });
  }

  // Keep the original issue's status in sync when it is resolved in a summary.
  updateIssueStatus(issueId: number, issueStatusId: number) {
    return this.prisma.issues.update({
      where: { id: issueId },
      data: { issueStatusId },
    });
  }

  // Create or update the single issue_resolves row for (summary, issue), then
  // replace its extra-agency links. Wrapped in a transaction so the resolve and
  // its agencies always stay consistent.
  async upsertIssueResolve(input: UpsertIssueResolveData) {
    return this.prisma.$transaction(async (tx) => {
      const existing = await tx.issueResolves.findFirst({
        where: {
          meetingSummaryId: input.meetingSummaryId,
          issueId: input.issueId,
          deletedAt: null,
        },
        select: { id: true },
      });

      // Only set a column when the caller provided it.
      const writableFields: Prisma.IssueResolvesUncheckedUpdateInput = {
        ...(input.issueStatusId !== undefined && {
          issueStatusId: input.issueStatusId,
        }),
        ...(input.escalateId !== undefined && { escalateId: input.escalateId }),
        ...(input.rgcDecision !== undefined && {
          rgcDecision: this.toJsonInput(input.rgcDecision),
        }),
        ...(input.nextStep !== undefined && {
          nextStep: this.toJsonInput(input.nextStep),
        }),
        ...(input.remark !== undefined && {
          remark: this.toJsonInput(input.remark),
        }),
        ...(input.documentReference !== undefined && {
          documentReference: input.documentReference,
        }),
      };

      let resolveId: number;

      if (existing) {
        await tx.issueResolves.update({
          where: { id: existing.id },
          data: writableFields,
        });
        resolveId = existing.id;
      } else {
        const created = await tx.issueResolves.create({
          data: {
            meetingSummaryId: input.meetingSummaryId,
            issueId: input.issueId,
            userId: input.userId,
            issueStatusId: input.issueStatusId ?? null,
            escalateId: input.escalateId ?? null,
            rgcDecision: this.toJsonInput(input.rgcDecision),
            nextStep: this.toJsonInput(input.nextStep),
            remark: this.toJsonInput(input.remark),
            documentReference: input.documentReference ?? null,
          },
          select: { id: true },
        });
        resolveId = created.id;
      }

      if (input.agencyStakeholderIds !== undefined) {
        await tx.issueResolveAgencies.deleteMany({
          where: { issueResolveId: resolveId },
        });

        if (input.agencyStakeholderIds.length > 0) {
          await tx.issueResolveAgencies.createMany({
            data: input.agencyStakeholderIds.map((stakeholderId) => ({
              issueResolveId: resolveId,
              stakeholderId,
            })),
          });
        }
      }

      if (input.governmentAgencies !== undefined) {
        await tx.governmentAgencyIssue.deleteMany({
          where: { issueId: input.issueId },
        });

        await tx.governmentAgencyIssue.createMany({
          data: input.governmentAgencies.map((agency) => ({
            issueId: input.issueId,
            stakeholderId: agency.stakeholderId,
            agencyOrder: agency.agencyOrder,
          })),
        });

        await tx.issueResolveAgencies.deleteMany({
          where: { issueResolveId: resolveId },
        });
      }

      return tx.issueResolves.findUnique({
        where: { id: resolveId },
        include: this.includeIssueResolveRelations(),
      });
    });
  }

  // Convert an incoming value into something a Prisma JSON column accepts.
  // A missing value clears the column (SQL NULL).
  private toJsonInput(
    value: unknown,
  ): Prisma.InputJsonValue | typeof Prisma.DbNull {
    if (value === undefined || value === null) {
      return Prisma.DbNull;
    }

    return value;
  }

  private includeIssueResolveRelations() {
    return {
      issueStatus: { select: { id: true, code: true, name: true } },
      escalate: { select: { id: true, name: true } },
      issueResolveAgencies: {
        include: {
          stakeholder: { select: { id: true, name: true, logo: true } },
        },
      },
    } as const;
  }

  private includeRelations() {
    return {
      user: { select: { id: true, name: true, email: true } },
      meetingSummaryStatus: {
        select: { id: true, code: true, name: true },
      },
      meeting: {
        select: {
          id: true,
          title: true,
          description: true,
          meetingDate: true,
          startTime: true,
          endTime: true,
          location: true,
          documentReference: true,
          status: true,
        },
      },
      meetingRequest: {
        include: {
          user: {
            select: {
              id: true,
              name: true,
              email: true,
              // The stakeholder(s) the requesting user belongs to, so the PSWG
              // column can show their working group / ministry name.
              stakeholders: {
                select: {
                  stakeholder: { select: { id: true, name: true, logo: true } },
                },
              },
            },
          },
          governmentAgencies: {
            orderBy: { stakeholderId: 'asc' },
            select: {
              stakeholderId: true,
              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,
              issueStatus: { select: { id: true, code: true, name: true } },
              category: { select: { id: true, name: true } },
              governmentAgencies: {
                orderBy: { agencyOrder: 'asc' },
                select: {
                  agencyOrder: true,
                  stakeholderId: true,
                  stakeholder: {
                    select: { id: true, name: true, logo: true },
                  },
                },
              },
            },
          },
        },
      },
      issueResolves: {
        where: { deletedAt: null },
        include: this.includeIssueResolveRelations(),
      },
    } as const;
  }
}
