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

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

import { CreateSystemNotificationDto } from './dto/create-system-notification.dto';
import { GetSystemNotificationsQueryDto } from './dto/get-system-notifications-query.dto';

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

  async create(dto: CreateSystemNotificationDto) {
    if (!dto.receiverUserId && !dto.receiverStakeholderId) {
      throw new BadRequestException(
        'receiverUserId or receiverStakeholderId is required.',
      );
    }

    return this.prisma.systemNotification.create({
      data: {
        title: dto.title,
        message: dto.message,
        type: dto.type ?? 'MEETING_REQUEST',
        senderUserId: dto.senderUserId,
        receiverUserId: dto.receiverUserId,
        receiverStakeholderId: dto.receiverStakeholderId,
        meetingRequestId: dto.meetingRequestId,
        ...(dto.data !== undefined ? { data: dto.data } : {}),
      },
    });
  }

  async createForMeetingRequest(params: {
    meetingRequestId: number;
    meetingRequestTitle: string;
    senderUserId: number;
    senderName: string;
    receiverStakeholderIds: number[];
  }) {
    const receiverStakeholderIds = Array.from(
      new Set(
        params.receiverStakeholderIds
          .map((id) => Number(id))
          .filter((id) => Number.isInteger(id) && id > 0),
      ),
    );

    if (receiverStakeholderIds.length === 0) {
      return {
        created: 0,
        skipped: 0,
      };
    }

    const senderName =
      params.senderName.trim().slice(0, 180) || 'Private Sector';

    const meetingRequestTitle =
      params.meetingRequestTitle.trim() || 'Meeting Request';

    const existingNotifications = await this.prisma.systemNotification.findMany(
      {
        where: {
          deletedAt: null,
          type: 'MEETING_REQUEST',
          meetingRequestId: params.meetingRequestId,
          receiverStakeholderId: {
            in: receiverStakeholderIds,
          },
        },
        select: {
          id: true,
          receiverStakeholderId: true,
        },
      },
    );

    for (const notification of existingNotifications) {
      await this.prisma.systemNotification.update({
        where: {
          id: notification.id,
        },
        data: {
          title: senderName,
          message: meetingRequestTitle,
          senderUserId: params.senderUserId,
          isRead: false,
          readAt: null,
          data: {
            notificationLabel: 'New Meeting Request',
            senderName,
            meetingRequestTitle,
            meetingRequestId: params.meetingRequestId,
            url: `/ministry/meeting-requests/${params.meetingRequestId}`,
          },
        },
      });
    }

    const existingStakeholderIds = new Set(
      existingNotifications
        .map((notification) => notification.receiverStakeholderId)
        .filter(
          (stakeholderId): stakeholderId is number =>
            typeof stakeholderId === 'number' && stakeholderId > 0,
        ),
    );

    const stakeholderIdsToCreate = receiverStakeholderIds.filter(
      (stakeholderId) => !existingStakeholderIds.has(stakeholderId),
    );

    if (stakeholderIdsToCreate.length === 0) {
      return {
        created: 0,
        skipped: receiverStakeholderIds.length,
      };
    }

    const result = await this.prisma.systemNotification.createMany({
      data: stakeholderIdsToCreate.map((stakeholderId) => ({
        title: senderName,
        message: meetingRequestTitle,
        type: 'MEETING_REQUEST',
        senderUserId: params.senderUserId,
        receiverStakeholderId: stakeholderId,
        meetingRequestId: params.meetingRequestId,
        data: {
          notificationLabel: 'New Meeting Request',
          senderName,
          meetingRequestTitle,
          meetingRequestId: params.meetingRequestId,
          url: `/ministry/meeting-requests/${params.meetingRequestId}`,
        },
      })),
    });

    return {
      created: result.count,
      skipped: receiverStakeholderIds.length - result.count,
    };
  }

  // Kept as a small wrapper because the initial report notification already
  // has callers and tests. Later workflow steps use the generic method below.
  async createForProgressReportSent(params: {
    progressReportId: number;
    progressReportTitle: string;
    senderUserId: number;
    senderName?: string;
    receiverStakeholderIds: number[];
  }) {
    return this.createForProgressReportWorkflowStep({
      ...params,
      step: 'INITIAL_REPORT',
    });
  }

  async createForProgressReportWorkflowStep(params: {
    progressReportId: number;
    progressReportTitle: string;
    senderUserId: number;
    senderName?: string;
    receiverStakeholderIds: number[];
    step:
      | 'INITIAL_REPORT'
      | 'FIRST_MEETING'
      | 'SECOND_DEADLINE'
      | 'SECOND_MEETING'
      | 'FINAL_DEADLINE';
  }) {
    const progressReportId = Number(params.progressReportId);

    const receiverStakeholderIds = Array.from(
      new Set(
        params.receiverStakeholderIds
          .map((id) => Number(id))
          .filter((id) => Number.isInteger(id) && id > 0),
      ),
    );

    if (
      !Number.isInteger(progressReportId) ||
      progressReportId <= 0 ||
      receiverStakeholderIds.length === 0
    ) {
      return { created: 0, updated: 0, skipped: receiverStakeholderIds.length };
    }

    // A progress report is an institutional document, not a personal
    // message, so default to the sending body's name rather than a person.
    const senderName = params.senderName?.trim().slice(0, 180) || 'CDC-GPSF';

    const progressReportTitle =
      params.progressReportTitle.trim().slice(0, 180) || 'Progress Report';

    const stepMessages = {
      INITIAL_REPORT: {
        label: 'Progress Report Sent',
        message: `New progress report sent: ${progressReportTitle}`,
      },
      FIRST_MEETING: {
        label: 'First Progress Report Meeting',
        message: `First meeting scheduled for: ${progressReportTitle}`,
      },
      SECOND_DEADLINE: {
        label: 'Second Progress Report Deadline',
        message: `Second deadline sent for: ${progressReportTitle}`,
      },
      SECOND_MEETING: {
        label: 'Second Progress Report Meeting',
        message: `Second meeting scheduled for: ${progressReportTitle}`,
      },
      FINAL_DEADLINE: {
        label: 'Final Progress Report Deadline',
        message: `Final deadline sent for: ${progressReportTitle}`,
      },
    } as const;
    const stepMessage = stepMessages[params.step];
    const notificationData = {
      notificationLabel: stepMessage.label,
      senderName,
      progressReportId,
      progressReportTitle,
      workflowStep: params.step,
      url: `/ministry/progress-reports/${progressReportId}`,
    };

    const message = stepMessage.message;

    // There is no dedicated progressReportId column on this model, so dedup
    // looks inside the stored `data` JSON, same as createForIssueEscalated.
    const existingNotifications = await this.prisma.systemNotification.findMany(
      {
        where: {
          deletedAt: null,
          type: 'PROGRESS_REPORT_SENT',
          receiverStakeholderId: {
            in: receiverStakeholderIds,
          },
        },
        select: {
          id: true,
          receiverStakeholderId: true,
          data: true,
        },
      },
    );

    const matchingNotifications = existingNotifications.filter(
      (notification) =>
        this.getJsonNumber(notification.data, 'progressReportId') ===
          progressReportId &&
        // Older initial-report notifications do not have workflowStep yet.
        (this.getJsonString(notification.data, 'workflowStep') ===
          params.step ||
          (params.step === 'INITIAL_REPORT' &&
            this.getJsonString(notification.data, 'workflowStep') === null)),
    );

    await Promise.all(
      matchingNotifications.map((notification) =>
        this.prisma.systemNotification.update({
          where: { id: notification.id },
          data: {
            title: senderName,
            message,
            senderUserId: params.senderUserId,
            isRead: false,
            readAt: null,
            data: notificationData,
          },
        }),
      ),
    );

    const existingStakeholderIds = new Set(
      matchingNotifications
        .map((notification) => notification.receiverStakeholderId)
        .filter(
          (stakeholderId): stakeholderId is number =>
            typeof stakeholderId === 'number' && stakeholderId > 0,
        ),
    );

    const stakeholderIdsToCreate = receiverStakeholderIds.filter(
      (stakeholderId) => !existingStakeholderIds.has(stakeholderId),
    );

    if (stakeholderIdsToCreate.length === 0) {
      return {
        created: 0,
        updated: matchingNotifications.length,
        skipped: 0,
      };
    }

    const result = await this.prisma.systemNotification.createMany({
      data: stakeholderIdsToCreate.map((stakeholderId) => ({
        title: senderName,
        message,
        type: 'PROGRESS_REPORT_SENT',
        senderUserId: params.senderUserId,
        receiverStakeholderId: stakeholderId,
        data: notificationData,
      })),
    });

    return {
      created: result.count,
      updated: matchingNotifications.length,
      skipped:
        receiverStakeholderIds.length -
        result.count -
        matchingNotifications.length,
    };
  }

  async createForPswgMeetingScheduled(params: {
    meetingId: number;
    meetingRequestId: number;
    meetingTitle: string;
    senderUserId: number;
    senderName: string;
    receiverUserId: number;
    meetingDate?: Date | null;
    startTime?: Date | null;
    endTime?: Date | null;
    location?: string | null;
  }) {
    return this.createForMeetingScheduled({
      ...params,
      url: `/pswg/meeting-requests?view=calendar&meetingId=${params.meetingId}`,
    });
  }

  async createForCdcGpsfMeetingScheduled(params: {
    meetingId: number;
    meetingRequestId?: number | null;
    meetingTitle: string;
    senderUserId: number;
    senderName: string;
    receiverUserId: number;
    meetingDate?: Date | null;
    startTime?: Date | null;
    endTime?: Date | null;
    location?: string | null;
  }) {
    return this.createForMeetingScheduled({
      ...params,
      url: '/cdc-gpsf/meeting-calendar',
    });
  }

  private async createForMeetingScheduled(params: {
    meetingId: number;
    meetingRequestId?: number | null;
    meetingTitle: string;
    senderUserId: number;
    senderName: string;
    receiverUserId: number;
    meetingDate?: Date | null;
    startTime?: Date | null;
    endTime?: Date | null;
    location?: string | null;
    url: string;
  }) {
    const meetingId = Number(params.meetingId);
    const meetingRequestId = Number(params.meetingRequestId);
    const receiverUserId = Number(params.receiverUserId);
    const hasMeetingRequest =
      Number.isInteger(meetingRequestId) && meetingRequestId > 0;

    if (
      !Number.isInteger(meetingId) ||
      meetingId <= 0 ||
      !Number.isInteger(receiverUserId) ||
      receiverUserId <= 0
    ) {
      return {
        created: false,
        updated: false,
        reason: 'Invalid meeting or recipient user ID.',
      };
    }

    const senderName = params.senderName.trim().slice(0, 180) || 'Ministry';

    const meetingTitle =
      params.meetingTitle.trim().slice(0, 180) || 'Scheduled Meeting';

    const meetingDate = params.meetingDate
      ? params.meetingDate.toISOString().slice(0, 10)
      : null;

    const startTime = params.startTime
      ? params.startTime.toISOString().slice(11, 16)
      : null;

    const endTime = params.endTime
      ? params.endTime.toISOString().slice(11, 16)
      : null;

    const location = params.location?.trim() || null;

    const timeText =
      startTime && endTime
        ? `${startTime} - ${endTime}`
        : startTime || endTime || null;

    const detailParts = [meetingDate, timeText, location].filter(
      (value): value is string => Boolean(value),
    );

    const detailText =
      detailParts.length > 0 ? ` (${detailParts.join(' • ')})` : '';

    const notificationData = {
      notificationLabel: 'Meeting Scheduled',
      senderName,
      meetingId,
      meetingRequestId: hasMeetingRequest ? meetingRequestId : null,
      meetingTitle,
      meetingDate,
      startTime,
      endTime,
      location,
      url: params.url,
    };

    const possibleDuplicates = await this.prisma.systemNotification.findMany({
      where: {
        deletedAt: null,
        type: 'MEETING_SCHEDULED',
        receiverUserId,
        ...(hasMeetingRequest ? { meetingRequestId } : {}),
      },
      select: {
        id: true,
        data: true,
      },
    });

    const existingNotification = possibleDuplicates.find(
      (notification) =>
        this.getJsonNumber(notification.data, 'meetingId') === meetingId,
    );

    if (existingNotification) {
      const notification = await this.prisma.systemNotification.update({
        where: {
          id: existingNotification.id,
        },
        data: {
          title: senderName,
          message: `Meeting scheduled: ${meetingTitle}${detailText}`,
          senderUserId: params.senderUserId,
          isRead: false,
          readAt: null,
          data: notificationData,
        },
      });

      return {
        created: false,
        updated: true,
        notification,
      };
    }

    const notification = await this.prisma.systemNotification.create({
      data: {
        title: senderName,
        message: `Meeting scheduled: ${meetingTitle}${detailText}`,
        type: 'MEETING_SCHEDULED',
        senderUserId: params.senderUserId,
        receiverUserId,
        meetingRequestId: hasMeetingRequest ? meetingRequestId : null,
        data: notificationData,
      },
    });

    return {
      created: true,
      updated: false,
      notification,
    };
  }

  // Notify a PSWG working group that a ministry shared a meeting summary with
  // them. One notification per active recipient, de-duplicated per summary.
  async createForMeetingSummaryShared(params: {
    meetingSummaryId: number;
    meetingRequestId: number;
    summaryTitle: string;
    senderUserId: number;
    senderName: string;
    receiverUserIds: number[];
  }) {
    const meetingSummaryId = Number(params.meetingSummaryId);
    const meetingRequestId = Number(params.meetingRequestId);

    const receiverUserIds = Array.from(
      new Set(
        params.receiverUserIds.filter((id) => Number.isInteger(id) && id > 0),
      ),
    );

    if (
      !Number.isInteger(meetingSummaryId) ||
      meetingSummaryId <= 0 ||
      receiverUserIds.length === 0
    ) {
      return { created: 0 };
    }

    const senderName = params.senderName.trim().slice(0, 180) || 'Ministry';
    const summaryTitle =
      params.summaryTitle.trim().slice(0, 180) || 'Meeting Summary';

    const notificationData = {
      notificationLabel: 'Meeting Summary Shared',
      senderName,
      meetingSummaryId,
      meetingRequestId,
      url: `/pswg/meeting-summary/${meetingSummaryId}`,
    };

    // Skip recipients that already have a notification for this summary.
    const existing = await this.prisma.systemNotification.findMany({
      where: {
        deletedAt: null,
        type: 'MEETING_SUMMARY_SHARED',
        receiverUserId: { in: receiverUserIds },
      },
      select: { receiverUserId: true, data: true },
    });

    const alreadyNotified = new Set(
      existing
        .filter(
          (item) =>
            this.getJsonNumber(item.data, 'meetingSummaryId') ===
            meetingSummaryId,
        )
        .map((item) => item.receiverUserId),
    );

    const targets = receiverUserIds.filter((id) => !alreadyNotified.has(id));

    if (targets.length === 0) {
      return { created: 0 };
    }

    const result = await this.prisma.systemNotification.createMany({
      data: targets.map((receiverUserId) => ({
        title: senderName,
        message: `Meeting summary shared: ${summaryTitle}`,
        type: 'MEETING_SUMMARY_SHARED',
        senderUserId: params.senderUserId,
        receiverUserId,
        meetingRequestId,
        data: notificationData,
      })),
    });

    return { created: result.count };
  }

  async createForCdcGpsfMeetingSummarySubmitted(params: {
    meetingSummaryId: number;
    meetingRequestId: number;
    summaryTitle: string;
    senderUserId: number;
    senderName: string;
    receiverUserIds: number[];
  }) {
    const meetingSummaryId = Number(params.meetingSummaryId);
    const meetingRequestId = Number(params.meetingRequestId);
    const receiverUserIds = Array.from(
      new Set(
        params.receiverUserIds.filter((id) => Number.isInteger(id) && id > 0),
      ),
    );

    if (
      !Number.isInteger(meetingSummaryId) ||
      meetingSummaryId <= 0 ||
      !Number.isInteger(meetingRequestId) ||
      meetingRequestId <= 0 ||
      receiverUserIds.length === 0
    ) {
      return { created: 0, updated: 0 };
    }

    const senderName = params.senderName.trim().slice(0, 180) || 'Ministry';
    const summaryTitle =
      params.summaryTitle.trim().slice(0, 180) || 'Meeting Summary';
    const notificationData = {
      notificationLabel: 'Meeting Summary Submitted',
      senderName,
      meetingSummaryId,
      meetingRequestId,
      summaryTitle,
      url: `/cdc-gpsf/meeting-summary/${meetingSummaryId}`,
    };

    const existingNotifications = await this.prisma.systemNotification.findMany(
      {
        where: {
          deletedAt: null,
          type: 'MEETING_SUMMARY_SUBMITTED',
          receiverUserId: { in: receiverUserIds },
        },
        select: {
          id: true,
          receiverUserId: true,
          data: true,
        },
      },
    );

    const matchingNotifications = existingNotifications.filter(
      (notification) =>
        this.getJsonNumber(notification.data, 'meetingSummaryId') ===
        meetingSummaryId,
    );

    await Promise.all(
      matchingNotifications.map((notification) =>
        this.prisma.systemNotification.update({
          where: { id: notification.id },
          data: {
            title: senderName,
            message: `Meeting summary submitted: ${summaryTitle}`,
            senderUserId: params.senderUserId,
            isRead: false,
            readAt: null,
            data: notificationData,
          },
        }),
      ),
    );

    const existingRecipientIds = new Set(
      matchingNotifications
        .map((notification) => notification.receiverUserId)
        .filter(
          (receiverUserId): receiverUserId is number =>
            typeof receiverUserId === 'number' && receiverUserId > 0,
        ),
    );
    const recipientIdsToCreate = receiverUserIds.filter(
      (receiverUserId) => !existingRecipientIds.has(receiverUserId),
    );

    const result = await this.prisma.systemNotification.createMany({
      data: recipientIdsToCreate.map((receiverUserId) => ({
        title: senderName,
        message: `Meeting summary submitted: ${summaryTitle}`,
        type: 'MEETING_SUMMARY_SUBMITTED',
        senderUserId: params.senderUserId,
        receiverUserId,
        meetingRequestId,
        data: notificationData,
      })),
    });

    return {
      created: result.count,
      updated: matchingNotifications.length,
    };
  }

  // Notify each CDC user when the ministry escalates an issue to CDC. Shown in
  // the notification bell only (no navigation target). De-duplicated per
  // (escalated issue + summary + receiver) so re-saving the resolution does not
  // create duplicate notifications.
  async createForIssueEscalated(params: {
    issueId: number;
    issueTitle: string;
    meetingSummaryId: number;
    escalateName: string;
    senderUserId: number;
    senderName: string;
    receiverUserIds: number[];
  }) {
    const issueId = Number(params.issueId);
    const meetingSummaryId = Number(params.meetingSummaryId);

    const receiverUserIds = Array.from(
      new Set(
        params.receiverUserIds.filter((id) => Number.isInteger(id) && id > 0),
      ),
    );

    if (
      !Number.isInteger(issueId) ||
      issueId <= 0 ||
      receiverUserIds.length === 0
    ) {
      return { created: 0 };
    }

    const senderName = params.senderName.trim().slice(0, 180) || 'Ministry';
    const escalateName = params.escalateName.trim().slice(0, 50) || 'CDC';
    const issueTitle = params.issueTitle.trim().slice(0, 180) || 'an issue';

    const notificationData = {
      notificationLabel: 'Issue Escalated',
      senderName,
      issueId,
      issueTitle,
      meetingSummaryId,
      escalateName,
    };

    // Skip recipients who already have a notification for this escalated issue.
    const existing = await this.prisma.systemNotification.findMany({
      where: {
        deletedAt: null,
        type: 'ISSUE_ESCALATED',
        receiverUserId: { in: receiverUserIds },
      },
      select: { receiverUserId: true, data: true },
    });

    const alreadyNotified = new Set(
      existing
        .filter(
          (item) =>
            this.getJsonNumber(item.data, 'issueId') === issueId &&
            this.getJsonNumber(item.data, 'meetingSummaryId') ===
              meetingSummaryId,
        )
        .map((item) => item.receiverUserId),
    );

    const targets = receiverUserIds.filter((id) => !alreadyNotified.has(id));

    if (targets.length === 0) {
      return { created: 0 };
    }

    const result = await this.prisma.systemNotification.createMany({
      data: targets.map((receiverUserId) => ({
        title: `Issue escalated to ${escalateName}`,
        message: `${senderName} escalated the issue "${issueTitle}" to ${escalateName}.`,
        type: 'ISSUE_ESCALATED',
        senderUserId: params.senderUserId,
        receiverUserId,
        data: notificationData,
      })),
    });

    return { created: result.count };
  }

  async findAllForCurrentUser(
    userId: number,
    query: GetSystemNotificationsQueryDto,
  ) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;

    const where = await this.buildCurrentUserWhere(userId, {
      type: query.type,
      isRead: query.isRead,
    });

    const unreadWhere = await this.buildCurrentUserWhere(userId, {
      isRead: false,
    });

    const [items, total, unreadCount] = await this.prisma.$transaction([
      this.prisma.systemNotification.findMany({
        where,
        orderBy: {
          createdAt: 'desc',
        },
        skip,
        take: limit,
      }),

      this.prisma.systemNotification.count({
        where,
      }),

      this.prisma.systemNotification.count({
        where: unreadWhere,
      }),
    ]);

    return {
      data: items,
      meta: {
        page,
        limit,
        total,
        unreadCount,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  async unreadCountForCurrentUser(userId: number) {
    const where = await this.buildCurrentUserWhere(userId, {
      isRead: false,
    });

    const count = await this.prisma.systemNotification.count({
      where,
    });

    return {
      count,
    };
  }

  async findOneForCurrentUser(id: number, userId: number) {
    const notificationScope = await this.buildCurrentUserWhere(userId, {});

    const notification = await this.prisma.systemNotification.findFirst({
      where: {
        AND: [
          notificationScope,
          {
            id,
          },
        ],
      },
    });

    if (!notification) {
      throw new NotFoundException('Notification not found.');
    }

    return notification;
  }

  async markAsReadForCurrentUser(id: number, userId: number) {
    const notification = await this.findOneForCurrentUser(id, userId);

    return this.prisma.systemNotification.update({
      where: {
        id: notification.id,
      },
      data: {
        isRead: true,
        readAt: new Date(),
      },
    });
  }

  async markAllAsReadForCurrentUser(userId: number) {
    const where = await this.buildCurrentUserWhere(userId, {
      isRead: false,
    });

    const result = await this.prisma.systemNotification.updateMany({
      where,
      data: {
        isRead: true,
        readAt: new Date(),
      },
    });

    return {
      updated: result.count,
    };
  }

  async softDeleteForCurrentUser(id: number, userId: number) {
    const notification = await this.findOneForCurrentUser(id, userId);

    return this.prisma.systemNotification.update({
      where: {
        id: notification.id,
      },
      data: {
        deletedAt: new Date(),
      },
    });
  }

  private async buildCurrentUserWhere(
    userId: number,
    filters: Pick<GetSystemNotificationsQueryDto, 'type' | 'isRead'>,
  ): Promise<Prisma.SystemNotificationWhereInput> {
    const validUserId = Number(userId);

    if (!Number.isInteger(validUserId) || validUserId <= 0) {
      throw new BadRequestException('Invalid authenticated user ID.');
    }

    const stakeholderLinks = await this.prisma.stakeholderUser.findMany({
      where: {
        userId: validUserId,
      },
      select: {
        stakeholderId: true,
      },
    });

    const stakeholderIds = Array.from(
      new Set(
        stakeholderLinks
          .map((link) => Number(link.stakeholderId))
          .filter((id) => Number.isInteger(id) && id > 0),
      ),
    );

    const receiverFilters: Prisma.SystemNotificationWhereInput[] = [
      {
        receiverUserId: validUserId,
      },
    ];

    if (stakeholderIds.length > 0) {
      receiverFilters.push({
        receiverStakeholderId: {
          in: stakeholderIds,
        },
      });
    }

    const whereFilters: Prisma.SystemNotificationWhereInput[] = [
      {
        deletedAt: null,
      },
      {
        OR: receiverFilters,
      },
    ];

    if (filters.type?.trim()) {
      whereFilters.push({
        type: filters.type.trim(),
      });
    }

    if (typeof filters.isRead === 'boolean') {
      whereFilters.push({
        isRead: filters.isRead,
      });
    }

    return {
      AND: whereFilters,
    };
  }

  private getJsonNumber(data: unknown, key: string): number | null {
    if (!data || typeof data !== 'object' || Array.isArray(data)) {
      return null;
    }

    const value = (data as Record<string, unknown>)[key];
    const parsedNumber = Number(value);

    return Number.isInteger(parsedNumber) && parsedNumber > 0
      ? parsedNumber
      : null;
  }

  private getJsonString(data: unknown, key: string): string | null {
    if (!data || typeof data !== 'object' || Array.isArray(data)) {
      return null;
    }

    const value = (data as Record<string, unknown>)[key];
    return typeof value === 'string' ? value : null;
  }
}
