import {
  HttpStatus,
  Injectable,
} from '@nestjs/common';
import { AppException, ErrorCode } from '../../common/errors';
import { LabCaseActivityType, LabTaskStatus, LinkStatus, Prisma, UserNotificationType } from '@prisma/client';
import { createReadStream, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { randomUUID } from 'crypto';
import {
  buildLabCaseShareUrl,
  generateLabCaseAccessToken,
} from '../../common/lab-case-access-token';
import { PrismaService } from '../../../prisma/prisma.service';
import { generateLabCaseTasks } from '../cases/lab-case-task.generator';
import { ProsthesisCatalogService } from '../prosthesis-catalog/prosthesis-catalog.service';
import { TreatmentCatalogService } from '../treatment-catalog/treatment-catalog.service';
import {
  SaveTreatmentDraftDto,
  SaveTreatmentLabCasesDto,
  UpdateLabCaseDueDateDto,
  CreateStandaloneTreatmentDto,
} from './dto/treatment.dto';
import {
  isLabCaseFullyCompleted,
  isLabCaseOverdue,
  parseDueDateInput,
} from '../../common/lab-case-due-date';
import { CLINIC_TREATMENT_TAB_ACTIVITY_TYPES } from '../../common/lab-case-activity';
import { LabCaseActivityService } from '../notifications/lab-case-activity.service';
import { UserNotificationService } from '../notifications/user-notification.service';
import {
  generateTreatmentTitle,
  normalizeTeeth,
  normalizeToothSelectionGroups,
} from './treatment.utils';
import { assertCompleteToothProsthesisMap } from './lab-case-send.validation';
import { hasEffectivePermission } from '../../common/membership-permissions';
import {
  isActorTreatmentProvider,
  treatmentProviderScopeWhere,
} from '../../common/treatment-provider-scope';
import { ensureWalkInPatient } from '../../common/walk-in-patient';

const sentLabCaseInclude = {
  treatment: {
    select: {
      id: true,
      appointmentId: true,
      treatmentAt: true,
      patientId: true,
      patient: { select: { id: true, firstName: true, lastName: true } },
    },
  },
  details: {
    include: {
      detail: {
        select: { clientKey: true, treatmentType: true, teeth: true, toothSelectionGroups: true },
      },
    },
  },
  sends: {
    orderBy: [{ sentAt: 'asc' as const }],
    include: { organization: { select: { id: true, name: true } } },
  },
  tasks: { select: { status: true, selectionGroupId: true, prosthesisTypeCode: true, teeth: true } },
  toothProsthesis: {
    select: { tooth: true, prosthesisTypeCode: true, selectionGroupId: true },
  },
} satisfies Prisma.LabCaseInclude;

type SentLabCaseRow = Prisma.LabCaseGetPayload<{ include: typeof sentLabCaseInclude }>;

const treatmentInclude = {
  patient: {
    select: { id: true, firstName: true, lastName: true, isWalkIn: true, mobile: true, email: true },
  },
  details: {
    orderBy: [{ sortOrder: 'asc' as const }],
    include: {
      attachments: { orderBy: [{ createdAt: 'asc' as const }] },
      labCaseLink: {
        include: {
          labCase: {
            include: {
              sends: {
                orderBy: [{ sentAt: 'asc' as const }],
                include: { organization: { select: { id: true, name: true } } },
              },
              tasks: { select: { id: true, status: true } },
            },
          },
        },
      },
    },
  },
  labCases: {
    orderBy: [{ sortOrder: 'asc' as const }],
    include: {
      details: {
        include: {
          detail: {
            select: {
              id: true,
              clientKey: true,
              treatmentType: true,
              teeth: true,
              toothSelectionGroups: true,
            },
          },
        },
      },
      sends: {
        orderBy: [{ sentAt: 'asc' as const }],
        include: { organization: { select: { id: true, name: true } } },
      },
      toothProsthesis: true,
      attachments: {
        include: {
          attachment: {
            select: { id: true, fileName: true, mimeType: true, sizeBytes: true, createdAt: true },
          },
        },
      },
      tasks: { select: { id: true, status: true } },
    },
  },
};

@Injectable()
export class TreatmentsService {
  private readonly uploadRoot = join(process.cwd(), 'uploads', 'treatments');

  constructor(
    private readonly prisma: PrismaService,
    private readonly treatmentCatalog: TreatmentCatalogService,
    private readonly prosthesisCatalog: ProsthesisCatalogService,
    private readonly labCaseActivity: LabCaseActivityService,
    private readonly userNotifications: UserNotificationService,
  ) {}

  getOrganizationIdFromUser(user: { organizationId?: string }) {
    if (!user?.organizationId) {
      throw new AppException(ErrorCode.AUTH_ORG_NOT_SELECTED, HttpStatus.BAD_REQUEST);
    }
    return user.organizationId;
  }

  async listLinkedOrganizations(userId: string, organizationId: string) {
    await this.assertCanReadTreatment(userId, organizationId);

    const [linksA, linksB] = await Promise.all([
      this.prisma.organizationLink.findMany({
        where: { organizationAId: organizationId, status: LinkStatus.ACTIVE },
        include: { organizationB: { select: { id: true, name: true } } },
      }),
      this.prisma.organizationLink.findMany({
        where: { organizationBId: organizationId, status: LinkStatus.ACTIVE },
        include: { organizationA: { select: { id: true, name: true } } },
      }),
    ]);

    const data = [
      ...linksA.map((l) => ({
        id: l.organizationB.id,
        name: l.organizationB.name,
        active: true,
      })),
      ...linksB.map((l) => ({
        id: l.organizationA.id,
        name: l.organizationA.name,
        active: true,
      })),
    ].sort((a, b) => a.name.localeCompare(b.name));

    return { success: true, data };
  }

  async listPatientHistory(
    patientId: string,
    organizationId: string,
    actorUserId: string,
    limit = 20,
  ) {
    await this.assertCanReadTreatment(actorUserId, organizationId);
    await this.ensurePatientExists(patientId);

    const items = await this.prisma.treatment.findMany({
      where: {
        patientId,
        organizationId,
        details: { some: {} },
        ...treatmentProviderScopeWhere(actorUserId),
      },
      include: treatmentInclude,
      orderBy: [{ treatmentAt: 'desc' }, { createdAt: 'desc' }],
      take: Math.min(Math.max(limit, 1), 100),
    });

    return { success: true, data: items.map((t) => this.mapTreatment(t)) };
  }

  async createStandalone(
    dto: CreateStandaloneTreatmentDto,
    organizationId: string,
    actorUserId: string,
  ) {
    await this.assertCanEditTreatment(actorUserId, organizationId);

    const walkIn = Boolean(dto.walkIn);
    if (!walkIn && !dto.patientId) {
      throw new AppException(ErrorCode.TREATMENT_PATIENT_OR_WALK_IN, HttpStatus.BAD_REQUEST);
    }

    const treatmentAt = new Date(dto.treatmentAt);
    if (Number.isNaN(treatmentAt.getTime())) {
      throw new AppException(ErrorCode.VALIDATION_INVALID_REQUEST, HttpStatus.BAD_REQUEST);
    }

    let patientId: string;
    if (walkIn) {
      const sentinel = await ensureWalkInPatient(this.prisma, organizationId);
      patientId = sentinel.id;
    } else {
      await this.ensurePatientInOrg(dto.patientId!, organizationId);
      patientId = dto.patientId!;
    }

    const treatment = await this.prisma.treatment.create({
      data: {
        organizationId,
        patientId,
        appointmentId: null,
        providerUserId: actorUserId,
        title: generateTreatmentTitle([]),
        treatmentAt,
      },
      include: treatmentInclude,
    });

    return { success: true, data: this.mapTreatment(treatment) };
  }

  async deleteStandalone(
    treatmentId: string,
    organizationId: string,
    actorUserId: string,
  ) {
    await this.assertCanEditTreatment(actorUserId, organizationId);
    const treatment = await this.ensureTreatmentProvider(
      treatmentId,
      organizationId,
      actorUserId,
    );

    if (treatment.appointmentId) {
      throw new AppException(ErrorCode.TREATMENT_NOT_STANDALONE, HttpStatus.CONFLICT);
    }

    const detailCount = await this.prisma.treatmentDetail.count({
      where: { treatmentId },
    });
    if (detailCount > 0) {
      throw new AppException(ErrorCode.TREATMENT_HAS_DETAILS, HttpStatus.CONFLICT);
    }

    await this.prisma.treatment.delete({ where: { id: treatmentId } });
    return { success: true };
  }

  async listDayStandalone(
    fromIso: string,
    toIso: string,
    organizationId: string,
    actorUserId: string,
  ) {
    await this.assertCanReadTreatment(actorUserId, organizationId);
    const from = new Date(fromIso);
    const to = new Date(toIso);
    if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || to <= from) {
      throw new AppException(ErrorCode.VALIDATION_INVALID_REQUEST, HttpStatus.BAD_REQUEST);
    }

    const items = await this.prisma.treatment.findMany({
      where: {
        organizationId,
        appointmentId: null,
        treatmentAt: { gte: from, lt: to },
        ...treatmentProviderScopeWhere(actorUserId),
      },
      include: treatmentInclude,
      orderBy: [{ treatmentAt: 'asc' }, { createdAt: 'asc' }],
    });

    return { success: true, data: items.map((t) => this.mapTreatment(t)) };
  }

  async listPatientLabCases(
    patientId: string,
    organizationId: string,
    actorUserId: string,
  ) {
    await this.assertCanReadTreatment(actorUserId, organizationId);
    await this.ensurePatientExists(patientId);

    const labCases = await this.prisma.labCase.findMany({
      where: {
        sentAt: { not: null },
        treatment: {
          patientId,
          organizationId,
          ...treatmentProviderScopeWhere(actorUserId),
        },
      },
      include: sentLabCaseInclude,
      orderBy: [{ sentAt: 'desc' }],
    });

    const summaries = await this.mapSentLabCaseSummaries(
      labCases,
      actorUserId,
      organizationId,
    );
    return { success: true, data: this.sortLabCaseSummaries(summaries) };
  }

  async listUnreadLabCases(organizationId: string, actorUserId: string) {
    await this.assertCanReadTreatment(actorUserId, organizationId);

    const labCases = await this.prisma.labCase.findMany({
      where: {
        sentAt: { not: null },
        treatment: {
          organizationId,
          ...treatmentProviderScopeWhere(actorUserId),
        },
      },
      include: sentLabCaseInclude,
      orderBy: [{ sentAt: 'desc' }],
    });

    const caseIds = labCases.map((row) => row.id);
    const unreadCaseIds = await this.labCaseActivity.unreadCaseIdsInBatch(
      actorUserId,
      organizationId,
      caseIds,
      CLINIC_TREATMENT_TAB_ACTIVITY_TYPES,
      'CLINIC',
    );

    const unreadCases = labCases.filter((row) => unreadCaseIds.has(row.id));
    const summaries = await this.mapSentLabCaseSummaries(
      unreadCases,
      actorUserId,
      organizationId,
      unreadCaseIds,
    );
    return { success: true, data: this.sortLabCaseSummaries(summaries) };
  }

  private async mapSentLabCaseSummaries(
    labCases: SentLabCaseRow[],
    actorUserId: string,
    organizationId: string,
    unreadCaseIdsOverride?: Set<string>,
  ) {
    const caseIds = labCases.map((row) => row.id);
    const [unreadCaseIds, lastActivities] = await Promise.all([
      unreadCaseIdsOverride ??
        (await this.labCaseActivity.unreadCaseIdsInBatch(
          actorUserId,
          organizationId,
          caseIds,
          CLINIC_TREATMENT_TAB_ACTIVITY_TYPES,
          'CLINIC',
        )),
      this.labCaseActivity.getLastActivitiesForCases(caseIds, 'CLINIC'),
    ]);

    return labCases.map((lc) => this.mapSentLabCaseSummary(lc, unreadCaseIds, lastActivities));
  }

  private mapSentLabCaseSummary(
    lc: SentLabCaseRow,
    unreadCaseIds: Set<string>,
    lastActivities: Awaited<
      ReturnType<LabCaseActivityService['getLastActivitiesForCases']>
    >,
  ) {
    const detailLink = lc.details[0];
    const detail = detailLink?.detail;
    const taskProgress = this.mapTaskProgress(lc.tasks);
    const labOrg = lc.sends[lc.sends.length - 1]?.organization ?? null;
    const detailTeeth = detail ? normalizeTeeth(detail.teeth) : [];
    const selectionGroups = normalizeToothSelectionGroups(
      detail?.toothSelectionGroups,
      detailTeeth,
    );
    const connectedGroupIds = new Set(
      selectionGroups.filter((g) => g.kind === 'connected').map((g) => g.groupId),
    );

    const prosthesisByGroup = new Map<
      string,
      { prosthesisTypeCode: string; teeth: string[]; selectionGroupId: string; connected: boolean }
    >();
    for (const row of lc.toothProsthesis ?? []) {
      const selectionGroupId = row.selectionGroupId || `legacy-${row.prosthesisTypeCode}`;
      const key = `${selectionGroupId}::${row.prosthesisTypeCode}`;
      const entry = prosthesisByGroup.get(key) ?? {
        prosthesisTypeCode: row.prosthesisTypeCode,
        teeth: [],
        selectionGroupId,
        connected: connectedGroupIds.has(row.selectionGroupId),
      };
      entry.teeth.push(row.tooth);
      prosthesisByGroup.set(key, entry);
    }
    const prosthesisGroups = [...prosthesisByGroup.values()]
      .map((group) => ({
        prosthesisTypeCode: group.prosthesisTypeCode,
        teeth: [...new Set(group.teeth)].sort(),
        connected: group.connected,
        selectionGroupId: group.selectionGroupId,
      }))
      .sort((a, b) => a.prosthesisTypeCode.localeCompare(b.prosthesisTypeCode));

    const treatment = lc.treatment;
    if (!treatment) {
      throw new AppException(ErrorCode.LAB_CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
    const patient = treatment.patient;

    return {
      labCaseId: lc.id,
      patientId: patient.id,
      patientFirstName: patient.firstName,
      patientLastName: patient.lastName,
      treatmentId: treatment.id,
      appointmentId: treatment.appointmentId,
      treatmentAt: treatment.treatmentAt.toISOString(),
      detailClientId: detail?.clientKey ?? detailLink?.treatmentDetailId ?? '',
      teeth: detailTeeth,
      prosthesisGroups,
      toothCount: prosthesisGroups.length
        ? prosthesisGroups.reduce((sum, group) => sum + group.teeth.length, 0)
        : detailTeeth.length,
      labOrganizationId: labOrg?.id ?? lc.destinationOrganizationId,
      labName: labOrg?.name ?? 'Unknown lab',
      sentAt: lc.sentAt?.toISOString() ?? null,
      dueDate: lc.dueDate?.toISOString() ?? null,
      isOverdue: isLabCaseOverdue(lc.dueDate, lc.tasks),
      taskProgress,
      hasUnread: unreadCaseIds.has(lc.id),
      lastActivity: lastActivities.get(lc.id) ?? null,
    };
  }

  private sortLabCaseSummaries<
    T extends { hasUnread: boolean; lastActivity: { createdAt: string } | null; sentAt: string | null },
  >(items: T[]): T[] {
    return [...items].sort((a, b) => {
      if (a.hasUnread !== b.hasUnread) {
        return a.hasUnread ? -1 : 1;
      }
      const aTime = a.lastActivity?.createdAt ?? a.sentAt ?? '';
      const bTime = b.lastActivity?.createdAt ?? b.sentAt ?? '';
      return bTime.localeCompare(aTime);
    });
  }

  async getDraftForAppointment(
    appointmentId: string,
    organizationId: string,
    actorUserId: string,
    actorLanguage?: string | null,
  ) {
    await this.assertCanReadTreatment(actorUserId, organizationId);
    const appointment = await this.ensureAppointmentProvider(
      appointmentId,
      organizationId,
      actorUserId,
    );

    const treatment = await this.prisma.treatment.findFirst({
      where: {
        appointmentId: appointment.id,
        organizationId,
      },
      include: treatmentInclude,
    });

    if (treatment) {
      await this.ensureSentLabCaseAccessTokens(treatment.labCases);
    }

    return {
      success: true,
      data: treatment ? this.mapTreatment(treatment, actorLanguage) : null,
    };
  }

  async getDraftForTreatment(
    treatmentId: string,
    organizationId: string,
    actorUserId: string,
    actorLanguage?: string | null,
  ) {
    const treatment = await this.ensureTreatmentProvider(treatmentId, organizationId, actorUserId);
    await this.ensureSentLabCaseAccessTokens(treatment.labCases);
    return { success: true, data: this.mapTreatment(treatment, actorLanguage) };
  }

  async saveDraftForAppointment(
    appointmentId: string,
    dto: SaveTreatmentDraftDto,
    organizationId: string,
    actorUserId: string,
    actorLanguage?: string | null,
  ) {
    await this.assertCanEditTreatment(actorUserId, organizationId);
    const appointment = await this.ensureAppointmentProvider(
      appointmentId,
      organizationId,
      actorUserId,
    );

    const existing = await this.prisma.treatment.findFirst({
      where: { appointmentId: appointment.id, organizationId },
      select: { id: true },
    });

    const treatmentId = existing
      ? existing.id
      : (
          await this.prisma.treatment.create({
            data: {
              organizationId,
              patientId: appointment.patientId,
              appointmentId: appointment.id,
              providerUserId: actorUserId,
              title: generateTreatmentTitle([]),
              treatmentAt: appointment.startAt,
            },
            select: { id: true },
          })
        ).id;

    const saved = await this.persistDraftDetails({
      treatmentId,
      organizationId,
      actorUserId,
      dto,
      pendingAppointmentId: appointment.id,
      treatmentAt: appointment.startAt,
      patientId: appointment.patientId,
    });

    return { success: true, data: this.mapTreatment(saved, actorLanguage) };
  }

  async saveDraftForTreatment(
    treatmentId: string,
    dto: SaveTreatmentDraftDto,
    organizationId: string,
    actorUserId: string,
    actorLanguage?: string | null,
  ) {
    await this.assertCanEditTreatment(actorUserId, organizationId);
    const existing = await this.ensureTreatmentProvider(treatmentId, organizationId, actorUserId);

    const saved = await this.persistDraftDetails({
      treatmentId,
      organizationId,
      actorUserId,
      dto,
      pendingAppointmentId: existing.appointmentId,
      pendingTreatmentId: existing.appointmentId ? null : existing.id,
      treatmentAt: existing.appointmentId ? existing.treatmentAt : existing.treatmentAt,
      patientId: existing.patientId,
    });

    return { success: true, data: this.mapTreatment(saved, actorLanguage) };
  }

  private async persistDraftDetails(args: {
    treatmentId: string;
    organizationId: string;
    actorUserId: string;
    dto: SaveTreatmentDraftDto;
    pendingAppointmentId: string | null;
    pendingTreatmentId?: string | null;
    treatmentAt: Date;
    patientId: string;
  }) {
    for (const d of args.dto.details) {
      this.treatmentCatalog.assertKnownTreatmentType(d.treatmentType);
    }

    const normalizedDetails = args.dto.details.map((d, index) => {
      const teeth = normalizeTeeth(d.teeth);
      const toothSelectionGroups = normalizeToothSelectionGroups(
        d.toothSelectionGroups,
        teeth,
      );
      return {
        ...d,
        sortOrder: index,
        teeth,
        toothSelectionGroups,
        comment: d.comment?.trim() || null,
        attachmentIds: d.attachmentIds ?? [],
      };
    });

    const title = generateTreatmentTitle(
      normalizedDetails.map((d) => ({ treatmentType: d.treatmentType, teeth: d.teeth })),
    );

    return this.prisma.$transaction(async (tx) => {
      const saved = await tx.treatment.update({
        where: { id: args.treatmentId },
        data: {
          title,
          treatmentAt: args.treatmentAt,
          patientId: args.patientId,
          providerUserId: args.actorUserId,
        },
      });

      const keepDetailIds = normalizedDetails.map((d) => d.id).filter(Boolean) as string[];

      const existingDetails = await tx.treatmentDetail.findMany({
        where: { treatmentId: saved.id },
        select: { id: true, labCaseLink: { select: { labCase: { select: { sentAt: true } } } } },
      });

      const lockedDetailIds = new Set(
        existingDetails
          .filter((d) => d.labCaseLink?.labCase.sentAt)
          .map((d) => d.id),
      );

      const removableDetailIds = existingDetails
        .filter((d) => !keepDetailIds.includes(d.id) && !lockedDetailIds.has(d.id))
        .map((d) => d.id);

      if (removableDetailIds.length > 0) {
        await tx.treatmentDetail.deleteMany({
          where: { id: { in: removableDetailIds }, treatmentId: saved.id },
        });
      }

      for (const d of normalizedDetails) {
        if (d.id && lockedDetailIds.has(d.id)) {
          continue;
        }

        const row = d.id
          ? await tx.treatmentDetail.update({
              where: { id: d.id },
              data: {
                clientKey: d.clientId,
                sortOrder: d.sortOrder,
                treatmentType: d.treatmentType,
                teeth: d.teeth,
                toothSelectionGroups: d.toothSelectionGroups,
                comment: d.comment,
              },
            })
          : await tx.treatmentDetail.create({
              data: {
                treatmentId: saved.id,
                clientKey: d.clientId,
                sortOrder: d.sortOrder,
                treatmentType: d.treatmentType,
                teeth: d.teeth,
                toothSelectionGroups: d.toothSelectionGroups,
                comment: d.comment,
              },
            });

        const allowedAttachmentIds = new Set(d.attachmentIds);
        const pendingWhere: Prisma.TreatmentDetailAttachmentWhereInput = {
          detailClientKey: d.clientId,
          ...(args.pendingAppointmentId
            ? { appointmentId: args.pendingAppointmentId }
            : { treatmentId: args.pendingTreatmentId ?? saved.id }),
        };
        const pendingAttachments = await tx.treatmentDetailAttachment.findMany({
          where: pendingWhere,
        });

        for (const attachment of pendingAttachments) {
          if (!allowedAttachmentIds.has(attachment.id)) {
            await tx.treatmentDetailAttachment.delete({ where: { id: attachment.id } });
          } else {
            await tx.treatmentDetailAttachment.update({
              where: { id: attachment.id },
              data: {
                detailId: row.id,
                appointmentId: null,
                treatmentId: null,
                detailClientKey: null,
              },
            });
          }
        }

        await tx.treatmentDetailAttachment.deleteMany({
          where: {
            detailId: row.id,
            id: { notIn: [...allowedAttachmentIds] },
          },
        });
      }

      return tx.treatment.findUniqueOrThrow({
        where: { id: saved.id },
        include: treatmentInclude,
      });
    });
  }

  async saveLabCasesForAppointment(
    appointmentId: string,
    dto: SaveTreatmentLabCasesDto,
    organizationId: string,
    actorUserId: string,
    actorLanguage?: string | null,
  ) {
    await this.assertCanEditTreatment(actorUserId, organizationId);
    const appointment = await this.ensureAppointmentProvider(
      appointmentId,
      organizationId,
      actorUserId,
    );

    const treatment = await this.prisma.treatment.findFirst({
      where: { appointmentId: appointment.id, organizationId },
      select: { id: true },
    });

    if (!treatment) {
      throw new AppException(ErrorCode.TREATMENT_SAVE_DETAILS_BEFORE_LAB, HttpStatus.NOT_FOUND);
    }

    return this.saveLabCasesForTreatment(
      treatment.id,
      dto,
      organizationId,
      actorUserId,
      actorLanguage,
    );
  }

  async saveLabCasesForTreatment(
    treatmentId: string,
    dto: SaveTreatmentLabCasesDto,
    organizationId: string,
    actorUserId: string,
    actorLanguage?: string | null,
  ) {
    await this.assertCanEditTreatment(actorUserId, organizationId);
    await this.ensureTreatmentProvider(treatmentId, organizationId, actorUserId);

    const treatment = { id: treatmentId };

    const detailIds = dto.labCases.map((lc) => lc.treatmentDetailId);
    const uniqueDetailIds = new Set(detailIds);
    if (uniqueDetailIds.size !== detailIds.length) {
      throw new AppException(ErrorCode.TREATMENT_DETAIL_ONE_CASE, HttpStatus.BAD_REQUEST);
    }

    const details = await this.prisma.treatmentDetail.findMany({
      where: { treatmentId: treatment.id, id: { in: detailIds } },
      select: { id: true, treatmentType: true, teeth: true },
    });
    if (details.length !== uniqueDetailIds.size) {
      throw new AppException(ErrorCode.TREATMENT_DETAILS_NOT_FOUND, HttpStatus.BAD_REQUEST);
    }

    for (const detail of details) {
      this.treatmentCatalog.assertLabDependentTreatmentType(detail.treatmentType);
    }

    const detailById = new Map(details.map((d) => [d.id, d]));
    const linkedOrgIds = await this.getActiveLinkedOrganizationIds(organizationId);

    for (const lc of dto.labCases) {
      if (lc.destinationOrganizationId && !linkedOrgIds.has(lc.destinationOrganizationId)) {
        throw new AppException(ErrorCode.TREATMENT_DEST_NOT_LINKED, HttpStatus.BAD_REQUEST);
      }

      for (const row of lc.toothProsthesis ?? []) {
        if (lc.treatmentDetailId !== row.treatmentDetailId) {
          throw new AppException(ErrorCode.TREATMENT_TOOTH_UNKNOWN_DETAIL, HttpStatus.BAD_REQUEST);
        }
        const detail = detailById.get(row.treatmentDetailId);
        if (!detail) {
          throw new AppException(ErrorCode.TREATMENT_TOOTH_UNKNOWN_DETAIL, HttpStatus.BAD_REQUEST);
        }
        const teeth = normalizeTeeth(detail.teeth);
        const isArchSentinel = row.tooth === 'UA' || row.tooth === 'LA';
        if (!isArchSentinel && !teeth.includes(row.tooth)) {
          throw new AppException(ErrorCode.TREATMENT_TOOTH_NOT_ON_DETAIL, HttpStatus.BAD_REQUEST);
        }
        this.prosthesisCatalog.assertKnownProsthesisType(row.prosthesisTypeCode);
      }
    }

    const saved = await this.prisma.$transaction(async (tx) => {
      const existingLabCases = await tx.labCase.findMany({
        where: { treatmentId: treatment.id },
        select: { id: true, sentAt: true },
      });

      const sentLabCaseIds = new Set(existingLabCases.filter((lc) => lc.sentAt).map((lc) => lc.id));
      const keepLabCaseIds = dto.labCases.map((lc) => lc.id).filter(Boolean) as string[];

      const removableLabCaseIds = existingLabCases
        .filter((lc) => !keepLabCaseIds.includes(lc.id) && !lc.sentAt)
        .map((lc) => lc.id);

      if (removableLabCaseIds.length > 0) {
        await tx.labCase.deleteMany({
          where: { id: { in: removableLabCaseIds }, treatmentId: treatment.id },
        });
      }

      for (const [index, lc] of dto.labCases.entries()) {
        if (lc.id && sentLabCaseIds.has(lc.id)) {
          if (lc.dueDate !== undefined) {
            await this.updateLabCaseDueDateInTx(
              tx,
              lc.id,
              organizationId,
              actorUserId,
              lc.dueDate,
            );
          }
          continue;
        }

        let dueDate: Date | null | undefined;
        if (lc.dueDate !== undefined) {
          try {
            dueDate = parseDueDateInput(lc.dueDate);
          } catch {
            throw new AppException(ErrorCode.TREATMENT_INVALID_DUE_DATE, HttpStatus.BAD_REQUEST);
          }
        }

        const row = lc.id
          ? await tx.labCase.update({
              where: { id: lc.id },
              data: {
                clientKey: lc.clientId,
                sortOrder: index,
                destinationOrganizationId: lc.destinationOrganizationId ?? null,
                ...(dueDate !== undefined ? { dueDate } : {}),
              },
            })
          : await tx.labCase.create({
              data: {
                treatmentId: treatment.id,
                origin: 'CLINIC_DISPATCH',
                clientKey: lc.clientId,
                sortOrder: index,
                destinationOrganizationId: lc.destinationOrganizationId ?? null,
                ...(dueDate !== undefined ? { dueDate } : {}),
              },
            });

        await tx.labCaseDetail.deleteMany({ where: { labCaseId: row.id } });
        await tx.labCaseDetail.create({
          data: {
            labCaseId: row.id,
            treatmentDetailId: lc.treatmentDetailId,
          },
        });

        await tx.labCaseToothProsthesis.deleteMany({ where: { labCaseId: row.id } });
        if (lc.toothProsthesis?.length) {
          await tx.labCaseToothProsthesis.createMany({
            data: lc.toothProsthesis.map((tp) => ({
              labCaseId: row.id,
              treatmentDetailId: tp.treatmentDetailId,
              sourceKey: tp.treatmentDetailId,
              treatmentType: detailById.get(tp.treatmentDetailId)?.treatmentType ?? 'prosthesis',
              tooth: tp.tooth,
              prosthesisTypeCode: tp.prosthesisTypeCode,
              selectionGroupId: tp.selectionGroupId?.trim() || '',
            })),
          });
        }

        await tx.labCaseAttachment.deleteMany({ where: { labCaseId: row.id } });
        const attachmentIds = lc.attachmentIds ?? [];
        if (attachmentIds.length > 0) {
          const validAttachments = await tx.treatmentDetailAttachment.findMany({
            where: {
              id: { in: attachmentIds },
              detailId: lc.treatmentDetailId,
            },
            select: { id: true },
          });
          if (validAttachments.length !== attachmentIds.length) {
            throw new AppException(ErrorCode.TREATMENT_CASE_INVALID_ATTACHMENTS, HttpStatus.BAD_REQUEST);
          }
          await tx.labCaseAttachment.createMany({
            data: attachmentIds.map((attachmentId) => ({
              labCaseId: row.id,
              attachmentId,
            })),
          });
        }
      }

      return tx.treatment.findUniqueOrThrow({
        where: { id: treatment.id },
        include: treatmentInclude,
      });
    });

    return { success: true, data: this.mapTreatment(saved, actorLanguage) };
  }

  async sendLabCase(
    labCaseId: string,
    organizationId: string,
    actorUserId: string,
    actorLanguage?: string | null,
  ) {
    await this.assertCanEditTreatment(actorUserId, organizationId);

    const labCase = await this.prisma.labCase.findFirst({
      where: {
        id: labCaseId,
        treatment: { organizationId },
      },
      include: {
        treatment: {
          select: {
            providerUserId: true,
            appointment: { select: { providerUserId: true } },
          },
        },
        sends: { select: { organizationId: true } },
        details: {
          include: {
            detail: { select: { id: true, treatmentType: true, teeth: true } },
          },
        },
        toothProsthesis: true,
      },
    });

    if (!labCase) {
      throw new AppException(ErrorCode.LAB_CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    if (!labCase.destinationOrganizationId) {
      throw new AppException(ErrorCode.TREATMENT_CASE_NO_DEST, HttpStatus.BAD_REQUEST);
    }

    if (labCase.details.length === 0) {
      throw new AppException(ErrorCode.TREATMENT_CASE_NEEDS_DETAIL, HttpStatus.BAD_REQUEST);
    }

    if (labCase.details.length > 1) {
      throw new AppException(ErrorCode.TREATMENT_CASE_ONE_DETAIL, HttpStatus.BAD_REQUEST);
    }

    if (!labCase.treatment) {
      throw new AppException(ErrorCode.LAB_CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    assertCompleteToothProsthesisMap({
      details: labCase.details,
      toothProsthesis: labCase.toothProsthesis.filter(
        (row): row is typeof row & { treatmentDetailId: string } =>
          Boolean(row.treatmentDetailId),
      ),
    });

    if (!isActorTreatmentProvider(labCase.treatment, actorUserId)) {
      throw new AppException(ErrorCode.TREATMENT_ONLY_PROVIDER_SEND, HttpStatus.FORBIDDEN);
    }

    const linkedOrgIds = await this.getActiveLinkedOrganizationIds(organizationId);
    if (!linkedOrgIds.has(labCase.destinationOrganizationId)) {
      throw new AppException(ErrorCode.TREATMENT_DEST_NOT_LINKED, HttpStatus.BAD_REQUEST);
    }

    const alreadySent = labCase.sends.some(
      (s) => s.organizationId === labCase.destinationOrganizationId,
    );
    if (alreadySent) {
      throw new AppException(ErrorCode.TREATMENT_CASE_ALREADY_SENT, HttpStatus.BAD_REQUEST);
    }

    const now = new Date();
    const isFirstSend = !labCase.sentAt;

    await this.prisma.$transaction(async (tx) => {
      await tx.labCaseSend.create({
        data: {
          labCaseId,
          organizationId: labCase.destinationOrganizationId!,
        },
      });

      if (!labCase.sentAt) {
        await tx.labCase.update({
          where: { id: labCaseId },
          data: {
            sentAt: now,
            accessToken: generateLabCaseAccessToken(),
          },
        });
      }

      await generateLabCaseTasks(tx, labCaseId, actorLanguage);

      if (isFirstSend) {
        await this.labCaseActivity.record(
          {
            labCaseId,
            type: LabCaseActivityType.CASE_SENT,
            actorUserId,
          },
          tx,
        );
      }
    });

    if (isFirstSend && labCase.destinationOrganizationId) {
      void this.userNotifications.notify({
        organizationId: labCase.destinationOrganizationId,
        type: UserNotificationType.CASE_SENT,
        href: `/cases?caseId=${encodeURIComponent(labCaseId)}`,
        actorUserId,
        payload: { labCaseId },
        requiredPermission: 'TAB_CASES_READ',
      });
    }

    const refreshed = await this.prisma.labCase.findUniqueOrThrow({
      where: { id: labCaseId },
      include: {
        details: {
          include: {
            detail: {
              select: { id: true, clientKey: true, treatmentType: true, teeth: true },
            },
          },
        },
        sends: {
          orderBy: [{ sentAt: 'asc' }],
          include: { organization: { select: { id: true, name: true } } },
        },
        toothProsthesis: true,
        tasks: { select: { id: true, status: true } },
      },
    });

    return { success: true, data: this.mapLabCase(refreshed, actorLanguage) };
  }

  async updateLabCaseDueDate(
    labCaseId: string,
    dto: UpdateLabCaseDueDateDto,
    organizationId: string,
    actorUserId: string,
  ) {
    await this.assertCanEditTreatment(actorUserId, organizationId);

    const updated = await this.prisma.$transaction(async (tx) => {
      await this.updateLabCaseDueDateInTx(
        tx,
        labCaseId,
        organizationId,
        actorUserId,
        dto.dueDate ?? null,
      );
      return tx.labCase.findFirstOrThrow({
        where: { id: labCaseId },
        include: {
          details: {
            include: {
              detail: {
                select: { id: true, clientKey: true, treatmentType: true, teeth: true },
              },
            },
          },
          sends: {
            orderBy: [{ sentAt: 'asc' }],
            include: { organization: { select: { id: true, name: true } } },
          },
          toothProsthesis: true,
          tasks: { select: { id: true, status: true } },
          attachments: {
            include: {
              attachment: {
                select: {
                  id: true,
                  fileName: true,
                  mimeType: true,
                  sizeBytes: true,
                  createdAt: true,
                },
              },
            },
          },
        },
      });
    });

    return { success: true, data: this.mapLabCase(updated) };
  }

  private async updateLabCaseDueDateInTx(
    tx: Prisma.TransactionClient,
    labCaseId: string,
    organizationId: string,
    actorUserId: string,
    dueDateInput?: string | null,
  ) {
    const labCase = await tx.labCase.findFirst({
      where: {
        id: labCaseId,
        treatment: {
          organizationId,
          ...treatmentProviderScopeWhere(actorUserId),
        },
        sentAt: { not: null },
      },
      include: { tasks: { select: { status: true } } },
    });

    if (!labCase) {
      throw new AppException(ErrorCode.LAB_CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    if (isLabCaseFullyCompleted(labCase.tasks)) {
      throw new AppException(ErrorCode.TREATMENT_DUE_DATE_LOCKED, HttpStatus.BAD_REQUEST);
    }

    let dueDate: Date | null;
    try {
      dueDate = parseDueDateInput(dueDateInput ?? null);
    } catch {
      throw new AppException(ErrorCode.TREATMENT_INVALID_DUE_DATE, HttpStatus.BAD_REQUEST);
    }

    await tx.labCase.update({
      where: { id: labCaseId },
      data: { dueDate },
    });
  }

  async uploadDetailAttachments(
    appointmentId: string,
    detailClientKey: string,
    files: Express.Multer.File[],
    organizationId: string,
    actorUserId: string,
  ) {
    await this.assertCanEditTreatment(actorUserId, organizationId);
    await this.ensureAppointmentProvider(appointmentId, organizationId, actorUserId);

    if (!detailClientKey?.trim()) {
      throw new AppException(ErrorCode.TREATMENT_DETAIL_KEY_REQUIRED, HttpStatus.BAD_REQUEST);
    }

    if (!files?.length) {
      throw new AppException(ErrorCode.TREATMENT_FILE_REQUIRED, HttpStatus.BAD_REQUEST);
    }

    const existingDetail = await this.prisma.treatmentDetail.findFirst({
      where: {
        clientKey: detailClientKey,
        treatment: {
          appointmentId,
          organizationId,
          ...treatmentProviderScopeWhere(actorUserId),
        },
      },
      select: {
        labCaseLink: {
          select: { labCase: { select: { sentAt: true } } },
        },
      },
    });
    if (existingDetail?.labCaseLink?.labCase.sentAt) {
      throw new AppException(ErrorCode.TREATMENT_DETAIL_SENT, HttpStatus.CONFLICT);
    }

    const orgDir = join(this.uploadRoot, organizationId);
    mkdirSync(orgDir, { recursive: true });

    const created: {
      id: string;
      fileName: string;
      mimeType: string;
      sizeBytes: number;
    }[] = [];

    for (const file of files) {
      const storageName = `${randomUUID()}-${file.originalname.replace(/[^\w.\-()+]/g, '_')}`;
      const storagePath = join(orgDir, storageName);
      const { writeFileSync } = await import('fs');
      writeFileSync(storagePath, file.buffer);

      const attachment = await this.prisma.treatmentDetailAttachment.create({
        data: {
          appointmentId,
          detailClientKey,
          fileName: file.originalname,
          mimeType: file.mimetype || 'application/octet-stream',
          sizeBytes: file.size,
          storagePath,
        },
      });

      created.push(this.mapAttachment(attachment));
    }

    return { success: true, data: created };
  }

  async uploadDetailAttachmentsForTreatment(
    treatmentId: string,
    detailClientKey: string,
    files: Express.Multer.File[],
    organizationId: string,
    actorUserId: string,
  ) {
    await this.assertCanEditTreatment(actorUserId, organizationId);
    await this.ensureTreatmentProvider(treatmentId, organizationId, actorUserId);

    if (!detailClientKey?.trim()) {
      throw new AppException(ErrorCode.TREATMENT_DETAIL_KEY_REQUIRED, HttpStatus.BAD_REQUEST);
    }

    if (!files?.length) {
      throw new AppException(ErrorCode.TREATMENT_FILE_REQUIRED, HttpStatus.BAD_REQUEST);
    }

    const existingDetail = await this.prisma.treatmentDetail.findFirst({
      where: {
        clientKey: detailClientKey,
        treatmentId,
      },
      select: {
        labCaseLink: {
          select: { labCase: { select: { sentAt: true } } },
        },
      },
    });
    if (existingDetail?.labCaseLink?.labCase.sentAt) {
      throw new AppException(ErrorCode.TREATMENT_DETAIL_SENT, HttpStatus.CONFLICT);
    }

    const orgDir = join(this.uploadRoot, organizationId);
    mkdirSync(orgDir, { recursive: true });

    const created: {
      id: string;
      fileName: string;
      mimeType: string;
      sizeBytes: number;
    }[] = [];

    for (const file of files) {
      const storageName = `${randomUUID()}-${file.originalname.replace(/[^\w.\-()+]/g, '_')}`;
      const storagePath = join(orgDir, storageName);
      const { writeFileSync } = await import('fs');
      writeFileSync(storagePath, file.buffer);

      const attachment = await this.prisma.treatmentDetailAttachment.create({
        data: {
          treatmentId,
          detailClientKey,
          fileName: file.originalname,
          mimeType: file.mimetype || 'application/octet-stream',
          sizeBytes: file.size,
          storagePath,
        },
      });

      created.push(this.mapAttachment(attachment));
    }

    return { success: true, data: created };
  }

  async streamAttachmentFile(
    attachmentId: string,
    organizationId: string,
    actorUserId: string,
  ) {
    await this.assertCanReadTreatment(actorUserId, organizationId);

    const attachment = await this.prisma.treatmentDetailAttachment.findFirst({
      where: {
        id: attachmentId,
        OR: [
          {
            detail: {
              treatment: {
                organizationId,
                ...treatmentProviderScopeWhere(actorUserId),
              },
            },
          },
          { appointmentId: { not: null } },
          { treatmentId: { not: null } },
        ],
      },
      include: {
        detail: { select: { treatment: { select: { organizationId: true } } } },
      },
    });

    if (!attachment) {
      throw new AppException(ErrorCode.TREATMENT_ATTACHMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    if (attachment.detail && attachment.detail.treatment.organizationId !== organizationId) {
      throw new AppException(ErrorCode.TREATMENT_ATTACHMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    if (!attachment.detail && attachment.appointmentId) {
      const appointment = await this.prisma.appointment.findFirst({
        where: {
          id: attachment.appointmentId,
          organizationId,
          providerUserId: actorUserId,
        },
        select: { id: true },
      });
      if (!appointment) {
        throw new AppException(ErrorCode.TREATMENT_ATTACHMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
      }
    }

    if (!attachment.detail && attachment.treatmentId) {
      const treatment = await this.prisma.treatment.findFirst({
        where: {
          id: attachment.treatmentId,
          organizationId,
          providerUserId: actorUserId,
        },
        select: { id: true },
      });
      if (!treatment) {
        throw new AppException(ErrorCode.TREATMENT_ATTACHMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
      }
    }

    if (!existsSync(attachment.storagePath)) {
      throw new AppException(ErrorCode.TREATMENT_FILE_UNAVAILABLE, HttpStatus.NOT_FOUND);
    }

    return {
      stream: createReadStream(attachment.storagePath),
      fileName: attachment.fileName,
      mimeType: attachment.mimeType,
    };
  }

  private mapTreatment(treatment: {
    id: string;
    patientId: string;
    appointmentId: string | null;
    providerUserId?: string;
    title: string;
    treatmentAt: Date;
    patient?: {
      id: string;
      firstName: string;
      lastName: string;
      isWalkIn: boolean;
      mobile?: string | null;
      email?: string | null;
    };
    details: Array<{
      id: string;
      clientKey: string | null;
      treatmentType: string;
      teeth: unknown;
      comment: string | null;
      attachments: Array<{
        id: string;
        fileName: string;
        mimeType: string;
        sizeBytes: number;
      }>;
      labCaseLink?: {
        labCase: {
          id: string;
          sentAt: Date | null;
          destinationOrganizationId: string | null;
          sends: Array<{
            organizationId: string;
            sentAt: Date;
            organization: { id: string; name: string };
          }>;
        };
      } | null;
    }>;
    labCases: Array<{
      id: string;
      clientKey: string | null;
      sortOrder: number;
      destinationOrganizationId: string | null;
      sentAt: Date | null;
      details: Array<{
        treatmentDetailId: string;
        detail: { id: string; clientKey: string | null; treatmentType: string; teeth: unknown };
      }>;
      sends: Array<{
        organizationId: string;
        sentAt: Date;
        organization: { id: string; name: string };
      }>;
    }>;
  }, locale?: string | null) {
    const documents = treatment.details.flatMap((d) =>
      d.attachments.map((a) => this.mapAttachment(a)),
    );

    return {
      id: treatment.id,
      patientId: treatment.patientId,
      appointmentId: treatment.appointmentId,
      providerUserId: treatment.providerUserId ?? null,
      title: treatment.title,
      treatmentAt: treatment.treatmentAt.toISOString(),
      patient: treatment.patient
        ? {
            id: treatment.patient.id,
            firstName: treatment.patient.firstName,
            lastName: treatment.patient.lastName,
            isWalkIn: treatment.patient.isWalkIn,
            mobile: treatment.patient.mobile ?? null,
            email: treatment.patient.email ?? null,
          }
        : null,
      details: treatment.details.map((d) => this.mapDetail(d)),
      labCases: treatment.labCases.map((lc) => this.mapLabCase(lc, locale)),
      documents,
    };
  }

  private mapDetail(d: {
    id: string;
    clientKey?: string | null;
    treatmentType: string;
    teeth: unknown;
    toothSelectionGroups?: unknown;
    comment?: string | null;
    attachments?: Array<{
      id: string;
      fileName: string;
      mimeType: string;
      sizeBytes: number;
    }>;
    labCaseLink?: {
      labCase: {
        id: string;
        sentAt: Date | null;
        destinationOrganizationId: string | null;
        sends: Array<{
          organizationId: string;
          sentAt: Date;
          organization?: { id: string; name: string };
        }>;
        tasks?: Array<{ id: string; status: LabTaskStatus }>;
      };
    } | null;
  }) {
    const labCase = d.labCaseLink?.labCase;
    const taskProgress = this.mapTaskProgress(labCase?.tasks ?? []);
    const teeth = normalizeTeeth(d.teeth);
    return {
      id: d.id,
      clientId: d.clientKey ?? d.id,
      treatmentType: d.treatmentType,
      teeth,
      toothSelectionGroups: normalizeToothSelectionGroups(d.toothSelectionGroups, teeth),
      notes: d.comment ?? null,
      attachmentMetas: (d.attachments ?? []).map((a) => this.mapAttachment(a)),
      labCaseId: labCase?.id ?? null,
      sentAt: labCase?.sentAt?.toISOString() ?? null,
      destinationOrganizationId: labCase?.destinationOrganizationId ?? null,
      taskProgress,
      sends:
        labCase?.sends.map((s) => ({
          organizationId: s.organizationId,
          organizationName: s.organization?.name ?? 'Unknown organization',
          sentAt: s.sentAt.toISOString(),
        })) ?? [],
    };
  }

  private mapLabCase(lc: {
    id: string;
    clientKey?: string | null;
    sortOrder?: number;
    destinationOrganizationId?: string | null;
    sentAt?: Date | null;
    dueDate?: Date | null;
    accessToken?: string | null;
    details?: Array<{
      treatmentDetailId: string;
      detail?: {
        id: string;
        clientKey: string | null;
        treatmentType: string;
        teeth: unknown;
        toothSelectionGroups?: unknown;
      };
    }>;
    sends?: Array<{
      organizationId: string;
      sentAt: Date;
      organization?: { id: string; name: string };
    }>;
    toothProsthesis?: Array<{
      treatmentDetailId: string | null;
      tooth: string;
      prosthesisTypeCode: string;
      selectionGroupId?: string;
    }>;
    attachments?: Array<{
      attachment: {
        id: string;
        fileName: string;
        mimeType: string;
        sizeBytes: number;
        createdAt: Date;
      };
    }>;
    tasks?: Array<{ id: string; status: LabTaskStatus }>;
  }, locale?: string | null) {
    const taskProgress = this.mapTaskProgress(lc.tasks ?? []);
    const detailTeeth = lc.details?.[0]?.detail
      ? normalizeTeeth(lc.details[0].detail.teeth)
      : [];
    return {
      id: lc.id,
      clientId: lc.clientKey ?? lc.id,
      destinationOrganizationId: lc.destinationOrganizationId ?? null,
      sentAt: lc.sentAt?.toISOString() ?? null,
      dueDate: lc.dueDate?.toISOString() ?? null,
      taskProgress,
      treatmentDetailId: lc.details?.[0]?.treatmentDetailId ?? null,
      detail: lc.details?.[0]
        ? {
            id: lc.details[0].detail?.id ?? lc.details[0].treatmentDetailId,
            clientId: lc.details[0].detail?.clientKey ?? lc.details[0].treatmentDetailId,
            treatmentType: lc.details[0].detail?.treatmentType ?? '',
            teeth: detailTeeth,
            toothSelectionGroups: normalizeToothSelectionGroups(
              lc.details[0].detail?.toothSelectionGroups,
              detailTeeth,
            ),
          }
        : null,
      toothProsthesis: (lc.toothProsthesis ?? []).map((tp) => ({
        treatmentDetailId: tp.treatmentDetailId,
        tooth: tp.tooth,
        prosthesisTypeCode: tp.prosthesisTypeCode,
        selectionGroupId: tp.selectionGroupId ?? '',
      })),
      attachments: (lc.attachments ?? []).map((row) => ({
        id: row.attachment.id,
        fileName: row.attachment.fileName,
        mimeType: row.attachment.mimeType,
        sizeBytes: row.attachment.sizeBytes,
        createdAt: row.attachment.createdAt.toISOString(),
      })),
      sends:
        lc.sends?.map((s) => ({
          organizationId: s.organizationId,
          organizationName: s.organization?.name ?? 'Unknown organization',
          sentAt: s.sentAt.toISOString(),
        })) ?? [],
      shareUrl: this.labCaseShareUrl(lc, locale),
    };
  }

  private labCaseShareUrl(
    lc: { sentAt?: Date | null; accessToken?: string | null },
    locale?: string | null,
  ): string | null {
    if (!lc.sentAt || !lc.accessToken) return null;
    return buildLabCaseShareUrl(lc.accessToken, locale ?? 'en');
  }

  private async ensureSentLabCaseAccessTokens(
    labCases: Array<{ id: string; sentAt: Date | null; accessToken: string | null }>,
  ) {
    for (const lc of labCases) {
      if (!lc.sentAt || lc.accessToken) continue;
      const accessToken = generateLabCaseAccessToken();
      await this.prisma.labCase.update({
        where: { id: lc.id },
        data: { accessToken },
      });
      lc.accessToken = accessToken;
    }
  }

  private mapTaskProgress(tasks: Array<{ status: LabTaskStatus }>) {
    const total = tasks.length;
    const completed = tasks.filter((task) => task.status === LabTaskStatus.COMPLETED).length;
    return { completed, total };
  }

  private mapAttachment(a: {
    id: string;
    fileName: string;
    mimeType: string;
    sizeBytes: number;
  }) {
    return {
      id: a.id,
      fileName: a.fileName,
      mimeType: a.mimeType,
      sizeBytes: a.sizeBytes,
    };
  }

  private async getActiveLinkedOrganizationIds(organizationId: string) {
    const [linksA, linksB] = await Promise.all([
      this.prisma.organizationLink.findMany({
        where: { organizationAId: organizationId, status: LinkStatus.ACTIVE },
        select: { organizationBId: true },
      }),
      this.prisma.organizationLink.findMany({
        where: { organizationBId: organizationId, status: LinkStatus.ACTIVE },
        select: { organizationAId: true },
      }),
    ]);

    return new Set([
      ...linksA.map((l) => l.organizationBId),
      ...linksB.map((l) => l.organizationAId),
    ]);
  }

  private async ensurePatientExists(patientId: string) {
    const patient = await this.prisma.patient.findUnique({
      where: { id: patientId },
      select: { id: true },
    });
    if (!patient) {
      throw new AppException(ErrorCode.PATIENT_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
  }

  private async ensurePatientInOrg(patientId: string, organizationId: string) {
    const patient = await this.prisma.patient.findFirst({
      where: {
        id: patientId,
        isWalkIn: false,
        createdByOrganizationId: organizationId,
      },
      select: { id: true },
    });
    if (!patient) {
      throw new AppException(ErrorCode.PATIENT_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
  }

  private async ensureTreatmentProvider(
    treatmentId: string,
    organizationId: string,
    actorUserId: string,
  ) {
    const treatment = await this.prisma.treatment.findFirst({
      where: { id: treatmentId, organizationId },
      include: treatmentInclude,
    });
    if (!treatment) {
      throw new AppException(ErrorCode.TREATMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
    if (!isActorTreatmentProvider(treatment, actorUserId)) {
      throw new AppException(ErrorCode.TREATMENT_ONLY_PROVIDER_SEND, HttpStatus.FORBIDDEN);
    }
    return treatment;
  }

  private async ensureAppointmentProvider(
    appointmentId: string,
    organizationId: string,
    actorUserId: string,
  ) {
    const appointment = await this.prisma.appointment.findFirst({
      where: { id: appointmentId, organizationId },
      select: {
        id: true,
        patientId: true,
        providerUserId: true,
        startAt: true,
      },
    });

    if (!appointment) {
      throw new AppException(ErrorCode.APPOINTMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    if (appointment.providerUserId !== actorUserId) {
      throw new AppException(ErrorCode.APPOINTMENT_NOT_PROVIDER, HttpStatus.FORBIDDEN);
    }

    return appointment;
  }

  private async assertCanReadTreatment(userId: string, organizationId: string) {
    const m = await this.getMembership(userId, organizationId);
    if (!m) {
      throw new AppException(ErrorCode.PERMISSION_NOT_MEMBER, HttpStatus.FORBIDDEN);
    }
    if (
      hasEffectivePermission(m, 'TAB_TREATMENT_READ') ||
      hasEffectivePermission(m, 'TAB_TREATMENT_EDIT')
    ) {
      return;
    }
    throw new AppException(ErrorCode.PERMISSION_ACCESS_TREATMENTS, HttpStatus.FORBIDDEN);
  }

  private async assertCanEditTreatment(userId: string, organizationId: string) {
    const m = await this.getMembership(userId, organizationId);
    if (!m) {
      throw new AppException(ErrorCode.PERMISSION_NOT_MEMBER, HttpStatus.FORBIDDEN);
    }
    if (hasEffectivePermission(m, 'TAB_TREATMENT_EDIT')) {
      return;
    }
    throw new AppException(ErrorCode.PERMISSION_EDIT_TREATMENTS, HttpStatus.FORBIDDEN);
  }

  private async getMembership(userId: string, organizationId: string) {
    return this.prisma.membership.findFirst({
      where: {
        userId,
        organizationId,
        OR: [{ isOwner: true }, { isActive: true }],
      },
      include: {
        permissions: { include: { permission: true } },
        organization: { include: { type: true, plan: true } },
      },
    });
  }
}
