import { getPrisma } from '../infrastructure/database/PrismaSingleton';
import { TeamResolutionService } from '../domain/team/TeamResolutionService';
import { NotificationFactory } from '../factories/NotificationFactory';
import { LeavesAppSettingsService } from '../services/LeavesAppSettingsService';
import { ForbiddenError, NotFoundError, ValidationError } from '../types/errors';
import { parseDateOnly, toDateOnly, getYearMonth, todayInCompanyTz, monthEndDate } from '../domain/leave/dateUtils';
import { HolidayRepository } from '../repositories/HolidayRepository';
import {
  computeDatesForWorkingDays,
  computeShiftWorkingDays,
  holidayDatesFromList,
  normalizeWorkingDaysInput,
  parseWorkingDays,
} from '../domain/shift/computeShiftWorkingDays';

export interface GenerateShiftDraftInput {
  shiftStartTime: string;
  shiftEndTime?: string;
  graceMinutes?: number;
  saturdayShiftEnabled?: boolean;
  sundayShiftEnabled?: boolean;
  holidayShiftEnabled?: boolean;
  /** When set, times + working days come from this admin-defined shift (holidays always skipped). */
  shiftDefinitionId?: string | null;
  employeeIds: string[];
}

interface FlatAssignmentRow {
  employeeId: string;
  date: string;
  startTime: string;
  endTime: string;
  notes?: string | null;
}

export interface ShiftInput {
  shiftStartTime: string;
  shiftEndTime?: string;
  graceMinutes?: number;
  saturdayShiftEnabled?: boolean;
  sundayShiftEnabled?: boolean;
  holidayShiftEnabled?: boolean;
  memberAssignments: Array<{
    employeeId: string;
    dates?: string[];
    shiftStartTime?: string;
    shiftEndTime?: string;
    notes?: string;
  }>;
}

export class TeamShiftFacade {
  constructor(
    private teamService = new TeamResolutionService(),
    private notifications: NotificationFactory,
    private holidayRepo = new HolidayRepository(),
    private appSettings = new LeavesAppSettingsService(),
  ) {}

  private prisma = getPrisma();

  private async minEditableMonth(): Promise<string> {
    const today = todayInCompanyTz();
    const currentMonth = getYearMonth(today);
    const allowCurrent = await this.appSettings.isShiftCurrentMonthEnabled();
    return allowCurrent ? currentMonth : this.nextMonthFrom(today);
  }

  private async assertMonthEditable(month: string) {
    const min = await this.minEditableMonth();
    if (month < min) {
      throw new ValidationError(
        `Shift planning for ${month} is not open. Minimum editable month is ${min}. Ask HR to enable current-month planning if needed.`,
      );
    }
  }

  private async assertPlannerIsManager(plannerEmployeeId: string, teamId: string) {
    const ok = await this.teamService.isTeamManager(plannerEmployeeId, teamId);
    if (!ok) {
      throw new ValidationError('Planner must be a manager on this team (is_manager=1)');
    }
  }

  private async getAssignedVerifierIds(teamId: string, month: string): Promise<string[]> {
    const rows = await this.prisma.team_shift_verifier_assignments.findMany({
      where: { team_id: teamId, effective_month: month },
    });
    return rows.map((r) => r.verifier_employee_id);
  }

  /** Only HR-assigned verifiers may approve; excludes submitter if listed. */
  private async resolveVerifierPool(teamId: string, month: string, submitter?: string | null) {
    const assigned = await this.getAssignedVerifierIds(teamId, month);
    return assigned.filter((id) => id !== submitter);
  }

  private async upsertDelegation(
    teamId: string,
    effectiveMonth: string,
    plannerEmployeeId: string,
    delegatedBy: string,
  ) {
    await this.prisma.team_shift_planner_delegations.upsert({
      where: { team_id_effective_month: { team_id: teamId, effective_month: effectiveMonth } },
      create: {
        team_id: teamId,
        planner_employee_id: plannerEmployeeId,
        effective_month: effectiveMonth,
        delegated_by: delegatedBy,
      },
      update: { planner_employee_id: plannerEmployeeId, delegated_by: delegatedBy },
    });

    const planner = await this.prisma.employees.findUnique({ where: { employee_id: plannerEmployeeId } });
    if (planner) {
      await this.notifications.createEmail('SHIFT_REMINDER_T5', {
        to: planner.email,
        month: effectiveMonth,
      }).send();
    }
    return { teamId, plannerEmployeeId, effectiveMonth };
  }

  async delegate(teamId: string, managerId: string, plannerEmployeeId: string, effectiveMonth: string) {
    await this.assertMonthEditable(effectiveMonth);
    const isLead = await this.teamService.isTeamLead(managerId, teamId);
    const isAdmin = await this.teamService.isAppAdmin(managerId);
    if (!isLead && !isAdmin) {
      throw new ForbiddenError('Only team lead or company admin can assign a shift planner');
    }
    await this.assertPlannerIsManager(plannerEmployeeId, teamId);
    return this.upsertDelegation(teamId, effectiveMonth, plannerEmployeeId, managerId);
  }

