import { Prisma } from '../../src/generated/prisma/client';

export type ResolutionSource = {
  sourceRef: string;
  issueRef: string;
  issueStatusCode: string;
  rgcDecision?: Prisma.JsonValue;
  rgcDecisionKh?: Prisma.JsonValue;
  nextStep?: Prisma.JsonValue;
  documentReference?: string | null;
};

export type MeetingSource = {
  sourceRef: string;
  ministryName: string;
  ownerEmail: string | null;
  title: string;
  description?: string | null;
  previousTitle?: string;
  previousDescription?: string;
  meetingDate: string | null;
  startTime: string | null;
  location?: string | null;
  documentReference?: string | null;
  request: {
    title: string;
    previousTitle?: string;
    description: string;
    previousDescription?: string;
    meetingRequestLetter?: string | null;
    issueRefs: string[];
  };
  summary: {
    documentReference?: string | null;
    resolutions: ResolutionSource[];
  };
};

type Validators = {
  object: (value: unknown, keys: string[], label: string) => void;
  text: (value: unknown, label: string, max?: number) => void;
  optionalText: (value: unknown, label: string, max?: number) => void;
  email: (value: unknown, label: string, exampleOnly: boolean) => void;
  unique: (value: string, seen: Set<string>, label: string) => void;
};

// sourceRef links are file-only references, never database IDs.
export function validateMeetingSources(
  value: unknown,
  issueRefs: Set<string>,
  refs: Set<string>,
  exampleOnly: boolean,
  check: Validators,
) {
  if (!Array.isArray(value)) throw new Error('meetings must be an array');
  const resolutions = new Map<string, ResolutionSource>();
  for (const raw of value as unknown[]) {
    check.object(
      raw,
      [
        'sourceRef',
        'ministryName',
        'ownerEmail',
        'title',
        'description',
        'previousTitle',
        'previousDescription',
        'meetingDate',
        'startTime',
        'location',
        'documentReference',
        'request',
        'summary',
      ],
      'Meeting',
    );
    const meeting = raw as MeetingSource;
    for (const key of ['sourceRef', 'ministryName', 'title'] as const)
      check.text(meeting[key], `Meeting ${key}`, key === 'title' ? 200 : 255);
    check.unique(meeting.sourceRef, refs, 'sourceRef');
    check.email(meeting.ownerEmail, 'Meeting ownerEmail', exampleOnly);
    if (
      meeting.previousTitle !== undefined ||
      meeting.previousDescription !== undefined
    ) {
      check.text(meeting.previousTitle, 'Previous meeting title', 200);
      check.text(meeting.previousDescription, 'Previous meeting description');
    }
    for (const key of ['description', 'location', 'documentReference'] as const)
      check.optionalText(meeting[key], `Meeting ${key}`);
    if (meeting.meetingDate !== null) {
      check.text(meeting.meetingDate, 'meetingDate', 10);
      const date = new Date(`${meeting.meetingDate}T00:00:00.000Z`);
      if (
        !/^\d{4}-\d{2}-\d{2}$/.test(meeting.meetingDate) ||
        !Number.isFinite(date.getTime()) ||
        date.toISOString().slice(0, 10) !== meeting.meetingDate
      )
        throw new Error('meetingDate must be a real YYYY-MM-DD date or null');
    }
    if (
      meeting.startTime !== null &&
      (typeof meeting.startTime !== 'string' ||
        !/^([01]\d|2[0-3]):[0-5]\d$/.test(meeting.startTime))
    )
      throw new Error('startTime must be HH:mm or null');
    check.object(
      meeting.request,
      [
        'title',
        'previousTitle',
        'description',
        'previousDescription',
        'meetingRequestLetter',
        'issueRefs',
      ],
      'Meeting request',
    );
    check.text(meeting.request.title, 'Request title', 200);
    check.optionalText(
      meeting.request.previousTitle,
      'Previous request title',
      200,
    );
    check.text(meeting.request.description, 'Request description');
    check.optionalText(
      meeting.request.previousDescription,
      'Previous request description',
    );
    check.optionalText(
      meeting.request.meetingRequestLetter,
      'meetingRequestLetter',
      255,
    );
    if (
      !Array.isArray(meeting.request.issueRefs) ||
      !meeting.request.issueRefs.length
    )
      throw new Error('Request issueRefs must include at least one issue');
    const requested = new Set<string>();
    for (const ref of meeting.request.issueRefs) {
      check.text(ref, 'Request issueRef', 255);
      if (!issueRefs.has(ref))
        throw new Error(`Unknown request issueRef: ${ref}`);
      check.unique(ref, requested, 'request issueRef');
    }
    check.object(
      meeting.summary,
      ['documentReference', 'resolutions'],
      'Meeting summary',
    );
    check.optionalText(
      meeting.summary.documentReference,
      'Summary documentReference',
      255,
    );
    if (!Array.isArray(meeting.summary.resolutions))
      throw new Error('Summary resolutions must be an array');
    const answered = new Set<string>();
    for (const resolution of meeting.summary.resolutions) {
      check.object(
        resolution,
        [
          'sourceRef',
          'issueRef',
          'issueStatusCode',
          'rgcDecision',
          'rgcDecisionKh',
          'nextStep',
          'documentReference',
        ],
        'Resolution',
      );
      check.text(resolution.sourceRef, 'Resolution sourceRef', 255);
      check.unique(resolution.sourceRef, refs, 'sourceRef');
      check.text(resolution.issueRef, 'Resolution issueRef', 255);
      if (!requested.has(resolution.issueRef))
        throw new Error(
          `Resolution issue is not in the request: ${resolution.issueRef}`,
        );
      check.unique(
        resolution.issueRef,
        answered,
        'summary resolution for issue',
      );
      if (
        !['SOLVED', 'IN_PROGRESS', 'NOT_ADDRESSED'].includes(
          resolution.issueStatusCode,
        )
      )
        throw new Error(
          'Resolution requires SOLVED, IN_PROGRESS or NOT_ADDRESSED',
        );
      check.optionalText(
        resolution.documentReference,
        'Solution documentReference',
        255,
      );
      resolutions.set(resolution.sourceRef, resolution);
    }
  }
  return resolutions;
}

