import {
  HttpStatus,
  Injectable,
} from '@nestjs/common';
import { AppException, ErrorCode } from '../../common/errors';
import { LabTaskStatus, LinkStatus, CatalogEntityKind } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import {
  isUnlimitedSeats,
} from '../../common/permissions';
import {
  OrganizationTypeName,
} from '../../common/organization-type';
import { getEffectivePermissionNames } from '../../common/membership-permissions';
import {
  CatalogLabelService,
  normalizeCatalogLocale,
  type CatalogLocale,
} from '../catalog/catalog-label.service';
import { StaffWorkingHoursService } from '../staff/staff-working-hours.service';
import { startOfUtcDay } from '../../common/lab-case-due-date';
import { TodaySummaryQueryDto } from './dto/today-summary-query.dto';

type ChartBucket = { code: string; label: string; count: number };

type StackedDayBucket = {
  code: string;
  label: string;
  completed: number;
  received: number;
};

type CompletionGaugeChart = {
  completed: number;
  total: number;
  percent: number;
};

type PartnerCasesBucket = {
  code: string;
  label: string;
  completed: number;
  pending: number;
};

type TodayCharts = {
  treatmentMixWeek?: ChartBucket[];
  tasksByProsthesis?: ChartBucket[];
  appointmentsByProvider?: ChartBucket[];
  caseCompletion?: CompletionGaugeChart;
  treatmentPlanCompletion?: CompletionGaugeChart;
  appointmentsWeekAll?: ChartBucket[];
  appointmentsWeekMine?: ChartBucket[];
  labTaskActivityWeek?: StackedDayBucket[];
  casePartnersMonth?: PartnerCasesBucket[];
  casesDueWeek?: ChartBucket[];
  efficiencyReport?: ChartBucket[];
};

type TodayActions = {
  upcomingAppointmentsToday?: Array<{
    id: string;
    patientName: string;
    startAt: string;
    endAt: string;
    purpose: string;
  }>;
};

type TodaySubscriptionWidget = {
  hasActivePlan: boolean;
  planName: string | null;
  seatsUsed: number;
  seatsLimit: number | null;
  seatsUnlimited: boolean;
  seatsPercent: number;
  periodStartAt: string;
  periodEndAt: string | null;
  periodTotalDays: number;
  periodElapsedDays: number;
  periodPercent: number;
};

type TodayWidgets = {
  appointmentsToday?: { count: number };
  patientsToday?: { count: number };
  treatmentsToday?: { count: number };
  labCasesPendingSend?: { count: number };
  casesReceivedToday?: { count: number };
  casesInProgress?: { count: number };
  tasksInProgress?: { count: number };
  importantTasks?: { count: number };
  overdueCases?: { count: number };
  unassignedTasks?: { count: number };
  pendingConnections?: { count: number };
  pendingStaffInvites?: { count: number };
  providersWithoutWorkingHours?: { count: number; membershipIds: string[] };
};