  async hrAssignPlanner(teamId: string, effectiveMonth: string, plannerEmployeeId: string, hrUsername: string) {
    await this.assertPlannerIsManager(plannerEmployeeId, teamId);
    const result = await this.upsertDelegation(teamId, effectiveMonth, plannerEmployeeId, `HR:${hrUsername}`);
    await this.prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'HR_ASSIGN_SHIFT_PLANNER',
        entity: 'team_shift_planner_delegations',
        entity_id: teamId,
        after_json: JSON.stringify({ effectiveMonth, plannerEmployeeId }),
      },
    });
    return result;
  }

  async hrAssignVerifiers(
    teamId: string,
    effectiveMonth: string,
    verifierEmployeeIds: string[],
    hrUsername: string,
  ) {
    const unique = [...new Set(verifierEmployeeIds.filter(Boolean))];
    if (unique.length !== 2) {
      throw new ValidationError('Assign exactly 2 verifiers (managers with is_manager=1 on this team)');
    }
    for (const id of unique) {
      await this.assertPlannerIsManager(id, teamId);
    }

    await this.prisma.team_shift_verifier_assignments.deleteMany({
      where: { team_id: teamId, effective_month: effectiveMonth },
    });
    for (const verifierEmployeeId of unique) {
      await this.prisma.team_shift_verifier_assignments.create({
        data: {
          team_id: teamId,
          effective_month: effectiveMonth,
          verifier_employee_id: verifierEmployeeId,
          assigned_by: `HR:${hrUsername}`,
        },
      });
    }

    await this.prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'HR_ASSIGN_SHIFT_VERIFIERS',
        entity: 'team_shift_verifier_assignments',
        entity_id: teamId,
        after_json: JSON.stringify({ effectiveMonth, verifierEmployeeIds: unique }),
      },
    });

    const emps = await this.prisma.employees.findMany({ where: { employee_id: { in: unique } } });
    return {
      teamId,
      effectiveMonth,
      verifiers: emps.map((e) => ({ employeeId: e.employee_id, name: e.name })),
    };
  }

  async getTeamShiftAssignments(teamId: string, month: string) {
    const team = await this.teamService.resolveTeamById(teamId);
    const managers = await this.teamService.listTeamManagers(teamId);
    const delegation = await this.prisma.team_shift_planner_delegations.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
    });
    const verifierIds = await this.getAssignedVerifierIds(teamId, month);
    const verifiers = verifierIds.length
      ? await this.prisma.employees.findMany({ where: { employee_id: { in: verifierIds } } })
      : [];

    return {
      teamId,
      teamName: team.teamName,
      effectiveMonth: month,
      managers,
      delegatedPlannerId: delegation?.planner_employee_id ?? null,
      verifierEmployeeIds: verifierIds,
      verifiers: verifiers.map((e) => ({ employeeId: e.employee_id, name: e.name })),
    };
  }

  private async listTeamMembersForShift(teamId: string, actorId?: string) {
    if (actorId) {
      return this.teamService.listVisibleTeamMembers(teamId, actorId).then((rows) =>
        rows.map((r) => ({
          employeeId: r.employeeId,
          name: r.name,
          managerLevel: r.managerLevel,
          roleLabel: r.roleLabel,
          isManager: r.isManager,
          isAdmin: r.isAdmin,
        })),
      );
    }
    const teamCtx = await this.teamService.resolveTeamById(teamId);
    const employees = await this.prisma.employees.findMany({
      where: {
        employee_id: { in: teamCtx.memberBusinessIds },
        status: 'active',
        is_manager: { in: [0, 1, 2] },
      },
      orderBy: { name: 'asc' },
    });
    return employees.map((e) => ({
      employeeId: e.employee_id,
      name: e.name,
      managerLevel: e.is_manager ?? 0,
      roleLabel: e.is_manager === 2 ? 'Admin' : e.is_manager === 1 ? 'Manager' : 'Employee',
      isManager: e.is_manager === 1,
      isAdmin: e.is_manager === 2,
    }));
  }

  private async listEditableEmployeeIds(teamId: string, month: string, actorId: string): Promise<string[] | null> {
    const access = await this.resolveEditAccess(teamId, month, actorId);
    if (!access.canEdit) return [];
    if (access.editScope === 'ALL') return null;
    return access.editableEmployeeIds;
  }

  async listShiftDefinitions(teamId: string) {
    const rows = await this.prisma.team_shift_definitions.findMany({
      where: { team_id: teamId, active: true },
      orderBy: { name: 'asc' },
    });
    return {
      teamId,
      items: rows.map((r) => this.mapShiftDefinition(r)),
    };
  }

  private mapShiftDefinition(r: {
    id: string;
    name: string;
    start_time: string;
    end_time: string;
    grace_minutes: number;
    working_days: string;
  }) {
    const workingDays = [...parseWorkingDays(r.working_days)].sort((a, b) => a - b);
    return {
      id: r.id,
      name: r.name,
      startTime: r.start_time,
      endTime: r.end_time,
      graceMinutes: r.grace_minutes,
      workingDays,
      workingDaysLabel: this.formatWorkingDaysLabel(workingDays),
    };
  }

  private formatWorkingDaysLabel(days: number[]): string {
    const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
    if (days.length === 5 && days.join(',') === '1,2,3,4,5') return 'Mon–Fri';
    if (days.length === 6 && days.join(',') === '1,2,3,4,5,6') return 'Mon–Sat';
    if (days.length === 7) return 'Every day';
    return days.map((d) => labels[d] ?? String(d)).join(', ');
  }

  async upsertShiftDefinition(
    teamId: string,
    actorLabel: string,
    input: {
      id?: string;
      name: string;
      startTime: string;
      endTime: string;
      graceMinutes?: number;
      workingDays?: number[];
    },
  ) {
    const name = input.name.trim();
    if (!name) throw new ValidationError('Shift name is required');
    if (!/^\d{2}:\d{2}$/.test(input.startTime) || !/^\d{2}:\d{2}$/.test(input.endTime)) {
      throw new ValidationError('Start and end times must be HH:MM');
    }
    let workingDaysRaw: string;
    try {
      workingDaysRaw = normalizeWorkingDaysInput(input.workingDays ?? [1, 2, 3, 4, 5]);
    } catch {
      throw new ValidationError('Select at least one working day');
    }

    const grace = input.graceMinutes ?? 10;
    const saved = await this.upsertShiftDefinitionOnTeam(teamId, actorLabel, {
      id: input.id,
      name,
      startTime: input.startTime,
      endTime: input.endTime,
      graceMinutes: grace,
      workingDaysRaw,
    });

    // Keep the same named shift on every active team so people stay consistent across teams
    const allTeams = await this.teamService.listAllActiveTeams();
    for (const t of allTeams) {
      if (t.teamId === teamId) continue;
      await this.upsertShiftDefinitionOnTeam(t.teamId, actorLabel, {
        name,
        startTime: input.startTime,
        endTime: input.endTime,
        graceMinutes: grace,
        workingDaysRaw,
      });
    }

    return saved;
  }

  private async upsertShiftDefinitionOnTeam(
    teamId: string,
    actorLabel: string,
    input: {
      id?: string;
      name: string;
      startTime: string;
      endTime: string;
      graceMinutes: number;
      workingDaysRaw: string;
    },
  ) {
    if (input.id) {
      const owned = await this.prisma.team_shift_definitions.findFirst({
        where: { id: input.id, team_id: teamId },
      });
      if (owned) {
        const updated = await this.prisma.team_shift_definitions.update({
          where: { id: input.id },
          data: {
            name: input.name,
            start_time: input.startTime,
            end_time: input.endTime,
            grace_minutes: input.graceMinutes,
            working_days: input.workingDaysRaw,
            active: true,
          },
        });
        return this.mapShiftDefinition(updated);
      }
    }

    const existing = await this.prisma.team_shift_definitions.findFirst({
      where: { team_id: teamId, name: input.name },
    });
    if (existing) {
      const updated = await this.prisma.team_shift_definitions.update({
        where: { id: existing.id },
        data: {
          start_time: input.startTime,
          end_time: input.endTime,
          grace_minutes: input.graceMinutes,
          working_days: input.workingDaysRaw,
          active: true,
          created_by: actorLabel,
        },
      });
      return this.mapShiftDefinition(updated);
    }

    const created = await this.prisma.team_shift_definitions.create({
      data: {
        team_id: teamId,
        name: input.name,
        start_time: input.startTime,
        end_time: input.endTime,
        grace_minutes: input.graceMinutes,
        working_days: input.workingDaysRaw,
        created_by: actorLabel,
      },
    });
    return this.mapShiftDefinition(created);
  }

  async deleteShiftDefinition(teamId: string, definitionId: string) {
    const row = await this.prisma.team_shift_definitions.findFirst({
      where: { id: definitionId, team_id: teamId },
    });
    if (!row) throw new NotFoundError('Shift definition not found');
    await this.prisma.team_shift_definitions.update({
      where: { id: definitionId },
      data: { active: false },
    });
    // Soft-delete same-named shift on other teams
    await this.prisma.team_shift_definitions.updateMany({
      where: { name: row.name, team_id: { not: teamId }, active: true },
      data: { active: false },
    });
    return { deleted: true, id: definitionId };
  }

  /** Company admin or team co-lead may create/update/delete named shift definitions. */
  async assertCanManageDefinitions(teamId: string, actorId: string) {
    if (await this.teamService.isAppAdmin(actorId)) return;
    if (await this.teamService.isTeamLead(actorId, teamId)) return;
    throw new ForbiddenError('Only company admin or team co-lead can add or remove shift definitions');
  }

  /** Admin-published schedules skip verifier approval. */
  private async markScheduleApproved(
    scheduleId: string,
    actorId: string,
  ) {
    return this.prisma.team_shift_schedules.update({
      where: { id: scheduleId },
      data: {
        status: 'APPROVED',
        submitted_by_employee_id: actorId,
        submitted_at: new Date(),
      },
      include: { team_shift_member_assignments: true, team_shift_approvals: true },
    });
  }

  private mapTimingAssignmentRow(
    r: {
      employee_id: string;
      manager_employee_id: string;
      shift_definition_id: string | null;
      shift_start_time: string | null;
      shift_end_time: string | null;
    },
    extra?: {
      readOnly?: boolean;
      lockedReason?: string | null;
      managerName?: string | null;
      shiftName?: string | null;
      originTeamId?: string | null;
      originTeamName?: string | null;
    },
  ) {
    const mapped = Boolean(r.shift_definition_id || (r.shift_start_time && r.shift_end_time));
    return {
      employeeId: r.employee_id,
      managerEmployeeId: r.manager_employee_id,
      shiftDefinitionId: r.shift_definition_id,
      shiftStartTime: r.shift_start_time,
      shiftEndTime: r.shift_end_time,
      readOnly: extra?.readOnly ?? mapped,
      lockedReason: extra?.lockedReason ?? (mapped ? 'Already mapped for this month' : null),
      managerName: extra?.managerName ?? null,
      shiftName: extra?.shiftName ?? null,
      originTeamId: extra?.originTeamId ?? null,
      originTeamName: extra?.originTeamName ?? null,
    };
  }

  private async enrichTimingAssignments(
    teamId: string,
    month: string,
    employeeIds: string[],
    localRows: Array<{
      employee_id: string;
      manager_employee_id: string;
      shift_definition_id: string | null;
      shift_start_time: string | null;
      shift_end_time: string | null;
    }>,
  ) {
    if (!employeeIds.length) return [];

    const [crossRows, schedule] = await Promise.all([
      this.prisma.team_shift_timing_managers.findMany({
        where: {
          effective_month: month,
          employee_id: { in: employeeIds },
          team_id: { not: teamId },
        },
      }),
      this.prisma.team_shift_schedules.findUnique({
        where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
        include: {
          team_shift_member_assignments: {
            where: { employee_id: { in: employeeIds } },
            select: { employee_id: true },
          },
        },
      }),
    ]);

    const hasDayAssignments = new Set(
      (schedule?.team_shift_member_assignments ?? []).map((a) => a.employee_id),
    );

    const localByEmp = new Map(localRows.map((r) => [r.employee_id, r]));
    const crossByEmp = new Map<string, (typeof crossRows)[0]>();
    for (const row of crossRows) {
      if (!row.shift_definition_id && !(row.shift_start_time && row.shift_end_time)) continue;
      if (!crossByEmp.has(row.employee_id)) crossByEmp.set(row.employee_id, row);
    }

    const managerIds = [
      ...new Set([
        ...localRows.map((r) => r.manager_employee_id),
        ...crossRows.map((r) => r.manager_employee_id),
      ]),
    ].filter(Boolean);
    const defIds = [
      ...new Set(
        [...localRows, ...crossRows]
          .map((r) => r.shift_definition_id)
          .filter((id): id is string => Boolean(id)),
      ),
    ];
    const originTeamIds = [...new Set(crossRows.map((r) => r.team_id))];

    const [managers, defs, originTeams] = await Promise.all([
      managerIds.length
        ? this.prisma.employees.findMany({
            where: { employee_id: { in: managerIds } },
            select: { employee_id: true, name: true },
          })
        : Promise.resolve([]),
      defIds.length
        ? this.prisma.team_shift_definitions.findMany({ where: { id: { in: defIds } } })
        : Promise.resolve([]),
      originTeamIds.length
        ? this.prisma.l10_teams.findMany({
            where: { id: { in: originTeamIds } },
            select: { id: true, name: true },
          })
        : Promise.resolve([]),
    ]);
    const managerNameById = new Map(managers.map((m) => [m.employee_id, m.name]));
    const defById = new Map(defs.map((d) => [d.id, d]));
    const teamNameById = new Map(originTeams.map((t) => [t.id, t.name]));

    const out: Array<{
      employeeId: string;
      managerEmployeeId: string;
      shiftDefinitionId: string | null;
      shiftStartTime: string | null;
      shiftEndTime: string | null;
      readOnly: boolean;
      lockedReason: string | null;
      managerName: string | null;
      shiftName: string | null;
      originTeamId: string | null;
      originTeamName: string | null;
    }> = [];

    for (const empId of employeeIds) {
      const local = localByEmp.get(empId);
      const cross = crossByEmp.get(empId);
      const applied = hasDayAssignments.has(empId);

      if (local) {
        const mapped = Boolean(local.shift_definition_id || (local.shift_start_time && local.shift_end_time));
        const lockedByOther = Boolean(cross);
        // Lock when already applied for the month, or when another team owns the mapping.
        // Draft local mapping stays editable so wrong managers can be fixed before apply.
        const readOnly = applied || lockedByOther;
        const def = local.shift_definition_id ? defById.get(local.shift_definition_id) : undefined;
        out.push(
          this.mapTimingAssignmentRow(local, {
            readOnly,
            lockedReason: applied
              ? 'Shift already applied for this month — read only'
              : lockedByOther
                ? `Mapped on ${teamNameById.get(cross!.team_id) ?? 'another team'} — read only`
                : null,
            managerName: lockedByOther
              ? null
              : managerNameById.get(local.manager_employee_id) ?? null,
            shiftName: def?.name ?? null,
            originTeamId: lockedByOther ? cross!.team_id : null,
            originTeamName: lockedByOther ? teamNameById.get(cross!.team_id) ?? null : null,
          }),
        );
        continue;
      }

      if (cross) {
        const def = cross.shift_definition_id ? defById.get(cross.shift_definition_id) : undefined;
        out.push(
          this.mapTimingAssignmentRow(cross, {
            readOnly: true,
            lockedReason: `Already mapped on ${teamNameById.get(cross.team_id) ?? 'another team'} — read only`,
            managerName: null,
            shiftName: def?.name ?? null,
            originTeamId: cross.team_id,
            originTeamName: teamNameById.get(cross.team_id) ?? null,
          }),
        );
      }
    }

    return out;
  }

  /**
   * Apply admin-defined mappings: generate month assignments using each employee's mapped shift times.
   */
  async applyMappedShiftDefinitions(
    teamId: string,
    month: string,
    actorId: string | null,
  ) {
    const team = await this.teamService.resolveTeamById(teamId);
    const saveActor = actorId ?? team.leadBusinessId;
    if (!saveActor) {
      throw new ValidationError('Team has no lead; assign a planner/lead before applying mapped shifts');
    }

    if (actorId) {
      await this.assertMonthEditable(month);
      await this.assertCanEdit(teamId, month, actorId);
      const access = await this.resolveEditAccess(teamId, month, actorId);
      if (access.editScope !== 'ALL') {
        throw new ForbiddenError('Only admin/lead/planner can apply mapped shifts for the whole team');
      }
    } else {
      await this.assertMonthEditable(month);
    }

    const mappings = await this.prisma.team_shift_timing_managers.findMany({
      where: { team_id: teamId, effective_month: month },
    });
    if (!mappings.length) {
      throw new ValidationError('No employee mappings found. Map employees to a shift (and manager) first.');
    }

    const schedule = await this.prisma.team_shift_schedules.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
    });

    const defaultStart = schedule?.shift_start_time ?? mappings[0].shift_start_time ?? '09:30';
    const defaultEnd = schedule?.shift_end_time ?? mappings[0].shift_end_time ?? '18:30';

    const holidays = await this.holidayRepo.listInRange(
      parseDateOnly(`${month}-01`),
      parseDateOnly(monthEndDate(month)),
    );
    const holidayDates = holidayDatesFromList(holidays, month);

    const defIds = [...new Set(mappings.map((m) => m.shift_definition_id).filter(Boolean))] as string[];
    const defs = defIds.length
      ? await this.prisma.team_shift_definitions.findMany({ where: { id: { in: defIds } } })
      : [];
    const defMap = new Map(defs.map((d) => [d.id, d]));

    // Fallback weekdays when mapping has times but no definition: Mon–Fri, never holidays
    const defaultDates = computeDatesForWorkingDays(month, parseWorkingDays('1,2,3,4,5'), holidayDates);

    const memberAssignments = mappings.map((m) => {
      const def = m.shift_definition_id ? defMap.get(m.shift_definition_id) : null;
      const dates = def
        ? computeDatesForWorkingDays(month, parseWorkingDays(def.working_days), holidayDates)
        : defaultDates;
      return {
        employeeId: m.employee_id,
        dates,
        shiftStartTime: m.shift_start_time ?? def?.start_time ?? defaultStart,
        shiftEndTime: m.shift_end_time ?? def?.end_time ?? defaultEnd,
      };
    });

    const mappedIds = new Set(mappings.map((m) => m.employee_id));
    if (schedule) {
      const existing = await this.prisma.team_shift_member_assignments.findMany({
        where: { schedule_id: schedule.id },
      });
      const byEmp = new Map<string, { dates: string[]; start: string; end: string }>();
      for (const row of existing) {
        if (mappedIds.has(row.employee_id)) continue;
        const date = toDateOnly(row.date);
        const cur = byEmp.get(row.employee_id) ?? { dates: [], start: row.start_time, end: row.end_time };
        if (!cur.dates.includes(date)) cur.dates.push(date);
        byEmp.set(row.employee_id, cur);
      }
      for (const [employeeId, v] of byEmp) {
        memberAssignments.push({
          employeeId,
          dates: v.dates,
          shiftStartTime: v.start,
          shiftEndTime: v.end,
        });
      }
    }

    return this.saveDraft(teamId, month, saveActor, {
      shiftStartTime: defaultStart!,
      shiftEndTime: defaultEnd!,
      saturdayShiftEnabled: schedule?.saturday_shift_enabled ?? false,
      sundayShiftEnabled: schedule?.sunday_shift_enabled ?? false,
      holidayShiftEnabled: false,
      memberAssignments,
    }).then(async (saved) => {
      // Company admin (or HR acting as admin) — publish as approved, no verifier flow
      const adminActor = actorId && (await this.teamService.isAppAdmin(actorId));
      if (adminActor || !actorId) {
        const row = await this.prisma.team_shift_schedules.findUnique({
          where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
        });
        if (row) {
          await this.markScheduleApproved(row.id, saveActor);
          if (actorId) return (await this.getSchedule(teamId, month, actorId)) ?? saved;
          return (await this.getSchedule(teamId, month)) ?? saved;
        }
      }
      return saved;
    });
  }

  async getTimingManagerAssignments(teamId: string, month: string, actorId: string) {
    const access = await this.resolveEditAccess(teamId, month, actorId);
    if (!access.canAssignTimingManagers) {
      throw new ForbiddenError('Not allowed to view timing manager assignments');
    }

    const rows = await this.prisma.team_shift_timing_managers.findMany({
      where: { team_id: teamId, effective_month: month },
    });
    const managers = await this.teamService.listTeamManagers(teamId);
    const members = await this.teamService.listVisibleTeamMembers(teamId, actorId);
    const assignableEmployees = members.filter((m) => !m.isAdmin && m.employeeId !== actorId);
    const employees = assignableEmployees.length
      ? assignableEmployees
      : members.filter((m) => m.managerLevel === 0 || m.managerLevel === 1);
    const definitions = await this.listShiftDefinitions(teamId);
    const assignments = await this.enrichTimingAssignments(
      teamId,
      month,
      employees.map((e) => e.employeeId),
      rows,
    );

    return {
      teamId,
      effectiveMonth: month,
      canAssign: access.canAssignTimingManagers,
      managers,
      shiftDefinitions: definitions.items,
      employees,
      assignments,
    };
  }

  async saveTimingManagerAssignments(
    teamId: string,
    month: string,
    actorId: string,
    assignments: Array<{
      employeeId: string;
      managerEmployeeId: string | null;
      shiftDefinitionId?: string | null;
      shiftStartTime?: string | null;
      shiftEndTime?: string | null;
    }>,
  ) {
    const access = await this.resolveEditAccess(teamId, month, actorId);
    if (!access.canAssignTimingManagers) {
      throw new ForbiddenError('Only team admin, team lead, or shift planner can assign managers for employee timings');
    }

    return this.persistTimingManagerAssignments(teamId, month, actorId, assignments, actorId);
  }

  async hrGetTimingManagerAssignments(teamId: string, month: string) {
    const team = await this.teamService.resolveTeamById(teamId);
    const rows = await this.prisma.team_shift_timing_managers.findMany({
      where: { team_id: teamId, effective_month: month },
    });
    const managers = await this.teamService.listTeamManagers(teamId);
    const employees = await this.prisma.employees.findMany({
      where: {
        employee_id: { in: team.memberBusinessIds },
        status: 'active',
        is_manager: { in: [0, 1] },
      },
      orderBy: { name: 'asc' },
    });
    const employeeRows = employees.map((e) => ({
      employeeId: e.employee_id,
      name: e.name,
    }));
    const definitions = await this.listShiftDefinitions(teamId);
    const assignments = await this.enrichTimingAssignments(
      teamId,
      month,
      employeeRows.map((e) => e.employeeId),
      rows,
    );

    return {
      teamId,
      teamName: team.teamName,
      effectiveMonth: month,
      canAssign: true,
      managers,
      shiftDefinitions: definitions.items,
      employees: employeeRows,
      assignments,
    };
  }

  async hrSaveTimingManagerAssignments(
    teamId: string,
    month: string,
    assignments: Array<{
      employeeId: string;
      managerEmployeeId: string | null;
      shiftDefinitionId?: string | null;
      shiftStartTime?: string | null;
      shiftEndTime?: string | null;
    }>,
    hrUsername: string,
  ) {
    return this.persistTimingManagerAssignments(teamId, month, null, assignments, `HR:${hrUsername}`);
  }

  private async persistTimingManagerAssignments(
    teamId: string,
    month: string,
    viewerEmployeeId: string | null,
    assignments: Array<{
      employeeId: string;
      managerEmployeeId: string | null;
      shiftDefinitionId?: string | null;
      shiftStartTime?: string | null;
      shiftEndTime?: string | null;
    }>,
    assignedBy: string,
  ) {
    const managers = await this.teamService.listTeamManagers(teamId);
    const managerIds = new Set(managers.map((m) => m.employeeId));
    const team = await this.teamService.resolveTeamById(teamId);
    const employees = await this.prisma.employees.findMany({
      where: {
        employee_id: { in: team.memberBusinessIds },
        status: 'active',
        is_manager: { in: [0, 1] },
      },
    });
    const employeeIds = new Set(employees.map((e) => e.employee_id));
    const definitions = await this.prisma.team_shift_definitions.findMany({
      where: { team_id: teamId, active: true },
    });
    const defMap = new Map(definitions.map((d) => [d.id, d]));

    const existingLocal = await this.prisma.team_shift_timing_managers.findMany({
      where: { team_id: teamId, effective_month: month },
    });
    const schedule = await this.prisma.team_shift_schedules.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
      include: {
        team_shift_member_assignments: {
          select: { employee_id: true },
        },
      },
    });
    const appliedEmployeeIds = new Set(
      (schedule?.team_shift_member_assignments ?? []).map((a) => a.employee_id),
    );
    // Only freeze people who already have generated day rows, or who are owned by another team
    const lockedLocal = new Map(
      existingLocal.filter((r) => appliedEmployeeIds.has(r.employee_id)).map((r) => [r.employee_id, r]),
    );

    const crossMapped = await this.prisma.team_shift_timing_managers.findMany({
      where: {
        effective_month: month,
        employee_id: { in: [...employeeIds] },
        team_id: { not: teamId },
        OR: [
          { shift_definition_id: { not: null } },
          { AND: [{ shift_start_time: { not: null } }, { shift_end_time: { not: null } }] },
        ],
      },
    });
    const lockedFromOther = new Set(crossMapped.map((r) => r.employee_id));

    // Manager is optional — only shift (or manager) needed to keep a row. Applied/cross-team stay locked.
    const rows = assignments.filter((a) => a.employeeId && (a.managerEmployeeId || a.shiftDefinitionId));
    const prepared: Array<{
      employeeId: string;
      managerEmployeeId: string;
      shiftDefinitionId: string | null;
      shiftStartTime: string | null;
      shiftEndTime: string | null;
      shiftName: string | null;
      workingDays: string | null;
    }> = [];
    const touched = new Set<string>();

    for (const row of rows) {
      if (!employeeIds.has(row.employeeId)) {
        throw new ValidationError(`Employee ${row.employeeId} is not assignable on this team`);
      }
      // Already applied or mapped on another team — ignore edit attempts
      if (lockedLocal.has(row.employeeId) || lockedFromOther.has(row.employeeId)) {
        continue;
      }
      if (row.managerEmployeeId && !managerIds.has(row.managerEmployeeId)) {
        throw new ValidationError(`Manager ${row.managerEmployeeId} is not a team manager (is_manager=1)`);
      }
      if (row.managerEmployeeId && row.employeeId === row.managerEmployeeId) {
        throw new ValidationError('An employee cannot be assigned as their own timing manager');
      }

      let start = row.shiftStartTime ?? null;
      let end = row.shiftEndTime ?? null;
      let defId = row.shiftDefinitionId ?? null;
      let defName: string | null = null;
      let workingDays: string | null = null;
      if (defId) {
        const def = defMap.get(defId);
        if (!def) throw new ValidationError(`Unknown shift definition ${defId}`);
        start = def.start_time;
        end = def.end_time;
        defName = def.name;
        workingDays = def.working_days;
      }

      // Manager optional: fall back to team lead only for NOT NULL column
      const managerEmployeeId =
        row.managerEmployeeId || team.leadBusinessId || managers[0]?.employeeId || row.employeeId;

      prepared.push({
        employeeId: row.employeeId,
        managerEmployeeId,
        shiftDefinitionId: defId,
        shiftStartTime: start,
        shiftEndTime: end,
        shiftName: defName,
        workingDays,
      });
      touched.add(row.employeeId);
    }

    // Preserve locked local rows unchanged
    for (const [empId, locked] of lockedLocal) {
      if (touched.has(empId)) continue;
      const def = locked.shift_definition_id ? defMap.get(locked.shift_definition_id) : undefined;
      prepared.push({
        employeeId: empId,
        managerEmployeeId: locked.manager_employee_id,
        shiftDefinitionId: locked.shift_definition_id,
        shiftStartTime: locked.shift_start_time,
        shiftEndTime: locked.shift_end_time,
        shiftName: def?.name ?? null,
        workingDays: def?.working_days ?? null,
      });
      touched.add(empId);
    }

    // Ensure cross-team locked people have a local mirror (shift only; local lead as manager placeholder)
    for (const cross of crossMapped) {
      if (touched.has(cross.employee_id) || !employeeIds.has(cross.employee_id)) continue;
      let defId: string | null = null;
      let defName: string | null = null;
      let workingDays: string | null = null;
      let start = cross.shift_start_time;
      let end = cross.shift_end_time;
      if (cross.shift_definition_id) {
        const srcDef = await this.prisma.team_shift_definitions.findUnique({
          where: { id: cross.shift_definition_id },
        });
        if (srcDef) {
          defName = srcDef.name;
          workingDays = srcDef.working_days;
          start = srcDef.start_time;
          end = srcDef.end_time;
          const localDef = await this.prisma.team_shift_definitions.findFirst({
            where: { team_id: teamId, name: srcDef.name },
          });
          defId = localDef?.id ?? null;
          if (!defId) {
            const created = await this.prisma.team_shift_definitions.create({
              data: {
                team_id: teamId,
                name: srcDef.name,
                start_time: srcDef.start_time,
                end_time: srcDef.end_time,
                grace_minutes: srcDef.grace_minutes,
                working_days: srcDef.working_days,
                created_by: assignedBy,
                active: true,
              },
            });
            defId = created.id;
          }
        }
      }
      prepared.push({
        employeeId: cross.employee_id,
        managerEmployeeId: team.leadBusinessId || managers[0]?.employeeId || cross.employee_id,
        shiftDefinitionId: defId,
        shiftStartTime: start,
        shiftEndTime: end,
        shiftName: defName,
        workingDays,
      });
      touched.add(cross.employee_id);
    }

    await this.prisma.$transaction(
      async (tx) => {
        await tx.team_shift_timing_managers.deleteMany({
          where: { team_id: teamId, effective_month: month },
        });
        if (prepared.length) {
          await tx.team_shift_timing_managers.createMany({
            data: prepared.map((row) => ({
              team_id: teamId,
              effective_month: month,
              employee_id: row.employeeId,
              manager_employee_id: row.managerEmployeeId,
              shift_definition_id: row.shiftDefinitionId,
              shift_start_time: row.shiftStartTime,
              shift_end_time: row.shiftEndTime,
              assigned_by: assignedBy,
            })),
          });
        }
      },
      { timeout: 30000 },
    );

    // Only sync newly editable mappings (not already-locked locals) to other teams — shift times only
    for (const row of prepared) {
      if (lockedLocal.has(row.employeeId) || lockedFromOther.has(row.employeeId)) continue;
      if (!row.shiftStartTime || !row.shiftEndTime) continue;
      await this.syncEmployeeShiftMappingToOtherTeams({
        sourceTeamId: teamId,
        month,
        employeeId: row.employeeId,
        shiftStartTime: row.shiftStartTime,
        shiftEndTime: row.shiftEndTime,
        shiftName: row.shiftName,
        workingDays: row.workingDays,
        assignedBy,
      });
    }

    if (viewerEmployeeId) {
      return this.getTimingManagerAssignments(teamId, month, viewerEmployeeId);
    }
    return this.hrGetTimingManagerAssignments(teamId, month);
  }

  /**
   * A person's mapped shift must be the same on every team they belong to.
   * Runs outside the source-team transaction so remote DB latency cannot abort the save.
   */
  private async syncEmployeeShiftMappingToOtherTeams(input: {
    sourceTeamId: string;
    month: string;
    employeeId: string;
    shiftStartTime: string;
    shiftEndTime: string;
    shiftName: string | null;
    workingDays: string | null;
    assignedBy: string;
  }) {
    const teamIds = await this.teamService.listTeamIdsForEmployee(input.employeeId);
    for (const otherTeamId of teamIds) {
      if (otherTeamId === input.sourceTeamId) continue;

      let defId: string | null = null;
      if (input.shiftName) {
        let def = await this.prisma.team_shift_definitions.findFirst({
          where: { team_id: otherTeamId, name: input.shiftName },
        });
        if (!def) {
          def = await this.prisma.team_shift_definitions.create({
            data: {
              team_id: otherTeamId,
              name: input.shiftName,
              start_time: input.shiftStartTime,
              end_time: input.shiftEndTime,
              working_days: input.workingDays ?? '1,2,3,4,5',
              created_by: input.assignedBy,
              active: true,
            },
          });
        } else if (!def.active) {
          def = await this.prisma.team_shift_definitions.update({
            where: { id: def.id },
            data: {
              active: true,
              start_time: input.shiftStartTime,
              end_time: input.shiftEndTime,
              working_days: input.workingDays ?? def.working_days,
            },
          });
        }
        defId = def.id;
      }

      const existingOther = await this.prisma.team_shift_timing_managers.findFirst({
        where: {
          team_id: otherTeamId,
          effective_month: input.month,
          employee_id: input.employeeId,
        },
      });

      // Never copy another team's manager. Keep existing local manager, else use that team's lead.
      const otherTeam = await this.teamService.resolveTeamById(otherTeamId);
      const otherManagers = await this.teamService.listTeamManagers(otherTeamId);
      const managerId =
        existingOther?.manager_employee_id ||
        otherTeam.leadBusinessId ||
        otherManagers[0]?.employeeId ||
        input.employeeId;

      if (existingOther) {
        await this.prisma.team_shift_timing_managers.update({
          where: { id: existingOther.id },
          data: {
            manager_employee_id: managerId,
            shift_definition_id: defId ?? existingOther.shift_definition_id,
            shift_start_time: input.shiftStartTime,
            shift_end_time: input.shiftEndTime,
            assigned_by: input.assignedBy,
          },
        });
      } else {
        await this.prisma.team_shift_timing_managers.create({
          data: {
            team_id: otherTeamId,
            effective_month: input.month,
            employee_id: input.employeeId,
            manager_employee_id: managerId,
            shift_definition_id: defId,
            shift_start_time: input.shiftStartTime,
            shift_end_time: input.shiftEndTime,
            assigned_by: input.assignedBy,
          },
        });
      }
    }
  }

  async getSchedule(teamId: string, month: string, actorId?: string) {
    const schedule = await this.prisma.team_shift_schedules.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
      include: { team_shift_member_assignments: true, team_shift_approvals: true },
    });

    const shiftPlanning = await this.buildShiftPlanningConfig();
    const monthEditable = month >= shiftPlanning.minEditableMonth;
    const teamMembers = actorId ? await this.listTeamMembersForShift(teamId, actorId) : [];

    if (!schedule) {
      if (!actorId) return null;
      const access = await this.resolveEditAccess(teamId, month, actorId);
      return {
        teamId,
        effectiveMonth: month,
        status: 'MISSING',
        shiftStartTime: '09:30',
        shiftEndTime: '18:30',
        graceMinutes: 10,
        saturdayShiftEnabled: false,
        sundayShiftEnabled: false,
        holidayShiftEnabled: false,
        submittedByEmployeeId: null,
        approvalCount: 0,
        memberAssignments: [],
        teamMembers,
        canEdit: monthEditable && access.canEdit,
        canApprove: false,
        isShiftResponsible: access.isShiftResponsible,
        isAdmin: access.isAdmin,
        isTeamLead: access.isTeamLead,
        canDefineShifts: access.isAdmin || access.isTeamLead,
        canAssignTimingManagers: access.canAssignTimingManagers,
        editScope: access.editScope,
        editableEmployeeIds: access.editableEmployeeIds,
        responsibleEmployeeId: access.responsibleEmployeeId,
        responsibleName: access.responsibleName,
        responsibleRole: access.responsibleRole,
        delegatedPlannerId: access.delegatedPlannerId,
        approverEmployeeIds: [],
        eligibleVerifiers: [],
        monthEditable,
        shiftPlanning,
      };
    }

    const mapped = this.mapSchedule(schedule);
    if (!actorId) return mapped;

    const access = await this.resolveEditAccess(teamId, month, actorId);
    const visibleIds = new Set(teamMembers.map((m) => m.employeeId));
    const memberAssignments = mapped.memberAssignments.filter((a) => visibleIds.has(a.employeeId));
    const approverIds = schedule.team_shift_approvals.map((a) => a.approver_employee_id);
    const submitter = schedule.submitted_by_employee_id;
    const eligibleVerifierIds = (await this.resolveVerifierPool(teamId, month, submitter)).filter(
      (id) => !approverIds.includes(id),
    );

    const verifierEmployees = await this.prisma.employees.findMany({
      where: { employee_id: { in: eligibleVerifierIds } },
    });

    const verifierPool = await this.resolveVerifierPool(teamId, month, submitter);
    const editableStatuses = new Set(['DRAFT', 'REJECTED', 'MISSING']);
    const plannerCanRecall =
      schedule.status === 'PENDING_APPROVAL' && access.isShiftResponsible;
    const adminCanRevise = access.isAdmin && schedule.status === 'APPROVED';

    return {
      ...mapped,
      memberAssignments,
      canEdit:
        monthEditable &&
        access.canEdit &&
        (editableStatuses.has(schedule.status) || plannerCanRecall || adminCanRevise),
      canApprove:
        schedule.status === 'PENDING_APPROVAL' &&
        submitter !== actorId &&
        !approverIds.includes(actorId) &&
        verifierPool.includes(actorId),
      isShiftResponsible: access.isShiftResponsible,
      isAdmin: access.isAdmin,
      isTeamLead: access.isTeamLead,
      canDefineShifts: access.isAdmin || access.isTeamLead,
      canAssignTimingManagers: access.canAssignTimingManagers,
      editScope: access.editScope,
      editableEmployeeIds: access.editableEmployeeIds,
      responsibleEmployeeId: access.responsibleEmployeeId,
      responsibleName: access.responsibleName,
      responsibleRole: access.responsibleRole,
      delegatedPlannerId: access.delegatedPlannerId,
      approverEmployeeIds: approverIds,
      eligibleVerifiers: verifierEmployees.map((e) => ({
        employeeId: e.employee_id,
        name: e.name,
      })),
      monthEditable,
      shiftPlanning,
      teamMembers,
    };
  }

  private flattenMemberAssignments(input: ShiftInput): FlatAssignmentRow[] {
    const seen = new Set<string>();
    const rows: FlatAssignmentRow[] = [];
    const defaultEnd = input.shiftEndTime ?? '18:30';

    for (const ma of input.memberAssignments) {
      const dates = ma.dates ?? [];
      const startTime = ma.shiftStartTime ?? input.shiftStartTime;
      const endTime = ma.shiftEndTime ?? defaultEnd;
      for (const d of dates) {
        const date = d.slice(0, 10);
        const key = `${ma.employeeId}:${date}`;
        if (seen.has(key)) continue;
        seen.add(key);
        rows.push({
          employeeId: ma.employeeId,
          date,
          startTime,
          endTime,
          notes: ma.notes,
        });
      }
    }
    return rows;
  }

  private async workingDaysForMonth(
    month: string,
    options: {
      saturdayShiftEnabled: boolean;
      sundayShiftEnabled: boolean;
      holidayShiftEnabled: boolean;
    },
  ): Promise<string[]> {
    const holidays = await this.holidayRepo.listInRange(
      parseDateOnly(`${month}-01`),
      parseDateOnly(monthEndDate(month)),
    );
    return computeShiftWorkingDays(month, {
      saturdayShiftEnabled: options.saturdayShiftEnabled,
      sundayShiftEnabled: options.sundayShiftEnabled,
      holidayShiftEnabled: options.holidayShiftEnabled,
      holidayDates: holidayDatesFromList(holidays, month),
    });
  }

  async generateAndSaveDraft(
    teamId: string,
    month: string,
    actorId: string,
    input: GenerateShiftDraftInput,
  ) {
    await this.assertMonthEditable(month);
    await this.assertCanEdit(teamId, month, actorId);

    const teamMembers = await this.listTeamMembersForShift(teamId, actorId);
    const validIds = new Set(teamMembers.map((m) => m.employeeId));
    const employeeIds = input.employeeIds.filter((id) => validIds.has(id));
    if (employeeIds.length === 0) {
      throw new ValidationError('No valid team members selected');
    }

    const editableIds = await this.listEditableEmployeeIds(teamId, month, actorId);
    if (editableIds) {
      const forbidden = employeeIds.filter((id) => !editableIds.includes(id));
      if (forbidden.length) {
        throw new ForbiddenError('You can only generate shifts for employees assigned to you');
      }
    }

    const holidays = await this.holidayRepo.listInRange(
      parseDateOnly(`${month}-01`),
      parseDateOnly(monthEndDate(month)),
    );
    const holidayDates = holidayDatesFromList(holidays, month);

    let shiftStartTime = input.shiftStartTime;
    let shiftEndTime = input.shiftEndTime ?? '18:30';
    let saturdayShiftEnabled = input.saturdayShiftEnabled ?? false;
    let sundayShiftEnabled = input.sundayShiftEnabled ?? false;
    let workingDays: string[];

    if (input.shiftDefinitionId) {
      const def = await this.prisma.team_shift_definitions.findFirst({
        where: { id: input.shiftDefinitionId, team_id: teamId, active: true },
      });
      if (!def) throw new ValidationError('Selected shift definition was not found');
      shiftStartTime = def.start_time;
      shiftEndTime = def.end_time;
      const days = parseWorkingDays(def.working_days);
      saturdayShiftEnabled = days.has(6);
      sundayShiftEnabled = days.has(0);
      workingDays = computeDatesForWorkingDays(month, days, holidayDates);
    } else {
      workingDays = await this.workingDaysForMonth(month, {
        saturdayShiftEnabled,
        sundayShiftEnabled,
        holidayShiftEnabled: input.holidayShiftEnabled ?? false,
      });
    }

    const existingSchedule = await this.prisma.team_shift_schedules.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
      include: { team_shift_member_assignments: true },
    });

    const generateSet = new Set(employeeIds);
    const memberAssignments: ShiftInput['memberAssignments'] = [];

    if (existingSchedule) {
      const preservedByEmployee = new Map<string, FlatAssignmentRow[]>();
      for (const row of existingSchedule.team_shift_member_assignments) {
        if (generateSet.has(row.employee_id)) continue;
        const date = toDateOnly(row.date);
        const list = preservedByEmployee.get(row.employee_id) ?? [];
        list.push({
          employeeId: row.employee_id,
          date,
          startTime: row.start_time,
          endTime: row.end_time,
          notes: row.notes,
        });
        preservedByEmployee.set(row.employee_id, list);
      }
      for (const [employeeId, rows] of preservedByEmployee) {
        memberAssignments.push({
          employeeId,
          dates: rows.map((r) => r.date),
          shiftStartTime: rows[0]?.startTime,
          shiftEndTime: rows[0]?.endTime,
        });
      }
    }

    for (const employeeId of employeeIds) {
      memberAssignments.push({
        employeeId,
        dates: workingDays,
        shiftStartTime,
        shiftEndTime,
      });
    }

    return this.saveDraft(teamId, month, actorId, {
      shiftStartTime,
      shiftEndTime,
      graceMinutes: input.graceMinutes,
      saturdayShiftEnabled,
      sundayShiftEnabled,
      holidayShiftEnabled: false,
      memberAssignments,
    });
  }

  async saveDraft(teamId: string, month: string, actorId: string, input: ShiftInput) {
    await this.assertMonthEditable(month);
    await this.assertCanEdit(teamId, month, actorId);

    const existing = await this.prisma.team_shift_schedules.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
      include: { team_shift_member_assignments: true },
    });

    let assignmentRows = this.flattenMemberAssignments(input);
    const editableIds = await this.listEditableEmployeeIds(teamId, month, actorId);

    if (editableIds) {
      const allowed = new Set(editableIds);
      const forbidden = assignmentRows.filter((r) => !allowed.has(r.employeeId));
      if (forbidden.length) {
        throw new ForbiddenError('You can only save timings for employees assigned to you');
      }
      assignmentRows = assignmentRows.filter((r) => allowed.has(r.employeeId));

      if (existing) {
        for (const row of existing.team_shift_member_assignments) {
          if (allowed.has(row.employee_id)) continue;
          assignmentRows.push({
            employeeId: row.employee_id,
            date: toDateOnly(row.date),
            startTime: row.start_time,
            endTime: row.end_time,
            notes: row.notes,
          });
        }
      }
    }

    const recallStatuses = new Set(['REJECTED', 'PENDING_APPROVAL']);
    const clearApprovals = existing && recallStatuses.has(existing.status);
    const access = await this.resolveEditAccess(teamId, month, actorId);
    const canUpdateScheduleMeta = access.editScope === 'ALL';

    const scheduleId = await this.prisma.$transaction(
      async (tx) => {
        const schedule = await tx.team_shift_schedules.upsert({
          where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
          create: {
            team_id: teamId,
            effective_month: month,
            status: 'DRAFT',
            shift_start_time: input.shiftStartTime,
            shift_end_time: input.shiftEndTime,
            grace_minutes: input.graceMinutes ?? 10,
            saturday_shift_enabled: input.saturdayShiftEnabled ?? false,
            sunday_shift_enabled: input.sundayShiftEnabled ?? false,
            holiday_shift_enabled: input.holidayShiftEnabled ?? false,
          },
          update: canUpdateScheduleMeta
            ? {
                shift_start_time: input.shiftStartTime,
                shift_end_time: input.shiftEndTime,
                grace_minutes: input.graceMinutes ?? 10,
                saturday_shift_enabled: input.saturdayShiftEnabled ?? false,
                sunday_shift_enabled: input.sundayShiftEnabled ?? false,
                holiday_shift_enabled: input.holidayShiftEnabled ?? false,
                status: 'DRAFT',
                submitted_by_employee_id: null,
                submitted_at: null,
              }
            : {
                status: 'DRAFT',
                submitted_by_employee_id: null,
                submitted_at: null,
              },
        });

        if (clearApprovals && canUpdateScheduleMeta) {
          await tx.team_shift_approvals.deleteMany({ where: { schedule_id: schedule.id } });
        }

        await tx.team_shift_member_assignments.deleteMany({ where: { schedule_id: schedule.id } });

        if (assignmentRows.length > 0) {
          await tx.team_shift_member_assignments.createMany({
            data: assignmentRows.map((row) => ({
              schedule_id: schedule.id,
              employee_id: row.employeeId,
              date: parseDateOnly(row.date),
              start_time: row.startTime,
              end_time: row.endTime,
              notes: row.notes,
            })),
          });
        }

        return schedule.id;
      },
      { timeout: 60000 },
    );

    const full = await this.prisma.team_shift_schedules.findUnique({
      where: { id: scheduleId },
      include: { team_shift_member_assignments: true, team_shift_approvals: true },
    });

    // Mirror each person's generated days/times onto their other teams for this month
    await this.syncEmployeeDayAssignmentsAcrossTeams(teamId, month, actorId, assignmentRows);

    const saved = await this.getSchedule(teamId, month, actorId);
    return saved ?? this.mapSchedule(full!);
  }

  /**
   * Keep a person's daily shift rows identical on every team schedule for the month.
   */
  private async syncEmployeeDayAssignmentsAcrossTeams(
    sourceTeamId: string,
    month: string,
    _actorId: string,
    assignmentRows: FlatAssignmentRow[],
  ) {
    const byEmployee = new Map<string, FlatAssignmentRow[]>();
    for (const row of assignmentRows) {
      const list = byEmployee.get(row.employeeId) ?? [];
      list.push(row);
      byEmployee.set(row.employeeId, list);
    }

    for (const [employeeId, rows] of byEmployee) {
      const teamIds = await this.teamService.listTeamIdsForEmployee(employeeId);
      for (const otherTeamId of teamIds) {
        if (otherTeamId === sourceTeamId) continue;

        const existing = await this.prisma.team_shift_schedules.findUnique({
          where: {
            team_id_effective_month: { team_id: otherTeamId, effective_month: month },
          },
        });

        await this.prisma.$transaction(
          async (tx) => {
            const schedule = await tx.team_shift_schedules.upsert({
              where: {
                team_id_effective_month: { team_id: otherTeamId, effective_month: month },
              },
              create: {
                team_id: otherTeamId,
                effective_month: month,
                status: 'DRAFT',
                shift_start_time: rows[0]?.startTime ?? '09:30',
                shift_end_time: rows[0]?.endTime ?? '18:30',
                grace_minutes: 10,
                saturday_shift_enabled: false,
                sunday_shift_enabled: false,
                holiday_shift_enabled: false,
              },
              update: {},
            });

            // Preserve APPROVED status if already published on the other team
            if (existing?.status === 'APPROVED' && schedule.status !== 'APPROVED') {
              await tx.team_shift_schedules.update({
                where: { id: schedule.id },
                data: { status: 'APPROVED' },
              });
            }

            await tx.team_shift_member_assignments.deleteMany({
              where: { schedule_id: schedule.id, employee_id: employeeId },
            });

            if (rows.length) {
              await tx.team_shift_member_assignments.createMany({
                data: rows.map((row) => ({
                  schedule_id: schedule.id,
                  employee_id: employeeId,
                  date: parseDateOnly(row.date),
                  start_time: row.startTime,
                  end_time: row.endTime,
                  notes: row.notes,
                })),
              });
            }
          },
          { timeout: 60000 },
        );
      }
    }
  }

  private async resolveResponsibleEmployeeId(teamId: string, month: string): Promise<string | null> {
    const delegation = await this.prisma.team_shift_planner_delegations.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
    });
    if (delegation?.planner_employee_id) {
      return delegation.planner_employee_id;
    }

    const teamCtx = await this.teamService.resolveTeamById(teamId);
    return teamCtx.leadBusinessId;
  }

  async resolveShiftResponsible(teamId: string, month: string) {
    const delegation = await this.prisma.team_shift_planner_delegations.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
    });
    const responsibleEmployeeId = await this.resolveResponsibleEmployeeId(teamId, month);
    const responsible = responsibleEmployeeId
      ? await this.prisma.employees.findUnique({ where: { employee_id: responsibleEmployeeId } })
      : null;

    return {
      responsibleEmployeeId,
      responsibleName: responsible?.name ?? null,
      responsibleRole: delegation?.planner_employee_id ? ('DELEGATE_PLANNER' as const) : ('MANAGER' as const),
      delegatedPlannerId: delegation?.planner_employee_id ?? null,
    };
  }

  private async resolveEditAccess(
    teamId: string,
    month: string,
    actorId: string,
    cachedIsAdmin?: boolean,
  ) {
    const responsible = await this.resolveShiftResponsible(teamId, month);
    const isTeamLead = await this.teamService.isTeamLead(actorId, teamId);
    const isShiftResponsible = responsible.responsibleEmployeeId === actorId;
    // is_manager=2 is company admin — full shift control on every team
    const isAdmin =
      cachedIsAdmin !== undefined ? cachedIsAdmin : await this.teamService.isAppAdmin(actorId);

    const timingRows = await this.prisma.team_shift_timing_managers.findMany({
      where: { team_id: teamId, effective_month: month, manager_employee_id: actorId },
    });
    const editableEmployeeIds = timingRows.map((r) => r.employee_id);
    const isScopedManager = editableEmployeeIds.length > 0;

    // Full team timing control: company admin (is_manager=2), team lead, or HR-assigned planner
    const canEditAll = isShiftResponsible || isAdmin || isTeamLead;
    const canEdit = canEditAll || isScopedManager;

    return {
      canEdit,
      canAssignTimingManagers: canEditAll,
      editScope: canEditAll ? ('ALL' as const) : ('ASSIGNED' as const),
      editableEmployeeIds,
      isTeamLead,
      isShiftResponsible: isShiftResponsible || isAdmin || isTeamLead,
      isAdmin,
      responsibleEmployeeId: responsible.responsibleEmployeeId,
      responsibleName: responsible.responsibleName,
      responsibleRole: responsible.responsibleRole,
      delegatedPlannerId: responsible.delegatedPlannerId,
    };
  }

  private async resolveCanReview(
    teamId: string,
    month: string,
    actorId: string,
    schedule: {
      status: string;
      submitted_by_employee_id: string | null;
      team_shift_approvals: Array<{ approver_employee_id: string }>;
    } | null,
  ) {
    if (!schedule || schedule.status !== 'PENDING_APPROVAL') return false;
    if (schedule.submitted_by_employee_id === actorId) return false;
    const approverIds = schedule.team_shift_approvals.map((a) => a.approver_employee_id);
    if (approverIds.includes(actorId)) return false;
    const verifierPool = await this.resolveVerifierPool(teamId, month, schedule.submitted_by_employee_id);
    return verifierPool.includes(actorId);
  }

  async listShiftTeamsForEmployee(employeeId: string, month?: string) {
    const shiftPlanning = await this.buildShiftPlanningConfig();
    const effectiveMonth = month ?? shiftPlanning.suggestedMonth;
    const isAppAdmin = await this.teamService.isAppAdmin(employeeId);
    const teams = isAppAdmin
      ? await this.teamService.listAllActiveTeams(employeeId)
      : await this.teamService.listActiveTeamsForEmployee(employeeId);
    const setupTeams: Array<{
      teamId: string;
      teamName: string;
      isTeamLead: boolean;
      canManage: boolean;
      canReview: boolean;
      canView?: boolean;
      isShiftResponsible: boolean;
      isAdmin?: boolean;
      scheduleStatus: string;
      responsibleEmployeeId: string | null;
      responsibleName: string | null;
      delegatedPlannerId: string | null;
      approvalCount: number;
      submittedByEmployeeId: string | null;
    }> = [];
    const reviewTeams: typeof setupTeams = [];
    const viewTeams: typeof setupTeams = [];
    const seenTeamIds = new Set<string>();
    const viewableStatuses = new Set(['PENDING_APPROVAL', 'APPROVED']);

    const teamIds = teams.map((t) => t.teamId);
    const [schedules, timingAsManager, verifierAssignments] = await Promise.all([
      teamIds.length
        ? this.prisma.team_shift_schedules.findMany({
            where: { team_id: { in: teamIds }, effective_month: effectiveMonth },
            include: { team_shift_approvals: true },
          })
        : Promise.resolve([]),
      this.prisma.team_shift_timing_managers.findMany({
        where: { manager_employee_id: employeeId, effective_month: effectiveMonth },
        select: { team_id: true, employee_id: true },
      }),
      this.prisma.team_shift_verifier_assignments.findMany({
        where: { verifier_employee_id: employeeId, effective_month: effectiveMonth },
      }),
    ]);
    const scheduleByTeam = new Map(schedules.map((s) => [s.team_id, s]));
    const editableByTeam = new Map<string, string[]>();
    for (const row of timingAsManager) {
      const list = editableByTeam.get(row.team_id) ?? [];
      list.push(row.employee_id);
      editableByTeam.set(row.team_id, list);
    }

    // Batch planner/lead lookups for listed teams
    const [delegations, teamRows] = await Promise.all([
      teamIds.length
        ? this.prisma.team_shift_planner_delegations.findMany({
            where: { team_id: { in: teamIds }, effective_month: effectiveMonth },
          })
        : Promise.resolve([]),
      teamIds.length
        ? this.prisma.l10_teams.findMany({
            where: { id: { in: teamIds } },
            select: { id: true, lead_employee_id: true },
          })
        : Promise.resolve([]),
    ]);
    const delegationByTeam = new Map(delegations.map((d) => [d.team_id, d.planner_employee_id]));
    const leadPkByTeam = new Map(teamRows.map((t) => [t.id, t.lead_employee_id ? String(t.lead_employee_id) : '']));

    const actorEmp = await this.prisma.employees.findUnique({
      where: { employee_id: employeeId },
      select: { id: true, employee_id: true, name: true },
    });
    const actorPk = actorEmp ? String(actorEmp.id) : '';

    const responsibleIds = [
      ...new Set(
        [...delegationByTeam.values(), ...[...leadPkByTeam.values()].filter(Boolean)].filter(
          (id): id is string => Boolean(id),
        ),
      ),
    ];
    // Lead may be stored as PK or business id — resolve names in one query
    const responsibleEmployees = responsibleIds.length
      ? await this.prisma.employees.findMany({
          where: {
            OR: [
              { employee_id: { in: responsibleIds } },
              {
                id: {
                  in: responsibleIds.map((id) => parseInt(id, 10)).filter((n) => !Number.isNaN(n)),
                },
              },
            ],
          },
          select: { id: true, employee_id: true, name: true },
        })
      : [];
    const nameByAnyId = new Map<string, { businessId: string; name: string }>();
    for (const e of responsibleEmployees) {
      const info = { businessId: e.employee_id, name: e.name };
      nameByAnyId.set(e.employee_id, info);
      nameByAnyId.set(String(e.id), info);
    }

    const pushTeam = async (
      teamId: string,
      teamName: string,
      isTeamLeadFlag: boolean,
      isTeamMember: boolean,
    ) => {
      if (seenTeamIds.has(teamId)) return;
      seenTeamIds.add(teamId);

      const schedule = scheduleByTeam.get(teamId) ?? null;
      const delegatedPlannerId = delegationByTeam.get(teamId) ?? null;
      const leadPk = leadPkByTeam.get(teamId) ?? '';
      const leadInfo = leadPk ? nameByAnyId.get(leadPk) : undefined;
      const responsibleBusinessId =
        delegatedPlannerId || leadInfo?.businessId || (leadPk || null);
      const responsibleName = responsibleBusinessId
        ? nameByAnyId.get(responsibleBusinessId)?.name ??
          nameByAnyId.get(leadPk)?.name ??
          null
        : null;
      const isTeamLead =
        isTeamLeadFlag ||
        (Boolean(leadPk) && (leadPk === employeeId || leadPk === actorPk)) ||
        leadInfo?.businessId === employeeId;
      const isShiftResponsible =
        isAppAdmin || isTeamLead || responsibleBusinessId === employeeId;
      const editableEmployeeIds = editableByTeam.get(teamId) ?? [];
      const canEdit = isShiftResponsible || editableEmployeeIds.length > 0;
      const canReview = await this.resolveCanReview(teamId, effectiveMonth, employeeId, schedule);
      const status = schedule?.status ?? 'MISSING';
      const row = {
        teamId,
        teamName,
        isTeamLead,
        canManage: canEdit,
        canReview,
        canView: false,
        isShiftResponsible,
        isAdmin: isAppAdmin,
        scheduleStatus: status,
        responsibleEmployeeId: responsibleBusinessId,
        responsibleName,
        delegatedPlannerId,
        approvalCount: schedule?.team_shift_approvals.length ?? 0,
        submittedByEmployeeId: schedule?.submitted_by_employee_id ?? null,
      };

      if (canEdit) setupTeams.push(row);
      if (canReview) reviewTeams.push(row);
      if (
        isTeamMember &&
        schedule &&
        viewableStatuses.has(status) &&
        !canEdit &&
        !canReview
      ) {
        viewTeams.push({ ...row, canView: true });
      }
    };

    // Process in parallel chunks to cut remote round-trip wait
    const chunkSize = 6;
    for (let i = 0; i < teams.length; i += chunkSize) {
      const chunk = teams.slice(i, i + chunkSize);
      await Promise.all(
        chunk.map((team) =>
          pushTeam(team.teamId, team.teamName, team.isTeamLead, !isAppAdmin),
        ),
      );
    }

    for (const assignment of verifierAssignments) {
      if (seenTeamIds.has(assignment.team_id)) continue;
      const l10 = await this.prisma.l10_teams.findUnique({ where: { id: assignment.team_id } });
      if (!l10?.active) continue;
      await pushTeam(assignment.team_id, l10.name, false, false);
    }

    const verifierTeams: typeof setupTeams = [];
    for (const assignment of verifierAssignments) {
      if (
        reviewTeams.some((t) => t.teamId === assignment.team_id) ||
        verifierTeams.some((t) => t.teamId === assignment.team_id)
      ) {
        continue;
      }
      const l10 = await this.prisma.l10_teams.findUnique({ where: { id: assignment.team_id } });
      if (!l10?.active) continue;
      const schedule = scheduleByTeam.get(assignment.team_id) ??
        (await this.prisma.team_shift_schedules.findUnique({
          where: {
            team_id_effective_month: {
              team_id: assignment.team_id,
              effective_month: effectiveMonth,
            },
          },
          include: { team_shift_approvals: true },
        }));
      const access = await this.resolveEditAccess(
        assignment.team_id,
        effectiveMonth,
        employeeId,
        isAppAdmin,
      );
      const status = schedule?.status ?? 'MISSING';
      verifierTeams.push({
        teamId: assignment.team_id,
        teamName: l10.name,
        isTeamLead: false,
        canManage: false,
        canReview: false,
        canView: false,
        isShiftResponsible: access.isShiftResponsible,
        scheduleStatus: status,
        responsibleEmployeeId: access.responsibleEmployeeId,
        responsibleName: access.responsibleName,
        delegatedPlannerId: access.delegatedPlannerId,
        approvalCount: schedule?.team_shift_approvals.length ?? 0,
        submittedByEmployeeId: schedule?.submitted_by_employee_id ?? null,
      });
    }

    return {
      effectiveMonth,
      shiftPlanning,
      setupTeams,
      reviewTeams,
      verifierTeams,
      viewTeams,
      manageableCount: setupTeams.length,
      reviewableCount: reviewTeams.length,
      verifierAssignedCount: verifierAssignments.length,
      viewableCount: viewTeams.length,
    };
  }

  private async buildShiftPlanningConfig() {
    const today = todayInCompanyTz();
    const currentMonth = getYearMonth(today);
    const nextMonth = this.nextMonthFrom(today);
    const allowCurrentMonth = await this.appSettings.isShiftCurrentMonthEnabled();
    const minEditableMonth = allowCurrentMonth ? currentMonth : nextMonth;
    return {
      allowCurrentMonth,
      currentMonth,
      nextMonth,
      minEditableMonth,
      suggestedMonth: allowCurrentMonth ? currentMonth : nextMonth,
    };
  }

  private async assertCanEdit(teamId: string, month: string, actorId: string) {
    const access = await this.resolveEditAccess(teamId, month, actorId);
    if (!access.canEdit) {
      throw new ForbiddenError(
        'Only the allocated shift planner for this month can manage the team shift. Ask HR or your team manager.',
      );
    }
  }

  async submit(teamId: string, month: string, actorId: string) {
    await this.assertMonthEditable(month);
    await this.assertCanEdit(teamId, month, actorId);
    const access = await this.resolveEditAccess(teamId, month, actorId);
    if (access.editScope !== 'ALL') {
      throw new ForbiddenError('Only team admin or the assigned shift planner can submit the full schedule');
    }
    const schedule = await this.prisma.team_shift_schedules.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
      include: { team_shift_member_assignments: true },
    });
    if (!schedule) throw new NotFoundError('Shift schedule not found');

    const overlaps = this.findOverlaps(schedule.team_shift_member_assignments);
    if (overlaps.length > 0) {
      throw new ValidationError('Overlapping member assignments detected');
    }

    // Company admin publishes as approved — no verifier approval required
    if (access.isAdmin) {
      await this.prisma.team_shift_approvals.deleteMany({ where: { schedule_id: schedule.id } });
      await this.markScheduleApproved(schedule.id, actorId);
      const enriched = await this.getSchedule(teamId, month, actorId);
      return enriched;
    }

    const assignedVerifiers = await this.getAssignedVerifierIds(teamId, month);
    if (assignedVerifiers.length < 2) {
      throw new ValidationError(
        'HR must assign 2 shift verifiers for this team and month before submitting',
      );
    }

    await this.prisma.team_shift_approvals.deleteMany({ where: { schedule_id: schedule.id } });

    const updated = await this.prisma.team_shift_schedules.update({
      where: { id: schedule.id },
      data: {
        status: 'PENDING_APPROVAL',
        submitted_by_employee_id: actorId,
        submitted_at: new Date(),
      },
      include: { team_shift_member_assignments: true, team_shift_approvals: true },
    });

    const team = await this.teamService.resolveTeamById(teamId);
    const submitter = await this.prisma.employees.findUnique({ where: { employee_id: actorId } });
    const submitterName = submitter?.name ?? actorId;

    const notifyIds = await this.resolveVerifierPool(teamId, month, actorId);
    for (const memberId of notifyIds) {
      const m = await this.prisma.employees.findUnique({ where: { employee_id: memberId } });
      if (m) {
        await this.notifications.createEmail('SHIFT_PENDING_APPROVAL', {
          to: m.email,
          month,
          teamName: team.teamName,
          submitterName,
        }).send();
      }
    }
    const enriched = await this.getSchedule(teamId, month, actorId);
    return enriched ?? this.mapSchedule(updated);
  }

  private findOverlaps(
    assignments: Array<{ employee_id: string; date: Date; start_time: string; end_time: string }>,
  ): string[] {
    const byKey = new Map<string, typeof assignments>();
    for (const a of assignments) {
      const key = `${a.employee_id}:${toDateOnly(a.date)}`;
      const list = byKey.get(key) ?? [];
      list.push(a);
      byKey.set(key, list);
    }
    const conflicts: string[] = [];
    for (const [key, list] of byKey) {
      if (list.length > 1) conflicts.push(key);
    }
    return conflicts;
  }

  async approve(teamId: string, month: string, approverId: string) {
    const schedule = await this.prisma.team_shift_schedules.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
      include: { team_shift_approvals: true },
    });
    if (!schedule || schedule.status !== 'PENDING_APPROVAL') throw new NotFoundError();
    if (schedule.submitted_by_employee_id === approverId) {
      throw new ForbiddenError('Submitter cannot approve own shift');
    }

    if (schedule.team_shift_approvals.some((a) => a.approver_employee_id === approverId)) {
      throw new ValidationError('You have already approved this shift');
    }

    const verifierPool = await this.resolveVerifierPool(teamId, month, schedule.submitted_by_employee_id);
    if (!verifierPool.includes(approverId)) {
      throw new ForbiddenError('Only HR-assigned shift verifiers can approve');
    }

    await this.prisma.team_shift_approvals.create({
      data: { schedule_id: schedule.id, approver_employee_id: approverId },
    });

    const count = schedule.team_shift_approvals.length + 1;
    if (count >= 2) {
      await this.prisma.team_shift_schedules.update({
        where: { id: schedule.id },
        data: { status: 'APPROVED' },
      });
    }

    const enriched = await this.getSchedule(teamId, month, approverId);
    if (enriched) {
      return count >= 2 ? enriched : { ...enriched, approvalCount: count };
    }

    const refreshed = await this.prisma.team_shift_schedules.findUnique({
      where: { id: schedule.id },
      include: { team_shift_member_assignments: true, team_shift_approvals: true },
    });
    return { ...this.mapSchedule(refreshed!), approvalCount: count };
  }

  async reject(teamId: string, month: string, approverId: string, comment?: string) {
    const schedule = await this.prisma.team_shift_schedules.findUnique({
      where: { team_id_effective_month: { team_id: teamId, effective_month: month } },
      include: { team_shift_approvals: true },
    });
    if (!schedule || schedule.status !== 'PENDING_APPROVAL') throw new NotFoundError();

    const verifierPool = await this.resolveVerifierPool(teamId, month, schedule.submitted_by_employee_id);
    if (!verifierPool.includes(approverId)) {
      throw new ForbiddenError('Only HR-assigned shift verifiers can reject');
    }

    await this.prisma.team_shift_approvals.deleteMany({ where: { schedule_id: schedule.id } });
    await this.prisma.team_shift_member_assignments.deleteMany({ where: { schedule_id: schedule.id } });
    const updated = await this.prisma.team_shift_schedules.update({
      where: { id: schedule.id },
      data: {
        status: 'REJECTED',
        submitted_by_employee_id: null,
        submitted_at: null,
      },
      include: { team_shift_member_assignments: true, team_shift_approvals: true },
    });

    const team = await this.teamService.resolveTeamById(teamId);
    const responsible = await this.resolveShiftResponsible(teamId, month);
    if (responsible.responsibleEmployeeId) {
      const planner = await this.prisma.employees.findUnique({
        where: { employee_id: responsible.responsibleEmployeeId },
      });
      if (planner?.email) {
        await this.notifications.createEmail('SHIFT_REJECTED', {
          to: planner.email,
          month,
          teamName: team.teamName,
          comment: comment ?? '',
        }).send();
      }
    }

    const enriched = await this.getSchedule(teamId, month, responsible.responsibleEmployeeId ?? approverId);
    return enriched ?? this.mapSchedule(updated);
  }

  async sendReminders() {
    const today = toDateOnly(new Date());
    const daysLeft = this.daysUntilMonthEnd(today);
    if (![5, 3, 1].includes(daysLeft)) return { sent: 0 };

    const nextMonth = this.nextMonthFrom(today);
    const teams = await this.prisma.l10_teams.findMany({ where: { active: true } });
    let sent = 0;

    for (const team of teams) {
      const approved = await this.prisma.team_shift_schedules.findFirst({
        where: { team_id: team.id, effective_month: nextMonth, status: 'APPROVED' },
      });
      if (approved) continue;

      const notifiedEmails = new Set<string>();

      const teamCtx = await this.teamService.resolveTeamById(team.id);
      if (teamCtx.leadBusinessId) {
        const lead = await this.prisma.employees.findUnique({
          where: { employee_id: teamCtx.leadBusinessId },
        });
        if (lead?.email) {
          await this.notifications.createEmail('SHIFT_REMINDER_T5', { to: lead.email, month: nextMonth }).send();
          notifiedEmails.add(lead.email);
          sent++;
        }
      }

      const delegation = await this.prisma.team_shift_planner_delegations.findUnique({
        where: { team_id_effective_month: { team_id: team.id, effective_month: nextMonth } },
      });
      if (delegation?.planner_employee_id) {
        const planner = await this.prisma.employees.findUnique({
          where: { employee_id: delegation.planner_employee_id },
        });
        if (planner?.email && !notifiedEmails.has(planner.email)) {
          await this.notifications.createEmail('SHIFT_REMINDER_T5', { to: planner.email, month: nextMonth }).send();
          sent++;
        }
      }
    }
    return { sent, daysLeft };
  }

  async shiftPlanningStatus(month?: string) {
    const today = todayInCompanyTz();
    const currentMonth = getYearMonth(today);
    const nextMonth = this.nextMonthFrom(today);
    const effectiveMonth = month ?? nextMonth;
    const allowCurrentMonth = await this.appSettings.isShiftCurrentMonthEnabled();

    const l10Teams = await this.prisma.l10_teams.findMany({ where: { active: true }, orderBy: { name: 'asc' } });
    const teams = [];

    for (const team of l10Teams) {
      const schedule = await this.prisma.team_shift_schedules.findUnique({
        where: { team_id_effective_month: { team_id: team.id, effective_month: effectiveMonth } },
        include: { team_shift_approvals: true },
      });

      const delegation = await this.prisma.team_shift_planner_delegations.findUnique({
        where: { team_id_effective_month: { team_id: team.id, effective_month: effectiveMonth } },
      });

      let managerEmployeeId: string | null = null;
      let managerName: string | null = null;
      let managerEmail: string | null = null;
      if (team.lead_employee_id) {
        const leadPk = parseInt(team.lead_employee_id, 10);
        const lead = !Number.isNaN(leadPk)
          ? await this.prisma.employees.findUnique({ where: { id: leadPk } })
          : await this.prisma.employees.findFirst({ where: { employee_id: team.lead_employee_id } });
        if (lead) {
          managerEmployeeId = lead.employee_id;
          managerName = lead.name;
          managerEmail = lead.email;
        }
      }

      let plannerEmployeeId: string | null = null;
      let plannerName: string | null = null;
      if (delegation?.planner_employee_id) {
        const planner = await this.prisma.employees.findUnique({
          where: { employee_id: delegation.planner_employee_id },
        });
        if (planner) {
          plannerEmployeeId = planner.employee_id;
          plannerName = planner.name;
        }
      }

      const responsibleEmployeeId = plannerEmployeeId ?? managerEmployeeId;
      const responsibleName = plannerName ?? managerName;
      const responsibleRole = plannerEmployeeId ? 'DELEGATE_PLANNER' : 'MANAGER';
      const status = schedule?.status ?? 'MISSING';
      const approvalCount = schedule?.team_shift_approvals.length ?? 0;
      const compliant = status === 'APPROVED';
      const needsAction = !compliant;

      let pendingVerifiers: Array<{ employeeId: string; name: string }> = [];
      const teamManagers = await this.teamService.listTeamManagers(team.id);
      const assignedVerifierIds = await this.getAssignedVerifierIds(team.id, effectiveMonth);
      const assignedVerifiers = assignedVerifierIds.length
        ? (await this.prisma.employees.findMany({ where: { employee_id: { in: assignedVerifierIds } } })).map(
            (e) => ({ employeeId: e.employee_id, name: e.name }),
          )
        : [];

      if (schedule?.status === 'PENDING_APPROVAL') {
        const approvedIds = new Set(schedule.team_shift_approvals.map((a) => a.approver_employee_id));
        const pendingIds = assignedVerifierIds.filter((id) => !approvedIds.has(id));
        if (pendingIds.length) {
          const pendingEmps = await this.prisma.employees.findMany({
            where: { employee_id: { in: pendingIds } },
          });
          pendingVerifiers = pendingEmps.map((e) => ({ employeeId: e.employee_id, name: e.name }));
        }
      }

      teams.push({
        teamId: team.id,
        teamName: team.name,
        effectiveMonth,
        managerEmployeeId,
        managerName,
        managerEmail,
        plannerEmployeeId,
        plannerName,
        responsibleEmployeeId,
        responsibleName,
        responsibleRole,
        status,
        approvalCount,
        submittedByEmployeeId: schedule?.submitted_by_employee_id ?? null,
        teamManagers,
        assignedVerifiers,
        pendingVerifiers,
        compliant,
        needsAction,
      });
    }

    const needsActionCount = teams.filter((t) => t.needsAction).length;

    return {
      effectiveMonth,
      currentMonth,
      nextMonth,
      allowCurrentMonthPlanning: allowCurrentMonth,
      minEditableMonth: allowCurrentMonth ? currentMonth : nextMonth,
      summary: {
        totalTeams: teams.length,
        needsAction: needsActionCount,
        compliant: teams.length - needsActionCount,
      },
      teams,
    };
  }

  /** @deprecated use shiftPlanningStatus */
  async shiftCompliance() {
    const report = await this.shiftPlanningStatus();
    return {
      effectiveMonth: report.effectiveMonth,
      teams: report.teams.filter((t) => t.needsAction),
    };
  }

  /** HR: assign delegate planner (is_manager=1 on team). */
  hrAssignPlannerForTeam(teamId: string, month: string, plannerId: string, hrUsername: string) {
    return this.hrAssignPlanner(teamId, month, plannerId, hrUsername);
  }

  /** HR: assign up to 2 verifiers (is_manager=1 on team). */
  hrAssignVerifiersForTeam(teamId: string, month: string, verifierIds: string[], hrUsername: string) {
    return this.hrAssignVerifiers(teamId, month, verifierIds, hrUsername);
  }

  getHrTeamShiftAssignments(teamId: string, month: string) {
    return this.getTeamShiftAssignments(teamId, month);
  }

  private mapSchedule(
    schedule: {
      team_id: string;
      effective_month: string;
      status: string;
      shift_start_time: string;
      shift_end_time: string | null;
      grace_minutes: number;
      saturday_shift_enabled: boolean;
      sunday_shift_enabled: boolean;
      holiday_shift_enabled: boolean;
      submitted_by_employee_id: string | null;
      team_shift_member_assignments: Array<{
        employee_id: string;
        date: Date;
        start_time: string;
        end_time: string;
        notes: string | null;
      }>;
      team_shift_approvals: Array<{ approver_employee_id: string }>;
    },
  ) {
    return {
      teamId: schedule.team_id,
      effectiveMonth: schedule.effective_month,
      status: schedule.status,
      shiftStartTime: schedule.shift_start_time,
      shiftEndTime: schedule.shift_end_time,
      graceMinutes: schedule.grace_minutes,
      saturdayShiftEnabled: schedule.saturday_shift_enabled,
      sundayShiftEnabled: schedule.sunday_shift_enabled,
      holidayShiftEnabled: schedule.holiday_shift_enabled,
      submittedByEmployeeId: schedule.submitted_by_employee_id,
      approvalCount: schedule.team_shift_approvals.length,
      memberAssignments: schedule.team_shift_member_assignments.map((a) => ({
        employeeId: a.employee_id,
        date: toDateOnly(a.date),
        shiftStartTime: a.start_time,
        shiftEndTime: a.end_time,
        notes: a.notes,
      })),
    };
  }

  private daysUntilMonthEnd(dateStr: string): number {
    const ym = getYearMonth(dateStr);
    const [y, m] = ym.split('-').map(Number);
    const last = new Date(Date.UTC(y, m, 0)).getUTCDate();
    const day = parseInt(dateStr.slice(8, 10), 10);
    return last - day;
  }

  private nextMonthFrom(dateStr: string): string {
    const [y, m] = getYearMonth(dateStr).split('-').map(Number);
    const d = new Date(Date.UTC(m === 12 ? y + 1 : y, m === 12 ? 0 : m, 1));
    return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;
  }
}