export type MeetingSeedIssue = {
  source: { sourceRef: string };
  id: number | null;
  groupId: number;
  userId: number;
  primaryRequestId: number | null;
  updatedAt: Date | null;
  agencies: { stakeholderId: number; agencyOrder: number }[];
};

export type ResolutionPlan = {
  source: ResolutionSource;
  issue: MeetingSeedIssue;
  id: number | null;
  ministryId: number;
  userId: number;
  statusId: number;
  missingAgencyIds: number[];
};

type Context = {
  finalizeSeedMeetings?: boolean;
  preserveExistingResolutions?: boolean;
  issues: MeetingSeedIssue[];
  ministryId: (name: string) => number;
  authorId: (email: string | null, ministryId: number) => number;
  statusId: (code: string) => number;
  sameJson: (left: unknown, right: unknown) => boolean;
};

export type MeetingPlan = {
  finalize: boolean;
  submitRequest: boolean;
  submittedAt: Date | null;
  completeSummary: boolean;
  shareSummary: boolean;
  calendarUpdate: boolean;
  renameMeeting: boolean;
  source: MeetingSource;
  requestId: number | null;
  previousRequestTitle: string | null;
  previousRequestDescription: string | null;
  meetingId: number | null;
  summaryId: number | null;
  userId: number;
  ministryId: number;
  requestOwnerId: number;
  summaryStatusId: number;
  issues: {
    issue: MeetingSeedIssue;
    missingRequestLink: boolean;
    setPrimary: boolean;
  }[];
  missingRequestAgency: boolean;
  missingMeetingLink: boolean;
  missingSummaryLink: boolean;
  resolutions: ResolutionPlan[];
};

export class UnverifiedSeedMeeting extends Error {}