@Injectable()
export class TodayService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly catalogLabels: CatalogLabelService,
    private readonly staffWorkingHoursService: StaffWorkingHoursService,
  ) {}

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

  async getSummary(
    userId: string,
    organizationId: string,
    query: TodaySummaryQueryDto,
    localeInput?: string | null,
  ) {
    const membership = await this.getActiveMembership(userId, organizationId);
    const permissionNames = this.resolvePermissionNames(membership);
    this.assertCanViewToday(membership.isOwner, permissionNames);

    const orgType = membership.organization.type.name as OrganizationTypeName;
    const { from, to } = this.resolveDayRange(query);
    const locale = normalizeCatalogLocale(localeInput);

    const widgets: TodayWidgets = {};
    const charts: TodayCharts = {};
    const actions: TodayActions = {};
    const tasks: Promise<void>[] = [];
    let subscription: TodaySubscriptionWidget | undefined;

    if (orgType === 'CLINIC') {
      if (this.canViewAppointments(membership.isOwner, permissionNames)) {
        tasks.push(
          this.loadAppointmentsToday(organizationId, from, to, widgets),
        );
        tasks.push(
          this.loadAppointmentsByProvider(organizationId, from, to, charts),
        );
        tasks.push(
          this.loadAppointmentsWeekAll(
            organizationId,
            to,
            query.utcOffsetMinutes,
            resolveWeekMonthPeriod(query.appointmentsWeekPeriod, 'week'),
            charts,
          ),
        );
      }

      if (this.canEditTreatment(membership.isOwner, permissionNames)) {
        tasks.push(
          this.loadCasePartnersMonth(
            'CLINIC',
            organizationId,
            userId,
            !membership.isOwner,
            to,
            resolveWeekMonthYearPeriod(query.casePartnersPeriod, 'month'),
            charts,
          ),
        );
        tasks.push(
          this.loadTreatmentPlanCompletion(
            organizationId,
            userId,
            !membership.isOwner,
            charts,
          ),
        );
      }

      if (this.canViewTreatment(membership.isOwner, permissionNames)) {
        tasks.push(
          this.loadTreatmentsToday(organizationId, from, to, widgets),
        );
        tasks.push(this.loadLabCasesPendingSend(organizationId, widgets));
        tasks.push(
          this.loadTreatmentMixWeek(
            organizationId,
            to,
            locale,
            resolveWeekMonthYearPeriod(query.treatmentMixPeriod, 'week'),
            charts,
          ),
        );
      }

      if (this.canViewMyAppointmentsWeekChart(membership.isOwner, permissionNames)) {
        tasks.push(
          this.loadUpcomingAppointmentsToday(
            organizationId,
            userId,
            from,
            to,
            actions,
          ),
        );
        tasks.push(
          this.loadAppointmentsWeekMine(
            organizationId,
            userId,
            to,
            query.utcOffsetMinutes,
            resolveWeekMonthPeriod(query.appointmentsWeekMinePeriod, 'week'),
            charts,
          ),
        );
      }

      if (
        this.canViewPatients(membership.isOwner, permissionNames) ||
        this.canViewAppointments(membership.isOwner, permissionNames)
      ) {
        tasks.push(
          this.loadPatientsToday(organizationId, from, to, widgets),
        );
      }

      if (this.canViewStaff(membership.isOwner, permissionNames)) {
        tasks.push(
          this.loadProvidersWithoutWorkingHours(organizationId, widgets),
        );
      }
    }

    if (orgType === 'LAB') {
      const canViewLabWork =
        this.canViewCases(membership.isOwner, permissionNames) ||
        this.canViewTasks(membership.isOwner, permissionNames);

      if (this.canViewCases(membership.isOwner, permissionNames)) {
        tasks.push(
          this.loadCasesReceivedToday(organizationId, from, to, widgets),
        );
        tasks.push(this.loadCasesInProgress(organizationId, widgets));
        tasks.push(this.loadCaseCompletion(organizationId, charts));
        tasks.push(
          this.loadCasesDueWeek(
            organizationId,
            from,
            query.utcOffsetMinutes,
            resolveWeekMonthPeriod(query.casesDuePeriod, 'week'),
            charts,
          ),
        );
      }

      if (this.canEditCases(membership.isOwner, permissionNames)) {
        tasks.push(
          this.loadCasePartnersMonth(
            'LAB',
            organizationId,
            userId,
            false,
            to,
            resolveWeekMonthYearPeriod(query.casePartnersPeriod, 'month'),
            charts,
          ),
        );
      }

      if (this.canViewTasks(membership.isOwner, permissionNames)) {
        tasks.push(this.loadTasksInProgress(organizationId, widgets));
        tasks.push(this.loadImportantTasks(organizationId, widgets));
        tasks.push(this.loadOverdueCases(organizationId, widgets));
        tasks.push(this.loadUnassignedTasks(organizationId, widgets));
        tasks.push(this.loadTasksByProsthesis(organizationId, locale, charts));
      }

      if (canViewLabWork) {
        tasks.push(
          this.loadLabTaskActivityWeek(
            organizationId,
            to,
            query.utcOffsetMinutes,
            resolveWeekMonthPeriod(query.labTaskActivityPeriod, 'week'),
            charts,
          ),
        );
      }
    }

    if (this.canManageOrganizations(membership.isOwner, permissionNames)) {
      tasks.push(
        this.loadPendingConnections(organizationId, widgets),
      );
    }

    if (membership.isOwner) {
      const efficiencyPeriod = resolveWeekMonthYearPeriod(
        query.efficiencyPeriod,
        'month',
      );
      if (orgType === 'CLINIC') {
        tasks.push(
          this.loadClinicEfficiencyReport(organizationId, to, efficiencyPeriod, charts),
        );
      }
      if (orgType === 'LAB') {
        tasks.push(
          this.loadLabEfficiencyReport(organizationId, to, efficiencyPeriod, charts),
        );
      }
      tasks.push(
        this.buildSubscriptionWidget(organizationId, membership.organization).then(
          (value) => {
            subscription = value;
          },
        ),
      );
    }

    if (this.canViewStaff(membership.isOwner, permissionNames)) {
      tasks.push(this.loadPendingStaffInvites(organizationId, widgets));
    }

    await Promise.all(tasks);

    return {
      success: true,
      data: {
        generatedAt: new Date().toISOString(),
        orgType,
        range: { from: from.toISOString(), to: to.toISOString() },
        widgets,
        charts,
        actions,
        ...(subscription ? { subscription } : {}),
      },
    };
  }

  private async loadTreatmentMixWeek(
    organizationId: string,
    rangeEnd: Date,
    locale: CatalogLocale,
    period: ChartPeriodWeekMonthYear,
    charts: TodayCharts,
  ) {
    const windowStart = new Date(
      rangeEnd.getTime() - daysForChartPeriod(period) * 86_400_000,
    );
    const grouped = await this.prisma.treatmentDetail.groupBy({
      by: ['treatmentType'],
      where: {
        treatment: {
          organizationId,
          treatmentAt: { gte: windowStart, lt: rangeEnd },
        },
      },
      _count: { _all: true },
    });

    const sorted = grouped
      .map((row) => ({
        code: row.treatmentType,
        count: aggregateCount(row._count),
      }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 8);

    if (sorted.length === 0) {
      charts.treatmentMixWeek = [];
      return;
    }

    const labels = await this.catalogLabels.resolveLabels(
      CatalogEntityKind.TREATMENT_TYPE,
      sorted.map((row) => row.code),
      locale,
    );

    charts.treatmentMixWeek = sorted.map((row) => ({
      code: row.code,
      label: labels.get(row.code) ?? row.code,
      count: row.count,
    }));
  }

  private async loadTasksByProsthesis(
    labOrganizationId: string,
    locale: CatalogLocale,
    charts: TodayCharts,
  ) {
    const grouped = await this.prisma.labCaseTask.groupBy({
      by: ['prosthesisTypeCode'],
      where: {
        status: LabTaskStatus.IN_PROGRESS,
        labCase: {
          sentAt: { not: null },
          sends: { some: { organizationId: labOrganizationId } },
        },
      },
      _count: { _all: true },
    });

    const sorted = grouped
      .map((row) => ({
        code: row.prosthesisTypeCode,
        count: aggregateCount(row._count),
      }))
      .sort((a, b) => b.count - a.count);

    if (sorted.length === 0) {
      charts.tasksByProsthesis = [];
      return;
    }

    const labels = await this.catalogLabels.resolveLabels(
      CatalogEntityKind.PROSTHESIS_TYPE,
      sorted.map((row) => row.code),
      locale,
    );

    charts.tasksByProsthesis = sorted.map((row) => ({
      code: row.code,
      label: labels.get(row.code) ?? row.code,
      count: row.count,
    }));
  }

  private resolveDayRange(query: TodaySummaryQueryDto): { from: Date; to: Date } {
    if (query.from && query.to) {
      const from = new Date(query.from);
      const to = new Date(query.to);
      if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) {
        throw new AppException(ErrorCode.TODAY_INVALID_RANGE, HttpStatus.BAD_REQUEST);
      }
      if (to <= from) {
        throw new AppException(ErrorCode.TODAY_INVALID_RANGE_ORDER, HttpStatus.BAD_REQUEST);
      }
      return { from, to };
    }

    const now = new Date();
    const from = new Date(
      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0),
    );
    const to = new Date(from.getTime() + 86_400_000);
    return { from, to };
  }

  private async loadAppointmentsToday(
    organizationId: string,
    from: Date,
    to: Date,
    widgets: TodayWidgets,
  ) {
    const count = await this.prisma.appointment.count({
      where: {
        organizationId,
        startAt: { lt: to },
        endAt: { gt: from },
      },
    });
    widgets.appointmentsToday = { count };
  }

  private async loadUpcomingAppointmentsToday(
    organizationId: string,
    providerUserId: string,
    from: Date,
    to: Date,
    actions: TodayActions,
  ) {
    const now = new Date();
    const items = await this.prisma.appointment.findMany({
      where: {
        organizationId,
        providerUserId,
        startAt: { lt: to },
        endAt: { gt: now > from ? now : from },
      },
      include: {
        patient: { select: { firstName: true, lastName: true } },
      },
      orderBy: { startAt: 'asc' },
      take: 10,
    });

    actions.upcomingAppointmentsToday = items.map((appointment) => ({
      id: appointment.id,
      patientName: `${appointment.patient.firstName} ${appointment.patient.lastName}`.trim(),
      startAt: appointment.startAt.toISOString(),
      endAt: appointment.endAt.toISOString(),
      purpose: appointment.purpose,
    }));
  }

  private async loadPatientsToday(
    organizationId: string,
    from: Date,
    to: Date,
    widgets: TodayWidgets,
  ) {
    const rows = await this.prisma.appointment.findMany({
      where: {
        organizationId,
        startAt: { lt: to },
        endAt: { gt: from },
      },
      select: { patientId: true },
      distinct: ['patientId'],
    });
    widgets.patientsToday = { count: rows.length };
  }

  private async loadTreatmentsToday(
    organizationId: string,
    from: Date,
    to: Date,
    widgets: TodayWidgets,
  ) {
    const count = await this.prisma.treatment.count({
      where: {
        organizationId,
        treatmentAt: { gte: from, lt: to },
      },
    });
    widgets.treatmentsToday = { count };
  }

  private async loadLabCasesPendingSend(organizationId: string, widgets: TodayWidgets) {
    const count = await this.prisma.labCase.count({
      where: {
        sentAt: null,
        destinationOrganizationId: { not: null },
        treatment: { organizationId },
      },
    });
    widgets.labCasesPendingSend = { count };
  }

  private async loadCasesReceivedToday(
    labOrganizationId: string,
    from: Date,
    to: Date,
    widgets: TodayWidgets,
  ) {
    const count = await this.prisma.labCase.count({
      where: {
        sentAt: { gte: from, lt: to },
        sends: { some: { organizationId: labOrganizationId } },
      },
    });
    widgets.casesReceivedToday = { count };
  }

  private async loadTasksInProgress(labOrganizationId: string, widgets: TodayWidgets) {
    const count = await this.prisma.labCaseTask.count({
      where: {
        status: LabTaskStatus.IN_PROGRESS,
        labCase: {
          sentAt: { not: null },
          sends: { some: { organizationId: labOrganizationId } },
        },
      },
    });
    widgets.tasksInProgress = { count };
  }

  private async loadImportantTasks(labOrganizationId: string, widgets: TodayWidgets) {
    const count = await this.prisma.labCaseTask.count({
      where: {
        status: LabTaskStatus.IN_PROGRESS,
        labCase: {
          isImportant: true,
          sentAt: { not: null },
          sends: { some: { organizationId: labOrganizationId } },
        },
      },
    });
    widgets.importantTasks = { count };
  }

  private async loadOverdueCases(labOrganizationId: string, widgets: TodayWidgets) {
    const count = await this.prisma.labCase.count({
      where: {
        sentAt: { not: null },
        dueDate: { not: null, lt: startOfUtcDay() },
        sends: { some: { organizationId: labOrganizationId } },
        tasks: { some: { status: LabTaskStatus.IN_PROGRESS } },
      },
    });
    widgets.overdueCases = { count };
  }

  private async loadUnassignedTasks(labOrganizationId: string, widgets: TodayWidgets) {
    const count = await this.prisma.labCaseTask.count({
      where: {
        status: LabTaskStatus.IN_PROGRESS,
        assigneeUserId: null,
        labCase: {
          sentAt: { not: null },
          sends: { some: { organizationId: labOrganizationId } },
        },
      },
    });
    widgets.unassignedTasks = { count };
  }

  private async loadPendingConnections(organizationId: string, widgets: TodayWidgets) {
    const links = await this.prisma.organizationLink.findMany({
      where: {
        status: LinkStatus.PENDING,
        OR: [{ organizationAId: organizationId }, { organizationBId: organizationId }],
      },
      select: { sharedDataTypes: true },
    });

    const count = links.filter((link) => {
      const requesterOrgId = this.getRequesterOrganizationId(link.sharedDataTypes);
      return requesterOrgId !== null && requesterOrgId !== organizationId;
    }).length;

    widgets.pendingConnections = { count };
  }

  private async getActiveEditAccessMembers(
    organizationId: string,
    editPermission: 'TAB_TREATMENT_EDIT' | 'TAB_TASKS_EDIT',
  ): Promise<Array<{ userId: string; isOwner: boolean }>> {
    const members = await this.prisma.membership.findMany({
      where: {
        organizationId,
        OR: [{ isOwner: true }, { isActive: true }],
        permissions: {
          some: { permission: { name: editPermission } },
        },
      },
      select: { userId: true, isOwner: true },
    });

    return members.map((member) => ({
      userId: member.userId,
      isOwner: member.isOwner,
    }));
  }

  private async buildEfficiencyReportRows(
    members: Array<{ userId: string; isOwner: boolean }>,
    countsByUser: Map<string, number>,
  ): Promise<ChartBucket[] | undefined> {
    if (members.length === 0) {
      return undefined;
    }

    const userIds = members.map((member) => member.userId);
    const users = await this.prisma.user.findMany({
      where: { id: { in: userIds } },
      select: { id: true, name: true },
    });
    const nameById = new Map(users.map((user) => [user.id, user.name]));

    const rows = members
      .map((member) => ({
        code: member.userId,
        label: nameById.get(member.userId) ?? member.userId,
        count: countsByUser.get(member.userId) ?? 0,
      }))
      .sort((a, b) => b.count - a.count);

    return rows.length >= 2 ? rows : undefined;
  }

  private async loadClinicEfficiencyReport(
    organizationId: string,
    rangeEnd: Date,
    period: ChartPeriodWeekMonthYear,
    charts: TodayCharts,
  ) {
    const members = await this.getActiveEditAccessMembers(
      organizationId,
      'TAB_TREATMENT_EDIT',
    );

    const windowStart = new Date(
      rangeEnd.getTime() - daysForChartPeriod(period) * 86_400_000,
    );
    const grouped = await this.prisma.treatment.groupBy({
      by: ['providerUserId'],
      where: {
        organizationId,
        treatmentAt: { gte: windowStart, lt: rangeEnd },
        providerUserId: { in: members.map((member) => member.userId) },
      },
      _count: { _all: true },
    });

    const countsByUser = new Map(
      members.map((member) => [member.userId, 0]),
    );
    for (const row of grouped) {
      countsByUser.set(row.providerUserId, aggregateCount(row._count));
    }

    const report = await this.buildEfficiencyReportRows(members, countsByUser);
    if (report) {
      charts.efficiencyReport = report;
    }
  }

  private async loadLabEfficiencyReport(
    labOrganizationId: string,
    rangeEnd: Date,
    period: ChartPeriodWeekMonthYear,
    charts: TodayCharts,
  ) {
    const members = await this.getActiveEditAccessMembers(
      labOrganizationId,
      'TAB_TASKS_EDIT',
    );

    const windowStart = new Date(
      rangeEnd.getTime() - daysForChartPeriod(period) * 86_400_000,
    );
    const grouped = await this.prisma.labCaseTaskStatusEvent.groupBy({
      by: ['changedByUserId'],
      where: {
        toStatus: LabTaskStatus.COMPLETED,
        changedAt: { gte: windowStart, lt: rangeEnd },
        changedByUserId: { in: members.map((member) => member.userId) },
        task: {
          labCase: {
            sends: { some: { organizationId: labOrganizationId } },
          },
        },
      },
      _count: { _all: true },
    });

    const countsByUser = new Map(
      members.map((member) => [member.userId, 0]),
    );
    for (const row of grouped) {
      if (!row.changedByUserId) continue;
      countsByUser.set(row.changedByUserId, aggregateCount(row._count));
    }

    const report = await this.buildEfficiencyReportRows(members, countsByUser);
    if (report) {
      charts.efficiencyReport = report;
    }
  }

  private async buildSubscriptionWidget(
    organizationId: string,
    organization: {
      createdAt: Date;
      plan: { name: string; maxUsers: number } | null;
    },
  ): Promise<TodaySubscriptionWidget> {
    const seatsUsed = await this.prisma.membership.count({
      where: {
        organizationId,
        OR: [{ isOwner: true }, { isActive: true }],
      },
    });

    const plan = organization.plan;
    const periodStartAt = organization.createdAt.toISOString();

    if (!plan) {
      return {
        hasActivePlan: false,
        planName: null,
        seatsUsed,
        seatsLimit: null,
        seatsUnlimited: false,
        seatsPercent: 0,
        periodStartAt,
        periodEndAt: null,
        periodTotalDays: 0,
        periodElapsedDays: 0,
        periodPercent: 0,
      };
    }

    const maxUsers = plan.maxUsers;
    const seatsUnlimited = isUnlimitedSeats(maxUsers);
    const seatsLimit = seatsUnlimited ? null : maxUsers;
    const seatsPercent =
      seatsUnlimited || maxUsers <= 0
        ? 0
        : Math.min(100, Math.round((seatsUsed / maxUsers) * 100));

    const durationDays = plan.name === 'trial' ? 30 : 90;
    const periodEnd = new Date(organization.createdAt);
    periodEnd.setDate(periodEnd.getDate() + durationDays);
    const periodEndAt = periodEnd.toISOString();
    const totalMs = periodEnd.getTime() - organization.createdAt.getTime();
    const elapsedMs = Math.min(
      Math.max(0, Date.now() - organization.createdAt.getTime()),
      totalMs,
    );
    const periodPercent =
      totalMs > 0 ? Math.min(100, Math.round((elapsedMs / totalMs) * 100)) : 0;
    const periodElapsedDays = Math.min(
      durationDays,
      Math.floor(elapsedMs / 86_400_000),
    );

    return {
      hasActivePlan: true,
      planName: plan.name,
      seatsUsed,
      seatsLimit,
      seatsUnlimited,
      seatsPercent,
      periodStartAt,
      periodEndAt,
      periodTotalDays: durationDays,
      periodElapsedDays,
      periodPercent,
    };
  }

  private async loadPendingStaffInvites(organizationId: string, widgets: TodayWidgets) {
    const members = await this.prisma.membership.findMany({
      where: { organizationId, isOwner: false, isActive: false },
      include: {
        invitations: {
          orderBy: { createdAt: 'desc' },
          take: 1,
        },
      },
    });

    const count = members.filter((member) => {
      const invitation = member.invitations[0];
      if (!invitation || invitation.acceptedAt || invitation.revokedAt) {
        return false;
      }
      return invitation.expiresAt.getTime() > Date.now();
    }).length;

    widgets.pendingStaffInvites = { count };
  }

  private async listTreatmentParticipatingMembers(organizationId: string) {
    return this.prisma.membership.findMany({
      where: {
        organizationId,
        OR: [{ isOwner: true }, { isActive: true }],
        permissions: {
          some: {
            permission: { name: 'TAB_TREATMENT_EDIT' },
          },
        },
      },
      select: {
        id: true,
        isOwner: true,
        user: { select: { id: true, name: true } },
      },
    });
  }

  private async loadAppointmentsByProvider(
    organizationId: string,
    from: Date,
    to: Date,
    charts: TodayCharts,
  ) {
    const members = await this.listTreatmentParticipatingMembers(organizationId);

    if (members.length === 0) {
      charts.appointmentsByProvider = [];
      return;
    }

    const participatingUserIds = new Set(members.map((member) => member.user.id));
    const ownerMember = members.find((member) => member.isOwner);

    const appointments = await this.prisma.appointment.findMany({
      where: {
        organizationId,
        startAt: { lt: to },
        endAt: { gt: from },
        providerUserId: { in: [...participatingUserIds] },
      },
      select: { providerUserId: true },
    });

    const countsByProvider = new Map<string, number>();
    for (const appointment of appointments) {
      countsByProvider.set(
        appointment.providerUserId,
        (countsByProvider.get(appointment.providerUserId) ?? 0) + 1,
      );
    }

    const rows = members.map((member) => ({
      userId: member.user.id,
      label: member.user.name,
      count: countsByProvider.get(member.user.id) ?? 0,
      isOwner: member.isOwner,
    }));

    const sorted = [...rows].sort((a, b) => b.count - a.count);
    let top = sorted.slice(0, 8);

    if (ownerMember) {
      const ownerUserId = ownerMember.user.id;
      const ownerInTop = top.some((row) => row.userId === ownerUserId);
      if (!ownerInTop) {
        const ownerRow = rows.find((row) => row.userId === ownerUserId)!;
        if (top.length >= 8) {
          top = [...top.slice(0, 7), ownerRow];
        } else {
          top = [...top, ownerRow];
        }
      }
    }

    charts.appointmentsByProvider = top.map((row) => ({
      code: row.userId,
      label: row.label,
      count: row.count,
    }));
  }

  private async loadProvidersWithoutWorkingHours(
    organizationId: string,
    widgets: TodayWidgets,
  ) {
    const members = await this.listTreatmentParticipatingMembers(organizationId);

    if (members.length === 0) {
      widgets.providersWithoutWorkingHours = { count: 0, membershipIds: [] };
      return;
    }

    const scheduleBlocksByMembership =
      await this.staffWorkingHoursService.loadScheduleBlocksByMembershipIds(
        members.map((member) => member.id),
      );

    const missingHoursMembers = members.filter((member) => {
      const blocks = scheduleBlocksByMembership.get(member.id) ?? [];
      return blocks.length === 0;
    });

    widgets.providersWithoutWorkingHours = {
      count: missingHoursMembers.length,
      membershipIds: missingHoursMembers.map((member) => member.id),
    };
  }

  private async loadCasesDueWeek(
    labOrganizationId: string,
    rangeStart: Date,
    utcOffsetMinutes: number | undefined,
    period: ChartPeriodWeekMonth,
    charts: TodayCharts,
  ) {
    const dayBuckets = buildLocalDayBuckets(
      rangeStart,
      daysForChartPeriod(period),
      'forward',
      utcOffsetMinutes,
    );
    const weekStart = dayBuckets[0]?.start ?? rangeStart;
    const weekEnd = dayBuckets[dayBuckets.length - 1]?.end ?? rangeStart;
    const offsetMs = (utcOffsetMinutes ?? 0) * 60_000;
    const counts = new Map(dayBuckets.map((bucket) => [bucket.code, 0]));

    const cases = await this.prisma.labCase.findMany({
      where: {
        dueDate: { not: null, gte: weekStart, lt: weekEnd },
        sentAt: { not: null },
        sends: { some: { organizationId: labOrganizationId } },
        tasks: { some: { status: LabTaskStatus.IN_PROGRESS } },
      },
      select: { dueDate: true },
    });

    for (const labCase of cases) {
      if (!labCase.dueDate) continue;
      const dayKey = localDayKeyFromDate(labCase.dueDate, offsetMs);
      if (counts.has(dayKey)) {
        counts.set(dayKey, (counts.get(dayKey) ?? 0) + 1);
      }
    }

    charts.casesDueWeek = dayBuckets.map((bucket) => ({
      code: bucket.code,
      label: bucket.label,
      count: counts.get(bucket.code) ?? 0,
    }));
  }

  private async loadCasesInProgress(labOrganizationId: string, widgets: TodayWidgets) {
    const cases = await this.prisma.labCase.findMany({
      where: {
        sentAt: { not: null },
        sends: { some: { organizationId: labOrganizationId } },
      },
      include: {
        tasks: { select: { status: true } },
      },
    });

    const count = cases.filter((labCase) => {
      if (labCase.tasks.length === 0) return false;
      const completed = labCase.tasks.filter(
        (task) => task.status === LabTaskStatus.COMPLETED,
      ).length;
      return completed < labCase.tasks.length;
    }).length;

    widgets.casesInProgress = { count };
  }

  private async loadCaseCompletion(labOrganizationId: string, charts: TodayCharts) {
    const tasks = await this.prisma.labCaseTask.findMany({
      where: {
        labCase: {
          sentAt: { not: null },
          sends: { some: { organizationId: labOrganizationId } },
        },
      },
      select: { status: true },
    });

    charts.caseCompletion = this.buildCompletionGauge(
      tasks.filter((task) => task.status === LabTaskStatus.COMPLETED).length,
      tasks.length,
    );
  }

  private async loadTreatmentPlanCompletion(
    organizationId: string,
    userId: string,
    scopeToUser: boolean,
    charts: TodayCharts,
  ) {
    const appointmentWhere = {
      organizationId,
      ...(scopeToUser ? { providerUserId: userId } : {}),
    };

    const [total, completed] = await Promise.all([
      this.prisma.appointment.count({ where: appointmentWhere }),
      this.prisma.appointment.count({
        where: {
          ...appointmentWhere,
          treatment: { details: { some: {} } },
        },
      }),
    ]);

    charts.treatmentPlanCompletion = this.buildCompletionGauge(completed, total);
  }

  private buildCompletionGauge(completed: number, total: number): CompletionGaugeChart {
    return {
      completed,
      total,
      percent: total > 0 ? Math.round((completed / total) * 100) : 0,
    };
  }

  private async loadAppointmentsWeekAll(
    organizationId: string,
    rangeEnd: Date,
    utcOffsetMinutes: number | undefined,
    period: ChartPeriodWeekMonth,
    charts: TodayCharts,
  ) {
    charts.appointmentsWeekAll = await this.loadAppointmentsWeekSeries(
      organizationId,
      rangeEnd,
      utcOffsetMinutes,
      period,
    );
  }

  private async loadAppointmentsWeekMine(
    organizationId: string,
    providerUserId: string,
    rangeEnd: Date,
    utcOffsetMinutes: number | undefined,
    period: ChartPeriodWeekMonth,
    charts: TodayCharts,
  ) {
    charts.appointmentsWeekMine = await this.loadAppointmentsWeekSeries(
      organizationId,
      rangeEnd,
      utcOffsetMinutes,
      period,
      providerUserId,
    );
  }

  private async loadAppointmentsWeekSeries(
    organizationId: string,
    rangeEnd: Date,
    utcOffsetMinutes: number | undefined,
    period: ChartPeriodWeekMonth,
    providerUserId?: string,
  ): Promise<ChartBucket[]> {
    const dayBuckets = buildLocalDayBuckets(
      rangeEnd,
      daysForChartPeriod(period),
      'lookback',
      utcOffsetMinutes,
    );
    const weekStart = dayBuckets[0]?.start ?? rangeEnd;
    const weekEnd = rangeEnd;

    const appointments = await this.prisma.appointment.findMany({
      where: {
        organizationId,
        startAt: { gte: weekStart, lt: weekEnd },
        ...(providerUserId ? { providerUserId } : {}),
      },
      select: { startAt: true },
    });

    const countsByDay = new Map<string, number>();
    for (const bucket of dayBuckets) {
      countsByDay.set(bucket.code, 0);
    }

    const offsetMs = (utcOffsetMinutes ?? 0) * 60_000;
    for (const appointment of appointments) {
      const dayKey = localDayKeyFromDate(appointment.startAt, offsetMs);
      if (countsByDay.has(dayKey)) {
        countsByDay.set(dayKey, (countsByDay.get(dayKey) ?? 0) + 1);
      }
    }

    return dayBuckets.map((bucket) => ({
      code: bucket.code,
      label: bucket.label,
      count: countsByDay.get(bucket.code) ?? 0,
    }));
  }

  private async loadLabTaskActivityWeek(
    labOrganizationId: string,
    rangeEnd: Date,
    utcOffsetMinutes: number | undefined,
    period: ChartPeriodWeekMonth,
    charts: TodayCharts,
  ) {
    const dayBuckets = buildLocalDayBuckets(
      rangeEnd,
      daysForChartPeriod(period),
      'lookback',
      utcOffsetMinutes,
    );
    const weekStart = dayBuckets[0]?.start ?? rangeEnd;
    const weekEnd = rangeEnd;
    const offsetMs = (utcOffsetMinutes ?? 0) * 60_000;

    const completedCounts = new Map<string, number>();
    const receivedCounts = new Map<string, number>();
    for (const bucket of dayBuckets) {
      completedCounts.set(bucket.code, 0);
      receivedCounts.set(bucket.code, 0);
    }

    const completedTasks = await this.prisma.labCaseTask.findMany({
      where: {
        status: LabTaskStatus.COMPLETED,
        lastStatusChangedAt: { gte: weekStart, lt: weekEnd },
        labCase: {
          sends: { some: { organizationId: labOrganizationId } },
        },
      },
      select: { lastStatusChangedAt: true },
    });

    for (const task of completedTasks) {
      if (!task.lastStatusChangedAt) continue;
      const dayKey = localDayKeyFromDate(task.lastStatusChangedAt, offsetMs);
      if (completedCounts.has(dayKey)) {
        completedCounts.set(dayKey, (completedCounts.get(dayKey) ?? 0) + 1);
      }
    }

    const sends = await this.prisma.labCaseSend.findMany({
      where: {
        organizationId: labOrganizationId,
        sentAt: { gte: weekStart, lt: weekEnd },
      },
      include: {
        labCase: { select: { tasks: { select: { id: true } } } },
      },
    });

    for (const send of sends) {
      const dayKey = localDayKeyFromDate(send.sentAt, offsetMs);
      if (receivedCounts.has(dayKey)) {
        receivedCounts.set(
          dayKey,
          (receivedCounts.get(dayKey) ?? 0) + send.labCase.tasks.length,
        );
      }
    }

    charts.labTaskActivityWeek = dayBuckets.map((bucket) => ({
      code: bucket.code,
      label: bucket.label,
      completed: completedCounts.get(bucket.code) ?? 0,
      received: receivedCounts.get(bucket.code) ?? 0,
    }));
  }

  private async loadCasePartnersMonth(
    orgType: 'CLINIC' | 'LAB',
    organizationId: string,
    userId: string,
    scopeToUser: boolean,
    rangeEnd: Date,
    period: ChartPeriodWeekMonthYear,
    charts: TodayCharts,
  ) {
    const rangeStart = new Date(
      rangeEnd.getTime() - daysForChartPeriod(period) * 86_400_000,
    );

    const cases = await this.prisma.labCase.findMany({
      where: {
        sentAt: { gte: rangeStart, lt: rangeEnd },
        ...(orgType === 'CLINIC'
          ? {
              destinationOrganizationId: { not: null },
              treatment: {
                organizationId,
                ...(scopeToUser ? { providerUserId: userId } : {}),
              },
            }
          : {
              sends: { some: { organizationId } },
            }),
      },
      select: {
        destinationOrganizationId: true,
        partnerClinicOrganizationId: true,
        tasks: { select: { status: true } },
        treatment: { select: { organizationId: true } },
      },
    });

    const countsByPartner = new Map<string, { completed: number; total: number }>();

    for (const labCase of cases) {
      const partnerId =
        orgType === 'CLINIC'
          ? labCase.destinationOrganizationId
          : labCase.treatment?.organizationId ?? labCase.partnerClinicOrganizationId;
      if (!partnerId) continue;

      const isCompleted =
        labCase.tasks.length > 0 &&
        labCase.tasks.every((task) => task.status === LabTaskStatus.COMPLETED);

      const entry = countsByPartner.get(partnerId) ?? { completed: 0, total: 0 };
      entry.total += 1;
      if (isCompleted) entry.completed += 1;
      countsByPartner.set(partnerId, entry);
    }

    if (countsByPartner.size === 0) {
      charts.casePartnersMonth = [];
      return;
    }

    const sorted = [...countsByPartner.entries()]
      .map(([code, counts]) => ({
        code,
        completed: counts.completed,
        pending: counts.total - counts.completed,
        total: counts.total,
      }))
      .sort((a, b) => b.total - a.total)
      .slice(0, 8);

    const partners = await this.prisma.organization.findMany({
      where: { id: { in: sorted.map((row) => row.code) } },
      select: { id: true, name: true },
    });
    const nameById = new Map(partners.map((org) => [org.id, org.name]));

    charts.casePartnersMonth = sorted.map((row) => ({
      code: row.code,
      label: nameById.get(row.code) ?? row.code,
      completed: row.completed,
      pending: row.pending,
    }));
  }

  private getRequesterOrganizationId(sharedDataTypes: unknown): string | null {
    if (!sharedDataTypes || typeof sharedDataTypes !== 'object') {
      return null;
    }
    const requester = (sharedDataTypes as { requesterOrganizationId?: unknown })
      .requesterOrganizationId;
    return typeof requester === 'string' ? requester : null;
  }

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

    if (!membership) {
      throw new AppException(ErrorCode.PERMISSION_NOT_MEMBER, HttpStatus.FORBIDDEN);
    }

    return membership;
  }

  private resolvePermissionNames(membership: {
    isOwner: boolean;
    organization: {
      planId: string | null;
      plan?: { name: string } | null;
      type: { name: string };
    };
    permissions: { permission: { name: string } }[];
  }): string[] {
    return getEffectivePermissionNames(membership);
  }

  private assertCanViewToday(isOwner: boolean, permissionNames: string[]) {
    if (isOwner) return;
    if (!permissionNames.includes('TAB_TODAY_READ')) {
      throw new AppException(ErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN);
    }
  }

  private canViewAppointments(isOwner: boolean, names: string[]): boolean {
    if (isOwner) return true;
    return names.some((p) =>
      ['TAB_APPOINTMENTS_READ', 'TAB_APPOINTMENTS_EDIT'].includes(p),
    );
  }

  private canViewPatients(isOwner: boolean, names: string[]): boolean {
    if (isOwner) return true;
    return names.some((p) =>
      ['TAB_PATIENTS_READ', 'TAB_PATIENTS_EDIT'].includes(p),
    );
  }

  private canViewTreatment(_isOwner: boolean, names: string[]): boolean {
    return names.some((p) =>
      ['TAB_TREATMENT_READ', 'TAB_TREATMENT_EDIT'].includes(p),
    );
  }

  private canViewMyAppointmentsWeekChart(_isOwner: boolean, names: string[]): boolean {
    return names.includes('TAB_TREATMENT_EDIT');
  }

  private canViewCases(isOwner: boolean, names: string[]): boolean {
    if (isOwner) return true;
    return names.some((p) =>
      ['TAB_CASES_READ', 'TAB_CASES_EDIT'].includes(p),
    );
  }

  private canViewTasks(_isOwner: boolean, names: string[]): boolean {
    return names.some((p) =>
      ['TAB_TASKS_READ', 'TAB_TASKS_EDIT'].includes(p),
    );
  }

  private canEditTreatment(_isOwner: boolean, names: string[]): boolean {
    return names.includes('TAB_TREATMENT_EDIT');
  }

  private canEditCases(isOwner: boolean, names: string[]): boolean {
    if (isOwner) return true;
    return names.includes('TAB_CASES_EDIT');
  }

  private canManageOrganizations(isOwner: boolean, names: string[]): boolean {
    if (isOwner) return true;
    return names.includes('TAB_ORGANIZATIONS_EDIT');
  }

  private canViewStaff(isOwner: boolean, names: string[]): boolean {
    if (isOwner) return true;
    return names.some((p) =>
      ['TAB_STAFF_READ', 'TAB_STAFF_EDIT'].includes(p),
    );
  }
}