function match<T>(rows: T[], label: string, verify = false): T | null {
  if (verify && rows.length > 1)
    throw new UnverifiedSeedMeeting(`Ambiguous ${label}`);
  if (rows.length > 1) throw new Error(`Ambiguous ${label}`);
  return rows[0] ?? null;
}

function conflict(condition: boolean, label: string) {
  if (condition)
    throw new Error(`Existing ${label} conflicts; not overwritten or restored`);
}

export async function buildMeetingPlan(
  tx: Prisma.TransactionClient,
  sources: MeetingSource[],
  context: Context,
): Promise<MeetingPlan[]> {
  if (!sources.length) return [];
  const targetCode = context.finalizeSeedMeetings ? 'COMPLETED' : 'DRAFT';
  const target = await tx.meetingSummaryStatus.findUnique({
    where: { code: targetCode },
    select: { id: true, deletedAt: true },
  });
  if (!target || target.deletedAt)
    throw new Error(`Missing active ${targetCode} meeting-summary status`);
  const issueByRef = new Map(
    context.issues.map((issue) => [issue.source.sourceRef, issue]),
  );
  const plans: MeetingPlan[] = [];
  const requestKeys = new Set<string>();
  const meetingKeys = new Set<string>();
  const primarySelected = new Set<string>();
  for (const source of sources) {
    const issues = source.request.issueRefs.map((ref) => issueByRef.get(ref)!);
    const ministryId = context.ministryId(source.ministryName);
    const userId = context.authorId(source.ownerEmail, ministryId);
    const owner = issues[0];
    if (
      issues.some(
        (issue) =>
          issue.groupId !== owner.groupId ||
          issue.userId !== owner.userId ||
          !issue.agencies.some(
            (agency) =>
              agency.stakeholderId === ministryId && agency.agencyOrder === 1,
          ),
      )
    )
      throw new Error(
        `Meeting requires one working-group owner and its primary ministry: ${source.sourceRef}`,
      );
    const requestKey = `${owner.userId}:${source.request.title}`;
    const meetingKey = `${userId}:${source.title}`;
    if (requestKeys.has(requestKey) || meetingKeys.has(meetingKey))
      throw new Error(
        'Use one meeting object with all issueRefs/resolutions for a shared request/meeting',
      );
    requestKeys.add(requestKey);
    meetingKeys.add(meetingKey);
    const request = match(
      await tx.meetingRequests.findMany({
        where: {
          title: source.request.previousTitle
            ? { in: [source.request.title, source.request.previousTitle] }
            : source.request.title,
          userId: owner.userId,
        },
        include: { governmentAgencies: true },
      }),
      `meeting request: ${source.sourceRef}`,
      context.finalizeSeedMeetings,
    );
    // Workflow changes after import are valid; never reset them on a rerun.
    if (request && !context.finalizeSeedMeetings)
      conflict(
        Boolean(request.deletedAt) ||
          (request.description !== source.request.description &&
            request.description !== source.request.previousDescription) ||
          request.meetingRequestLetter !==
            (source.request.meetingRequestLetter ?? ''),
        `meeting request: ${source.sourceRef}`,
      );
    const meeting = match(
      await tx.meeting.findMany({
        where: {
          title: source.previousTitle
            ? { in: [source.title, source.previousTitle] }
            : source.title,
          userId,
        },
        select: {
          id: true,
          title: true,
          deletedAt: true,
          status: true,
          meetingRequestId: true,
          description: true,
          meetingDate: true,
          startTime: true,
          location: true,
          documentReference: true,
        },
      }),
      `meeting: ${source.sourceRef}`,
      context.finalizeSeedMeetings,
    );
    const renameMeeting = Boolean(
      meeting &&
      source.previousTitle &&
      meeting.title === source.previousTitle &&
      meeting.description === source.previousDescription,
    );
    if (
      source.previousTitle &&
      meeting &&
      !renameMeeting &&
      (meeting.title !== source.title ||
        meeting.description !== source.description)
    )
      throw new UnverifiedSeedMeeting(
        `Manually edited meeting text: ${source.sourceRef}`,
      );
    if (
      source.previousTitle &&
      !request &&
      !meeting &&
      issues.some((issue) => issue.primaryRequestId !== null)
    )
      throw new UnverifiedSeedMeeting(
        `Existing linked issues without a matching seed meeting: ${source.sourceRef}`,
      );
    if (renameMeeting && !context.finalizeSeedMeetings)
      throw new UnverifiedSeedMeeting(
        `Use the verified main seed to rename: ${source.sourceRef}`,
      );
    if (context.finalizeSeedMeetings && (meeting || request)) {
      if (
        !request ||
        !meeting ||
        request.deletedAt ||
        meeting.deletedAt ||
        meeting.meetingRequestId !== request.id ||
        !request.governmentAgencies.some((a) => a.stakeholderId === ministryId)
      )
        throw new UnverifiedSeedMeeting(
          `Missing, deleted or unverified request/meeting links: ${source.sourceRef}`,
        );
    }
    if (meeting && !context.finalizeSeedMeetings)
      conflict(
        Boolean(meeting.deletedAt) ||
          !request ||
          meeting.meetingRequestId !== request.id ||
          meeting.description !== (source.description ?? null) ||
          (meeting.meetingDate?.toISOString().slice(0, 10) ?? null) !==
            source.meetingDate ||
          (meeting.startTime?.toISOString().slice(11, 16) ?? null) !==
            source.startTime ||
          meeting.location !== (source.location ?? null) ||
          meeting.documentReference !== (source.documentReference ?? null),
        `meeting: ${source.sourceRef}`,
      );
    const summary = meeting
      ? await tx.meetingSummary.findUnique({
          where: { meetingId: meeting.id },
          include: { meetingSummaryStatus: { select: { code: true } } },
        })
      : null;
    if (
      context.finalizeSeedMeetings &&
      summary &&
      (summary.deletedAt ||
        summary.userId !== userId ||
        summary.meetingRequestId !== request?.id)
    )
      throw new UnverifiedSeedMeeting(
        `Deleted or unverified summary: ${source.sourceRef}`,
      );
    if (summary && !context.finalizeSeedMeetings)
      conflict(
        Boolean(summary.deletedAt) ||
          summary.userId !== userId ||
          summary.meetingRequestId !== request!.id ||
          summary.documentReference !==
            (source.summary.documentReference ?? null),
        `meeting summary: ${source.sourceRef}`,
      );
    const meetingLink =
      meeting && request
        ? await tx.meetingMeetingRequest.findUnique({
            where: {
              meetingId_meetingRequestId: {
                meetingId: meeting.id,
                meetingRequestId: request.id,
              },
            },
          })
        : null;
    const summaryLink =
      summary && meeting
        ? await tx.meetingSummaryMeeting.findUnique({
            where: {
              meetingSummaryId_meetingId: {
                meetingSummaryId: summary.id,
                meetingId: meeting.id,
              },
            },
          })
        : null;
    const plannedIssues: MeetingPlan['issues'] = [];
    for (const issue of issues) {
      const link =
        request && issue.id !== null
          ? await tx.issueMeetingRequest.findUnique({
              where: {
                issueId_meetingRequestId: {
                  issueId: issue.id,
                  meetingRequestId: request.id,
                },
              },
            })
          : null;
      const setPrimary =
        issue.primaryRequestId === null &&
        !primarySelected.has(issue.source.sourceRef);
      if (setPrimary) primarySelected.add(issue.source.sourceRef);
      plannedIssues.push({ issue, missingRequestLink: !link, setPrimary });
    }
    const resolutions: ResolutionPlan[] = [];
    if (
      context.finalizeSeedMeetings &&
      request &&
      (!meetingLink ||
        (summary && !summaryLink) ||
        !plannedIssues.some((item) => !item.missingRequestLink) ||
        plannedIssues.some(
          (item) => item.issue.id !== null && item.missingRequestLink,
        ))
    )
      throw new UnverifiedSeedMeeting(
        `Existing meeting cannot be verified against the linked seed issues: ${source.sourceRef}`,
      );
    for (const resolution of source.summary.resolutions) {
      const issue = issueByRef.get(resolution.issueRef)!;
      const statusId = context.statusId(resolution.issueStatusCode);
      const saved =
        summary && issue.id !== null
          ? match(
              await tx.issueResolves.findMany({
                where: { meetingSummaryId: summary.id, issueId: issue.id },
                include: { issueResolveAgencies: true },
              }),
              `resolution: ${resolution.sourceRef}`,
              context.finalizeSeedMeetings,
            )
          : null;
      if (saved) {
        if (
          context.finalizeSeedMeetings &&
          (saved.deletedAt || saved.userId !== userId)
        )
          throw new UnverifiedSeedMeeting(
            `Deleted or unverified resolution: ${resolution.sourceRef}`,
          );
        conflict(
          Boolean(saved.deletedAt) ||
            saved.userId !== userId ||
            (!context.finalizeSeedMeetings && saved.issueStatusId !== statusId),
          `resolution: ${resolution.sourceRef}`,
        );
        const [kh] = await tx.$queryRaw<
          { rgcDecisionKh: Prisma.JsonValue }[]
        >`SELECT rgc_decision_kh AS "rgcDecisionKh" FROM issue_resolves WHERE id = ${saved.id}`;
        const current = { ...saved, ...kh };
        for (const key of [
          'rgcDecision',
          'rgcDecisionKh',
          'nextStep',
          'documentReference',
        ] as const)
          if (
            !context.preserveExistingResolutions &&
            !context.finalizeSeedMeetings &&
            Object.hasOwn(resolution, key)
          )
            conflict(
              !context.sameJson(resolution[key], current[key]),
              `resolution ${key}: ${resolution.sourceRef}`,
            );
      }
      const existingAgencies = new Set(
        saved?.issueResolveAgencies.map((agency) => agency.stakeholderId) ?? [],
      );
      resolutions.push({
        source: resolution,
        issue,
        id: saved?.id ?? null,
        ministryId,
        userId,
        statusId,
        missingAgencyIds:
          saved &&
          (context.preserveExistingResolutions || context.finalizeSeedMeetings)
            ? []
            : issue.agencies
                .map((agency) => agency.stakeholderId)
                .filter((id) => !existingAgencies.has(id)),
      });
    }
    if (
      context.finalizeSeedMeetings &&
      summary &&
      resolutions.length &&
      !resolutions.some((resolution) => resolution.id !== null)
    )
      throw new UnverifiedSeedMeeting(
        `Summary has no verified issue resolutions: ${source.sourceRef}`,
      );
    if (
      context.finalizeSeedMeetings &&
      source.meetingDate &&
      meeting?.meetingDate &&
      meeting.meetingDate.toISOString().slice(0, 10) !== source.meetingDate
    )
      throw new UnverifiedSeedMeeting(
        `Existing meeting has a different calendar date: ${source.sourceRef}`,
      );
    plans.push({
      renameMeeting,
      calendarUpdate: Boolean(
        context.finalizeSeedMeetings &&
        source.meetingDate &&
        (!meeting || !meeting.meetingDate || meeting.status !== 'COMPLETED'),
      ),
      finalize: Boolean(context.finalizeSeedMeetings),
      submitRequest: Boolean(
        context.finalizeSeedMeetings && request?.status === 'DRAFT',
      ),
      submittedAt: request?.submittedAt ?? null,
      completeSummary: Boolean(
        context.finalizeSeedMeetings &&
        summary &&
        summary.meetingSummaryStatusId !== target.id,
      ),
      shareSummary: Boolean(
        context.finalizeSeedMeetings && summary && !summary.share,
      ),
      source,
      requestId: request?.id ?? null,
      previousRequestTitle:
        !context.finalizeSeedMeetings &&
        request &&
        request.status === 'DRAFT' &&
        source.request.previousTitle === request.title
          ? request.title
          : null,
      previousRequestDescription:
        !context.finalizeSeedMeetings &&
        request &&
        request.status === 'DRAFT' &&
        source.request.previousDescription === request.description &&
        request.description !== source.request.description
          ? request.description
          : null,
      meetingId: meeting?.id ?? null,
      summaryId: summary?.id ?? null,
      userId,
      ministryId,
      requestOwnerId: owner.userId,
      summaryStatusId: target.id,
      issues: plannedIssues,
      missingRequestAgency: !request?.governmentAgencies.some(
        (agency) => agency.stakeholderId === ministryId,
      ),
      missingMeetingLink: !meetingLink,
      missingSummaryLink: !summaryLink,
      resolutions,
    });
  }
  return plans;
}