function aggregateCount(
  count: true | { _all?: number } | undefined,
): number {
  if (!count || count === true) return 0;
  return count._all ?? 0;
}

type LocalDayBucket = { code: string; label: string; start: Date; end: Date };

type ChartPeriodWeekMonth = 'week' | 'month';
type ChartPeriodWeekMonthYear = 'week' | 'month' | 'year';

function daysForChartPeriod(period: ChartPeriodWeekMonthYear): 7 | 30 | 365 {
  switch (period) {
    case 'month':
      return 30;
    case 'year':
      return 365;
    default:
      return 7;
  }
}

function resolveWeekMonthPeriod(
  value: string | undefined,
  fallback: ChartPeriodWeekMonth,
): ChartPeriodWeekMonth {
  return value === 'week' || value === 'month' ? value : fallback;
}

function resolveWeekMonthYearPeriod(
  value: string | undefined,
  fallback: ChartPeriodWeekMonthYear,
): ChartPeriodWeekMonthYear {
  return value === 'week' || value === 'month' || value === 'year' ? value : fallback;
}

function buildLocalDayBuckets(
  anchor: Date,
  count: number,
  direction: 'lookback' | 'forward',
  utcOffsetMinutes?: number,
): LocalDayBucket[] {
  const offsetMs = (utcOffsetMinutes ?? 0) * 60_000;
  const dayMs = 86_400_000;
  const buckets: LocalDayBucket[] = [];

  if (direction === 'lookback') {
    for (let index = 0; index < count; index += 1) {
      const start = new Date(anchor.getTime() - (count - index) * dayMs);
      const end = new Date(start.getTime() + dayMs);
      const code = localDayKeyFromDate(start, offsetMs);
      buckets.push({ code, label: code, start, end });
    }
    return buckets;
  }

  const localMs = anchor.getTime() + offsetMs;
  const local = new Date(localMs);
  local.setUTCHours(0, 0, 0, 0);
  const dayStart = new Date(local.getTime() - offsetMs);

  for (let index = 0; index < count; index += 1) {
    const start = new Date(dayStart.getTime() + index * dayMs);
    const end = new Date(start.getTime() + dayMs);
    const code = localDayKeyFromDate(start, offsetMs);
    buckets.push({ code, label: code, start, end });
  }

  return buckets;
}

function localDayKeyFromDate(date: Date, offsetMs: number): string {
  const localMs = date.getTime() + offsetMs;
  const local = new Date(localMs);
  const year = local.getUTCFullYear();
  const month = String(local.getUTCMonth() + 1).padStart(2, '0');
  const day = String(local.getUTCDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}