const json = (value: Prisma.JsonValue | undefined) =>
  value == null ? Prisma.DbNull : value;

// Only missing links are filled, except explicitly approved request text changes.
export async function applyMeetingPlan(
  tx: Prisma.TransactionClient,
  plans: MeetingPlan[],
) {
  for (const plan of plans) {
    const source = plan.source;
    if (plan.renameMeeting && plan.meetingId !== null) {
      const changed = await tx.meeting.updateMany({
        where: {
          id: plan.meetingId,
          userId: plan.userId,
          deletedAt: null,
          meetingRequestId: plan.requestId,
          title: source.previousTitle,
          description: source.previousDescription,
        },
        data: { title: source.title, description: source.description },
      });
      if (changed.count !== 1)
        throw new Error('Meeting text changed; preview again');
    }
    if (plan.calendarUpdate && plan.meetingId !== null && source.meetingDate) {
      const date = new Date(`${source.meetingDate}T00:00:00.000Z`);
      const changed = await tx.meeting.updateMany({
        where: {
          id: plan.meetingId,
          deletedAt: null,
          OR: [{ meetingDate: null }, { meetingDate: date }],
        },
        data: { status: 'COMPLETED', meetingDate: date },
      });
      if (changed.count !== 1)
        throw new Error('Calendar date changed; preview again');
    }
    if (plan.submitRequest && plan.requestId !== null) {
      const changed = await tx.meetingRequests.updateMany({
        where: { id: plan.requestId, deletedAt: null, status: 'DRAFT' },
        data: {
          status: 'SUBMITTED',
          submittedAt: plan.submittedAt ?? new Date(),
        },
      });
      if (changed.count !== 1)
        throw new Error('Meeting request changed; preview again');
    }
    if (plan.completeSummary && plan.summaryId !== null) {
      const changed = await tx.meetingSummary.updateMany({
        where: {
          id: plan.summaryId,
          deletedAt: null,
          meetingSummaryStatusId: { not: plan.summaryStatusId },
        },
        data: { meetingSummaryStatusId: plan.summaryStatusId },
      });
      if (changed.count !== 1)
        throw new Error('Meeting summary changed; preview again');
    }
    if (plan.shareSummary && plan.summaryId !== null) {
      const changed = await tx.meetingSummary.updateMany({
        where: { id: plan.summaryId, deletedAt: null, share: false },
        data: { share: true, sharedAt: new Date() },
      });
      if (changed.count !== 1)
        throw new Error('Meeting summary sharing changed; preview again');
    }
    if (plan.requestId !== null && plan.previousRequestDescription) {
      const changed = await tx.$executeRaw`UPDATE meeting_requests
        SET description = ${source.request.description}
        WHERE id = ${plan.requestId} AND description = ${plan.previousRequestDescription}
          AND status = 'DRAFT' AND deleted_at IS NULL`;
      if (changed !== 1)
        throw new Error('Meeting request changed; preview again');
    }
    if (plan.requestId !== null && plan.previousRequestTitle) {
      // Preserve timestamps and every other value; reject stale or changed records.
      const changed = await tx.$executeRaw`UPDATE meeting_requests
        SET title = ${source.request.title}
        WHERE id = ${plan.requestId} AND title = ${plan.previousRequestTitle}
          AND status = 'DRAFT' AND deleted_at IS NULL`;
      if (changed !== 1)
        throw new Error('Meeting request changed; preview again');
    }
    if (plan.requestId === null)
      plan.requestId = (
        await tx.meetingRequests.create({
          data: {
            title: source.request.title,
            description: source.request.description,
            meetingRequestLetter: source.request.meetingRequestLetter ?? '',
            status: plan.finalize ? 'SUBMITTED' : 'DRAFT',
            ...(plan.finalize ? { submittedAt: new Date() } : {}),
            userId: plan.requestOwnerId,
          },
          select: { id: true },
        })
      ).id;
    if (plan.missingRequestAgency)
      await tx.governmentAgencyMeetingRequest.create({
        data: {
          meetingRequestId: plan.requestId,
          stakeholderId: plan.ministryId,
        },
      });
    for (const item of plan.issues) {
      if (item.missingRequestLink)
        await tx.issueMeetingRequest.create({
          data: { issueId: item.issue.id!, meetingRequestId: plan.requestId },
        });
      if (item.setPrimary) {
        const result = await tx.issues.updateMany({
          where: {
            id: item.issue.id!,
            meetingRequestId: null,
            deletedAt: null,
          },
          data: {
            meetingRequestId: plan.requestId,
            updatedAt: item.issue.updatedAt ?? new Date(),
          },
        });
        if (result.count !== 1)
          throw new Error(
            'Issue meeting-request link changed; preview the batch again',
          );
        item.issue.primaryRequestId = plan.requestId;
      }
    }
    if (plan.meetingId === null)
      plan.meetingId = (
        await tx.meeting.create({
          data: {
            title: source.title,
            description: source.description,
            meetingDate: source.meetingDate
              ? new Date(`${source.meetingDate}T00:00:00.000Z`)
              : null,
            startTime: source.startTime
              ? new Date(`1970-01-01T${source.startTime}:00.000Z`)
              : null,
            location: source.location,
            documentReference: source.documentReference,
            meetingRequestId: plan.requestId,
            userId: plan.userId,
            status: plan.calendarUpdate ? 'COMPLETED' : 'DRAFT',
          },
          select: { id: true },
        })
      ).id;
    if (plan.missingMeetingLink)
      await tx.meetingMeetingRequest.create({
        data: { meetingId: plan.meetingId, meetingRequestId: plan.requestId },
      });
    if (plan.summaryId === null)
      plan.summaryId = (
        await tx.meetingSummary.create({
          data: {
            meetingRequestId: plan.requestId,
            meetingId: plan.meetingId,
            documentReference: source.summary.documentReference,
            userId: plan.userId,
            meetingSummaryStatusId: plan.summaryStatusId,
            share: plan.finalize,
            ...(plan.finalize ? { sharedAt: new Date() } : {}),
          },
          select: { id: true },
        })
      ).id;
    if (plan.missingSummaryLink)
      await tx.meetingSummaryMeeting.create({
        data: { meetingSummaryId: plan.summaryId, meetingId: plan.meetingId },
      });
    for (const resolution of plan.resolutions) {
      if (resolution.id === null) {
        resolution.id = (
          await tx.issueResolves.create({
            data: {
              meetingSummaryId: plan.summaryId,
              issueId: resolution.issue.id!,
              issueStatusId: resolution.statusId,
              userId: resolution.userId,
              rgcDecision: json(resolution.source.rgcDecision),
              nextStep: json(resolution.source.nextStep),
              documentReference: resolution.source.documentReference,
            },
            select: { id: true },
          })
        ).id;
        if (resolution.source.rgcDecisionKh != null)
          await tx.$executeRaw`UPDATE issue_resolves SET rgc_decision_kh = ${JSON.stringify(resolution.source.rgcDecisionKh)}::jsonb WHERE id = ${resolution.id}`;
      }
      for (const stakeholderId of resolution.missingAgencyIds)
        await tx.issueResolveAgencies.create({
          data: { issueResolveId: resolution.id, stakeholderId },
        });
    }
  }
}
