// src/server/features/schedule/schedule.service.ts
// Business logic for schedule management

import { db } from '../../shared/db/client';
import { shifts } from './db/shifts.table';
import { shiftTasks } from './db/shift_tasks.table';
import { employees, employmentPeriods } from '../../shared/db/schema';
import { pendingNotifications } from '../notifications/db/pending_notifications.table';
import { approvalRequests } from '../approval/db/approval_requests.table';
import { eq, and, isNull, between, lte, gte, or, inArray, sql } from 'drizzle-orm';
import { randomUUID } from 'node:crypto';
import { loggerService } from '../../shared/lib/logger';
import { getUsersForScheduleGrid } from '../personnel/personnel.service';
import { notifyAdmins, notifyManagers } from '../../shared/plugins/socket';
import { notifyMultipleUsers } from '../notifications/notifications.service';
import { exportToCSV, generateExportFilename } from '../../shared/lib/csv';
import { createApprovalRequest } from '../approval/approval.service';
import { USER_ROLES } from '@shared/constants/roles';
import type { ScheduleUser, EmploymentPeriod } from '@shared/contracts/personnel';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';

dayjs.extend(utc);

const scheduleLogger = loggerService.get('DB').child({ module: 'schedule' });

export interface Shift {
  id: string;
  date: string;
  userId: string;
  roleAtShift: 'Manager' | 'Maid';
  status: 'Blank' | 'Work' | 'Holiday' | 'Unavailable';
  isApproved: boolean;
  createdBy: string;
  createdAt: string;
  updatedAt: string;
  archivedAt: string | null;
}

export interface ShiftsGridData {
  managers: ScheduleUser[];
  maids: ScheduleUser[];
  shifts: Shift[];
  employmentPeriods: EmploymentPeriod[];
}

export interface ShiftUpdate {
  userId: string;
  date: Date;
  roleAtShift: 'Manager' | 'Maid';
  status: 'Blank' | 'Work' | 'Holiday' | 'Unavailable';
}

/**
 * НОРМАЛИЗАЦИЯ ДАТЫ
 * Сбрасываем время в 00:00:00 UTC для точного сравнения в БД.
 */
function normalizeDate(date: Date): Date {
  const d = new Date(date);
  d.setUTCHours(0, 0, 0, 0);
  return d;
}

/**
 * Проверяет, попадает ли дата в один из периодов работы пользователя с учетом роли
 */
async function isWithinEmploymentPeriods(userId: string, date: Date, roleAtShift?: 'Manager' | 'Maid'): Promise<boolean> {
  const targetDate = normalizeDate(date);
  
  // Если роль не указана, проверяем все периоды (для обратной совместимости)
  let periods = await db
    .select()
    .from(employmentPeriods)
    .where(eq(employmentPeriods.userId, userId));
  
  // Если роль указана, фильтруем периоды по isMaid
  if (roleAtShift) {
    const isMaid = roleAtShift === 'Maid';
    periods = periods.filter(p => p.isMaid === isMaid);
  }
  
  if (periods.length === 0) {
    return false;
  }
  
  // Проверяем, попадает ли дата хотя бы в один период
  for (const period of periods) {
    const startDate = new Date(period.startDate);
    startDate.setUTCHours(0, 0, 0, 0);
    
    // Если endDate null, значит сотрудник все еще работает
    const endDate = period.endDate ? new Date(period.endDate) : null;
    if (endDate) {
      endDate.setUTCHours(0, 0, 0, 0);
    }
    
    // Проверяем вхождение в период [startDate, endDate] или [startDate, ∞)
    if (targetDate >= startDate && (endDate === null || targetDate <= endDate)) {
      return true;
    }
  }
  
  return false;
}

export async function getActiveUsers(month?: number, year?: number): Promise<ScheduleUser[]> {
  // Используем dayjs.utc для консистентности с остальным проектом
  const firstDayOfMonth = month && year ? dayjs.utc().year(year).month(month - 1).startOf('month').toDate() : null;
  const lastDayOfMonth = month && year ? dayjs.utc().year(year).month(month - 1).endOf('month').toDate() : null;

  // Если месяц и год не указаны, возвращаем всех пользователей
  // Employment Periods - единственный источник правды для отображения в сетке расписания
  if (!firstDayOfMonth || !lastDayOfMonth) {
    const result = await db
      .select({
        id: employees.id,
        username: employees.username,
        email: employees.email,
        role: employees.role,
        isMaidAlso: employees.isMaidAlso,
        isFired: employees.isFired,
        fullName: employees.fullName,
      })
      .from(employees);

    return result.map(u => {
      const nameParts = (u.fullName || '').split(' ');
      return {
        ...u,
        firstName: nameParts[0] || '',
        lastName: nameParts.slice(1).join(' ') || '',
        fullName: u.fullName || u.username || '',
        role: u.role as 'ADMIN' | 'MANAGER' | 'MAID',
      };
    }) as ScheduleUser[];
  }

  // Employment Periods - единственный источник правды для отображения в сетке расписания
  // Логика: Если у пользователя есть хоть один период (даже закрытый), который попадает в выбранный месяц — он должен быть в списке User[] для этого месяца
  const result = await db
    .selectDistinct({
      id: employees.id,
      username: employees.username,
      email: employees.email,
      role: employees.role,
      isMaidAlso: employees.isMaidAlso,
      isFired: employees.isFired,
      fullName: employees.fullName,
    })
    .from(employees)
    .innerJoin(employmentPeriods, eq(employees.id, employmentPeriods.userId))
    .where(and(
      lte(employmentPeriods.startDate, lastDayOfMonth),
      or(
        gte(employmentPeriods.endDate, firstDayOfMonth),
        isNull(employmentPeriods.endDate)
      )
    ));

  return result.map(u => {
    const nameParts = (u.fullName || '').split(' ');
    return {
      ...u,
      firstName: nameParts[0] || '',
      lastName: nameParts.slice(1).join(' ') || '',
      fullName: u.fullName || u.username || '',
      role: u.role as 'ADMIN' | 'MANAGER' | 'MAID',
    };
  }) as ScheduleUser[];
}

export async function getShiftsGrid(month: number, year: number): Promise<ShiftsGridData> {
  // Используем новую функцию для получения пользователей на основе периодов работы
  const { managers, maids } = await getUsersForScheduleGrid(month, year);
  const allUsers = [...managers, ...maids];

  // Используем dayjs.utc для консистентности с остальным проектом
  const startDate = dayjs.utc().year(year).month(month - 1).startOf('month').toDate();
  const endDate = dayjs.utc().year(year).month(month - 1).endOf('month').toDate();

  const shiftsResult = await db
    .select()
    .from(shifts)
    .where(and(
      between(shifts.date, startDate, endDate),
      isNull(shifts.archivedAt)
    ));

  // Получаем все периоды работы для пользователей в сетке
  const userIds = allUsers.map(u => u.id);
  const employmentPeriodsResult = await db
    .select({
      id: employmentPeriods.id,
      userId: employmentPeriods.userId,
      startDate: employmentPeriods.startDate,
      endDate: employmentPeriods.endDate,
      isMaid: employmentPeriods.isMaid, // БД возвращает is_maid, Drizzle маппит в isMaid
      createdAt: employmentPeriods.createdAt,
    })
    .from(employmentPeriods)
    .where(inArray(employmentPeriods.userId, userIds));

  return {
    managers,
    maids,
    shifts: shiftsResult.map(s => ({
      ...s,
      date: s.date.toISOString(),
      createdAt: s.createdAt.toISOString(),
      updatedAt: s.updatedAt.toISOString(),
      archivedAt: s.archivedAt ? s.archivedAt.toISOString() : null,
      roleAtShift: s.roleAtShift as 'Manager' | 'Maid',
      status: s.status as 'Blank' | 'Work' | 'Holiday' | 'Unavailable',
    })) as Shift[],
    employmentPeriods: employmentPeriodsResult.map(ep => {
      // ПРЯМАЯ ПРОВЕРКА: смотрим оба варианта ключа
      const rawValue = (ep as any).isMaid !== undefined ? (ep as any).isMaid : (ep as any).is_maid;
      const isMaidFinal = rawValue === true || Number(rawValue) === 1 || rawValue === '1';
      
      // ВОЗВРАЩАЕМ ПЛОСКИЙ ОБЪЕКТ БЕЗ СКРЫТЫХ СВОЙСТВ
      return {
        id: ep.id,
        userId: ep.userId,
        startDate: ep.startDate instanceof Date ? ep.startDate.toISOString().split('T')[0] : String(ep.startDate),
        endDate: ep.endDate ? (ep.endDate instanceof Date ? ep.endDate.toISOString().split('T')[0] : String(ep.endDate)) : null,
        isMaid: isMaidFinal, // ЗДЕСЬ ОБЯЗАТЕЛЬНО ДОЛЖЕН БЫТЬ BOOLEAN
        createdAt: ep.createdAt instanceof Date ? ep.createdAt.toISOString() : String(ep.createdAt)
      };
    }) as EmploymentPeriod[]
  };
}

export async function updateShift(
  userId: string,
  date: Date,
  roleAtShift: 'Manager' | 'Maid',
  status: 'Blank' | 'Work' | 'Holiday' | 'Unavailable',
  createdBy: string,
  isAdmin: boolean = false
): Promise<void> {
  // Админ автоматически согласовывает свои изменения при сохранении
  const isApproved = isAdmin;
  const targetDate = normalizeDate(date);

  // Проверка: дата смены должна находиться в одном из периодов работы сотрудника с учетом роли
  const isWithinPeriods = await isWithinEmploymentPeriods(userId, date, roleAtShift);
  if (!isWithinPeriods) {
    throw new Error('Дата смены вне периода работы сотрудника для данной роли');
  }

  // ОГРАНИЧЕНИЕ ДЛЯ АДМИНА: Проверяем наличие УЖЕ согласованного WORK именно в ЭТОЙ роли
  // Только админ не может назначить более одной смены Work на одну дату в рамках одной категории
  // Менеджеры могут предлагать изменения поверх согласованных записей
  if (isAdmin && status === 'Work') {
    const conflict = await db
      .select()
      .from(shifts)
      .where(and(
        eq(shifts.date, targetDate),
        eq(shifts.roleAtShift, roleAtShift), // СТРОГО ищем только в той же категории
        eq(shifts.status, 'Work'),
        eq(shifts.isApproved, true),
        isNull(shifts.archivedAt)
      ));

    const otherShift = conflict.filter(s => s.userId !== userId);
    if (otherShift.length > 0) {
      const allUsers = await getActiveUsers();
      const user = allUsers.find(u => u.id === otherShift[0].userId);
      const roleText = roleAtShift === 'Manager' ? 'менеджеров' : 'горничных';
      throw new Error(`Ошибка: На эту дату уже назначена согласованная рабочая смена для категории ${roleText}. Сотрудник: ${user?.fullName || otherShift[0].userId}`);
    }
  }

  // Транзакция для обеспечения атомарности операций
  await db.transaction(async (tx) => {
    // Находим существующие смены для этого userId, date и roleAtShift (может быть несколько)
    const existing = await tx
      .select()
      .from(shifts)
      .where(and(
        eq(shifts.userId, userId),
        eq(shifts.date, targetDate),
        eq(shifts.roleAtShift, roleAtShift),
        isNull(shifts.archivedAt)
      ));

    const previousStatus = existing.length > 0 ? existing[0].status : null;

    // Если статус меняется на Blank, Holiday или Unavailable - удаляем связанные задачи
    if (status !== 'Work' && previousStatus === 'Work') {
      // Удаляем задачи на эту дату (связь через дату)
      await tx.delete(shiftTasks).where(and(
        eq(shiftTasks.date, targetDate),
        isNull(shiftTasks.archivedAt)
      ));
    }

    // Если статус Blank - обрабатываем в зависимости от роли пользователя
    if (status === 'Blank') {
      if (isAdmin) {
        // Админ удаляет всё под чистую (решение окончательное)
        await tx.delete(shifts).where(and(
          eq(shifts.userId, userId),
          eq(shifts.date, targetDate),
          eq(shifts.roleAtShift, roleAtShift)
        ));
      } else {
        // Менеджер предлагает удаление - создаем черновик поверх согласованной записи
        const approvedExists = existing.some(s => s.isApproved);
        if (approvedExists) {
          // Создаем или обновляем черновик со статусом Blank
          const draft = existing.find(s => !s.isApproved);
          if (draft) {
            // Обновляем существующий черновик
            await tx.update(shifts).set({ status: 'Blank', updatedAt: new Date() }).where(eq(shifts.id, draft.id));
          } else {
            // Создаем новый черновик
            await tx.insert(shifts).values({
              id: randomUUID(),
              date: targetDate,
              userId,
              roleAtShift,
              status: 'Blank',
              isApproved: false,
              createdBy
            });
          }
        } else {
          // Smart Removal: Если согласованной записи нет, просто удаляем черновики
          await tx.delete(shifts).where(and(
            eq(shifts.userId, userId),
            eq(shifts.date, targetDate),
            eq(shifts.roleAtShift, roleAtShift),
            eq(shifts.isApproved, false)
          ));
        }
      }
    } else {
      // Логика для менеджера: гарантированное обновление черновика
      if (!isAdmin) {
        // Находим существующий черновик (если есть)
        const draft = existing.find(s => !s.isApproved);
        
        if (draft) {
          // Если черновик найден - обновляем его
          await tx.update(shifts).set({ status, updatedAt: new Date() }).where(eq(shifts.id, draft.id));
        } else {
          // Если черновик не найден - создаем новую запись с isApproved: false
          await tx.insert(shifts).values({
            id: randomUUID(),
            date: targetDate,
            userId,
            roleAtShift,
            status,
            isApproved: false,
            createdBy
          });
        }
      } else {
        // Логика для админа: обновление существующей записи или создание новой
        if (existing.length > 0) {
          await tx.update(shifts).set({ status, isApproved, updatedAt: new Date() }).where(eq(shifts.id, existing[0].id));
        } else {
          await tx.insert(shifts).values({
            id: randomUUID(),
            date: targetDate,
            userId,
            roleAtShift,
            status,
            isApproved,
            createdBy
          });
        }
      }
    }
  });

  // Синхронизация Manager -> Maid (только для менеджеров с флагом isMaidAlso)
  if (roleAtShift === 'Manager') {
    // Получаем данные пользователя для проверки флага isMaidAlso
    const userResult = await db
      .select({ isMaidAlso: employees.isMaidAlso })
      .from(employees)
      .where(eq(employees.id, userId))
      .limit(1);

    const user = userResult[0];
    
    // Синхронизируем только если пользователь имеет флаг isMaidAlso === true
    if (user && user.isMaidAlso) {
      // Передаем текущий статус включая Blank для физического удаления
      await syncManagerToMaid(userId, targetDate, status, isAdmin);
    }
  }

  // Создаем заявку на согласование для менеджеров (не админов)
  if (!isAdmin) {
    const userResult = await db
      .select({ role: employees.role })
      .from(employees)
      .where(eq(employees.id, createdBy))
      .limit(1);

    const user = userResult[0];
    
    // Если это менеджер, создаем заявку на согласование
    if (user && user.role === USER_ROLES.MANAGER) {
      const month = targetDate.getMonth() + 1;
      const year = targetDate.getFullYear();
      
      scheduleLogger.debug('Creating approval request for manager', { createdBy, month, year });
      await createApprovalRequest({
        entityType: 'shift',
        targetId: null,
        requesterId: createdBy,
        payload: {
          month,
          year,
          updatesCount: 1,
        },
      });
    }
  }

  // Уведомляем админов и менеджеров об изменениях в графике
  await Promise.all([
    notifyAdmins('shift:update', { month: targetDate.getMonth() + 1, year: targetDate.getFullYear() }),
    notifyManagers('shift:update', { month: targetDate.getMonth() + 1, year: targetDate.getFullYear() }),
  ]);
}

async function syncManagerToMaid(
  userId: string,
  date: Date,
  status: 'Blank' | 'Work' | 'Holiday' | 'Unavailable',
  isAdmin: boolean = false
): Promise<void> {
  // ПРОВЕРКА: активна ли роль горничной на эту дату
  const isAllowed = await isWithinEmploymentPeriods(userId, date, 'Maid');
  
  if (!isAllowed) {
    // Если роль неактивна, мы не создаем смену, а если была старая - удаляем её (очистка)
    await db.delete(shifts).where(and(
        eq(shifts.userId, userId),
        eq(shifts.date, date),
        eq(shifts.roleAtShift, 'Maid')
    ));
    return;
  }
  
  await db.transaction(async (tx) => {
    // Находим существующие смены для этого userId, date и roleAtShift='Maid' (может быть несколько)
    const existing = await tx
      .select()
      .from(shifts)
      .where(and(
        eq(shifts.userId, userId),
        eq(shifts.date, date),
        eq(shifts.roleAtShift, 'Maid'),
        isNull(shifts.archivedAt)
      ));

    const previousStatus = existing.length > 0 ? existing[0].status : null;

    // Если статус меняется на Blank, Holiday или Unavailable - удаляем связанные задачи
    if (status !== 'Work' && previousStatus === 'Work') {
      // Удаляем задачи на эту дату (связь через дату)
      await tx.delete(shiftTasks).where(and(
        eq(shiftTasks.date, date),
        isNull(shiftTasks.archivedAt)
      ));
    }

    // Если статус Blank - обрабатываем в зависимости от роли пользователя
    if (status === 'Blank') {
      if (isAdmin) {
        // Админ удаляет всё под чистую (решение окончательное)
        await tx.delete(shifts).where(and(
          eq(shifts.userId, userId),
          eq(shifts.date, date),
          eq(shifts.roleAtShift, 'Maid')
        ));
      } else {
        // Менеджер предлагает удаление - создаем черновик поверх согласованной записи
        const approvedExists = existing.some(s => s.isApproved);
        if (approvedExists) {
          // Создаем или обновляем черновик со статусом Blank
          const draft = existing.find(s => !s.isApproved);
          if (draft) {
            await tx.update(shifts).set({ status: 'Blank', updatedAt: new Date() }).where(eq(shifts.id, draft.id));
          } else {
            await tx.insert(shifts).values({
              id: randomUUID(),
              date,
              userId,
              roleAtShift: 'Maid',
              status: 'Blank',
              isApproved: false,
              createdBy: 'system'
            });
          }
        } else {
          // Если согласованной записи нет, просто удаляем черновики
          await tx.delete(shifts).where(and(
            eq(shifts.userId, userId),
            eq(shifts.date, date),
            eq(shifts.roleAtShift, 'Maid'),
            eq(shifts.isApproved, false)
          ));
        }
      }
    } else {
      // Логика для менеджера: создание черновика поверх согласованной записи
      const approvedShift = existing.find(s => s.isApproved);
      const draftShift = existing.find(s => !s.isApproved);

      if (approvedShift && !draftShift) {
        // Если есть согласованная запись и нет черновика - создаем новый черновик
        await tx.insert(shifts).values({
          id: randomUUID(),
          date,
          userId,
          roleAtShift: 'Maid',
          status,
          isApproved: false,
          createdBy: 'system'
        });
      } else if (draftShift) {
        // Если есть черновик - обновляем его
        await tx.update(shifts).set({ status, updatedAt: new Date() }).where(eq(shifts.id, draftShift.id));
      } else {
        // Если нет записей - создаем новую
        await tx.insert(shifts).values({
          id: randomUUID(),
          date,
          userId,
          roleAtShift: 'Maid',
          status,
          isApproved: false,
          createdBy: 'system'
        });
      }
    }
  });
}

export async function bulkUpdateShifts(
  updates: ShiftUpdate[],
  createdBy: string,
  isAdmin: boolean = false
): Promise<void> {
  // Проверка: все даты смен должны находиться в периодах работы сотрудников с учетом роли
  // Для maid shifts, которые пришли как результат синхронизации, просто пропускаем невалидные
  for (const update of updates) {
    const isWithinPeriods = await isWithinEmploymentPeriods(update.userId, update.date, update.roleAtShift);
    if (!isWithinPeriods) {
      // Если это maid shift, просто пропускаем её (игнорируем), а не прерываем всю транзакцию
      if (update.roleAtShift === 'Maid') {
        continue;
      }
      throw new Error('Дата смены вне периода работы сотрудника для данной роли');
    }
  }

  // ОГРАНИЧЕНИЕ ДЛЯ АДМИНА: Проверяем наличие УЖЕ согласованного WORK именно в ЭТОЙ роли
  // Только админ не может назначить более одной смены Work на одну дату в рамках одной категории
  // Менеджеры могут предлагать изменения поверх согласованных записей
  if (isAdmin) {
    for (const update of updates) {
      if (update.status !== 'Work') continue;

      const targetDate = normalizeDate(update.date);
      const approvedShifts = await db
        .select()
        .from(shifts)
        .where(and(
          eq(shifts.date, targetDate),
          eq(shifts.roleAtShift, update.roleAtShift), // ИЗОЛЯЦИЯ: База ищет только своих
          eq(shifts.isApproved, true),
          eq(shifts.status, 'Work'),
          isNull(shifts.archivedAt)
        ));

      // Конфликт, если в МОЕЙ категории уже есть КТО-ТО ДРУГОЙ
      const otherShift = approvedShifts.filter(s => s.userId !== update.userId);
      if (otherShift.length > 0) {
        const allUsers = await getActiveUsers();
        const user = allUsers.find(u => u.id === otherShift[0].userId);
        const roleText = update.roleAtShift === 'Manager' ? 'менеджеров' : 'горничных';
        throw new Error(`Ошибка: На эту дату уже назначена согласованная рабочая смена для категории ${roleText}. Сотрудник: ${user?.fullName || otherShift[0].userId}`);
      }
    }
  }

  // Транзакция для обеспечения атомарности всех обновлений
  scheduleLogger.info(`[bulkUpdateShifts] Starting transaction for ${updates.length} updates`);
  
  await db.transaction(async (tx) => {
    for (const update of updates) {
      // Админ автоматически согласовывает свои изменения при сохранении
      const isApproved = isAdmin;
      const targetDate = normalizeDate(update.date);

      // Находим существующие смены для этого userId, date и roleAtShift (может быть несколько)
      const existing = await tx
        .select()
        .from(shifts)
        .where(and(
          eq(shifts.userId, update.userId),
          eq(shifts.date, targetDate),
          eq(shifts.roleAtShift, update.roleAtShift),
          isNull(shifts.archivedAt)
        ));

      const previousStatus = existing.length > 0 ? existing[0].status : null;

      // Если статус меняется на Blank, Holiday или Unavailable - удаляем связанные задачи
      if (update.status !== 'Work' && previousStatus === 'Work') {
        // Удаляем задачи на эту дату (связь через дату)
        await tx.delete(shiftTasks).where(and(
          eq(shiftTasks.date, targetDate),
          isNull(shiftTasks.archivedAt)
        ));
      }

      // Если статус Blank - обрабатываем в зависимости от роли пользователя
      if (update.status === 'Blank') {
        if (isAdmin) {
          // Админ удаляет всё под чистую (решение окончательное)
          await tx.delete(shifts).where(and(
            eq(shifts.userId, update.userId),
            eq(shifts.date, targetDate),
            eq(shifts.roleAtShift, update.roleAtShift)
          ));
        } else {
          // Менеджер предлагает удаление - создаем черновик поверх согласованной записи
          const approvedExists = existing.some(s => s.isApproved);
          if (approvedExists) {
            // Создаем или обновляем черновик со статусом Blank
            const draft = existing.find(s => !s.isApproved);
            if (draft) {
              // Обновляем существующий черновик
              await tx.update(shifts).set({ status: 'Blank', updatedAt: new Date() }).where(eq(shifts.id, draft.id));
            } else {
              // Создаем новый черновик
              await tx.insert(shifts).values({
                id: randomUUID(),
                date: targetDate,
                userId: update.userId,
                roleAtShift: update.roleAtShift,
                status: 'Blank',
                isApproved: false,
                createdBy
              });
            }
          } else {
            // Smart Removal: Если согласованной записи нет, просто удаляем черновики
            await tx.delete(shifts).where(and(
              eq(shifts.userId, update.userId),
              eq(shifts.date, targetDate),
              eq(shifts.roleAtShift, update.roleAtShift),
              eq(shifts.isApproved, false)
            ));
          }
        }
      } else {
        // Логика для менеджера: гарантированное обновление черновика
        if (!isAdmin) {
          // Находим существующий черновик (если есть)
          const draft = existing.find(s => !s.isApproved);
          
          if (draft) {
            // Если черновик найден - обновляем его
            await tx.update(shifts).set({ status: update.status, updatedAt: new Date() }).where(eq(shifts.id, draft.id));
          } else {
            // Если черновик не найден - создаем новую запись с isApproved: false
            await tx.insert(shifts).values({
              id: randomUUID(),
              date: targetDate,
              userId: update.userId,
              roleAtShift: update.roleAtShift,
              status: update.status,
              isApproved: false,
              createdBy
            });
          }
        } else {
          // Логика для админа: обновление существующей записи или создание новой
          // Сначала удаляем черновики для этой ячейки (решение админа финальное)
          await tx.delete(shifts).where(and(
            eq(shifts.userId, update.userId),
            eq(shifts.date, targetDate),
            eq(shifts.roleAtShift, update.roleAtShift),
            eq(shifts.isApproved, false)
          ));

          if (existing.length > 0) {
            // Обновляем существующую запись (теперь там только согласованные)
            await tx.update(shifts).set({ status: update.status, isApproved, updatedAt: new Date() }).where(eq(shifts.id, existing[0].id));
          } else {
            // Создаем новую запись
            await tx.insert(shifts).values({
              id: randomUUID(),
              date: targetDate,
              userId: update.userId,
              roleAtShift: update.roleAtShift,
              status: update.status,
              isApproved,
              createdBy
            });
          }
        }
      }

      // Синхронизация Manager -> Maid (только для менеджеров с флагом isMaidAlso)
      if (update.roleAtShift === 'Manager') {
        // Получаем данные пользователя для проверки флага isMaidAlso
        const userResult = await tx
          .select({ isMaidAlso: employees.isMaidAlso })
          .from(employees)
          .where(eq(employees.id, update.userId))
          .limit(1);

        const user = userResult[0];
        
        // Синхронизируем только если пользователь имеет флаг isMaidAlso === true
        if (user && user.isMaidAlso) {
          // Находим существующую смену Maid для этого userId и date
          const existingMaid = await tx
            .select()
            .from(shifts)
            .where(and(
              eq(shifts.userId, update.userId),
              eq(shifts.date, targetDate),
              eq(shifts.roleAtShift, 'Maid'),
              isNull(shifts.archivedAt)
            ));

          const previousMaidStatus = existingMaid.length > 0 ? existingMaid[0].status : null;

          // Если статус меняется на Blank, Holiday или Unavailable - удаляем связанные задачи
          if (update.status !== 'Work' && previousMaidStatus === 'Work') {
            // Удаляем задачи на эту дату (связь через дату)
            await tx.delete(shiftTasks).where(and(
              eq(shiftTasks.date, targetDate),
              isNull(shiftTasks.archivedAt)
            ));
          }

          // Если статус Blank - обрабатываем в зависимости от роли пользователя
          if (update.status === 'Blank') {
            if (isAdmin) {
              // Админ удаляет всё под чистую (решение окончательное)
              await tx.delete(shifts).where(and(
                eq(shifts.userId, update.userId),
                eq(shifts.date, targetDate),
                eq(shifts.roleAtShift, 'Maid')
              ));
            } else {
              // Менеджер предлагает удаление - создаем черновик поверх согласованной записи
              const approvedExists = existingMaid.some(s => s.isApproved);
              if (approvedExists) {
                // Создаем или обновляем черновик со статусом Blank
                const draft = existingMaid.find(s => !s.isApproved);
                if (draft) {
                  // Обновляем существующий черновик
                  await tx.update(shifts).set({ status: 'Blank', updatedAt: new Date() }).where(eq(shifts.id, draft.id));
                } else {
                  // Создаем новый черновик
                  await tx.insert(shifts).values({
                    id: randomUUID(),
                    date: targetDate,
                    userId: update.userId,
                    roleAtShift: 'Maid',
                    status: 'Blank',
                    isApproved: false,
                    createdBy: 'system'
                  });
                }
              } else {
                // Smart Removal: Если согласованной записи нет, просто удаляем черновики
                await tx.delete(shifts).where(and(
                  eq(shifts.userId, update.userId),
                  eq(shifts.date, targetDate),
                  eq(shifts.roleAtShift, 'Maid'),
                  eq(shifts.isApproved, false)
                ));
              }
            }
          } else {
            // Логика для менеджера: гарантированное обновление черновика
            const draft = existingMaid.find(s => !s.isApproved);
            
            if (draft) {
              // Если черновик найден - обновляем его
              await tx.update(shifts).set({ status: update.status, updatedAt: new Date() }).where(eq(shifts.id, draft.id));
            } else {
              // Если черновик не найден - создаем новую запись с isApproved: false
              await tx.insert(shifts).values({
                id: randomUUID(),
                date: targetDate,
                userId: update.userId,
                roleAtShift: 'Maid',
                status: update.status,
                isApproved: false,
                createdBy: 'system'
              });
            }

            // Для админа: чистка черновиков Maid после синхронизации
            if (isAdmin) {
              await tx.delete(shifts).where(and(
                eq(shifts.userId, update.userId),
                eq(shifts.date, targetDate),
                eq(shifts.roleAtShift, 'Maid'),
                eq(shifts.isApproved, false)
              ));
            }
          }
        }
      }
    }
  });

  // Уведомляем админов/менеджеров об изменениях в графике
  if (updates.length > 0) {
    const firstUpdateDate = updates[0].date;
    const month = firstUpdateDate.getMonth() + 1;
    const year = firstUpdateDate.getFullYear();

    // Создаем заявку на согласование для менеджеров (не админов)
    if (!isAdmin) {
      // Получаем роль пользователя, который сделал изменение
      const userResult = await db
        .select({ role: employees.role })
        .from(employees)
        .where(eq(employees.id, createdBy))
        .limit(1);

      const user = userResult[0];
      
      // Если это менеджер, создаем заявку на согласование (всегда, если менеджер нажал "Сохранить")
      if (user && user.role === USER_ROLES.MANAGER) {
        scheduleLogger.debug('Creating approval request for manager', { createdBy, month, year, updatesCount: updates.length });
        await createApprovalRequest({
          entityType: 'shift',
          targetId: null,
          requesterId: createdBy,
          payload: {
            month,
            year,
            updatesCount: updates.length,
          },
        });
      }
    }

    await notifyAdmins('shift:update', { month, year });
  }
}

export async function approveShiftsForMonth(month: number, year: number): Promise<void> {
  // Используем dayjs.utc для консистентности с остальным проектом
  // Нормализуем даты: начало дня для startDate, конец дня для endDate
  const startDate = dayjs.utc().year(year).month(month - 1).startOf('month').startOf('day').toDate();
  const endDate = dayjs.utc().year(year).month(month - 1).endOf('month').endOf('day').toDate();

  const allShifts = await db
    .select()
    .from(shifts)
    .where(and(
      gte(shifts.date, startDate),
      lte(shifts.date, endDate),
      isNull(shifts.archivedAt)
    ));

  const allUsers = await getActiveUsers();
  const userMap = new Map(allUsers.map(u => [u.id, u.fullName]));
  const shiftsByDateAndRole = new Map<string, typeof allShifts>();

  allShifts.forEach(shift => {
    // ИЗОЛЯЦИЯ: ключ группировки строго по календарному дню UTC и роли
    const key = `${dayjs.utc(shift.date).format('YYYY-MM-DD')}-${shift.roleAtShift}`;
    if (!shiftsByDateAndRole.has(key)) shiftsByDateAndRole.set(key, []);
    shiftsByDateAndRole.get(key)!.push(shift);
  });

  for (const [key, group] of shiftsByDateAndRole.entries()) {
    const workShifts = group.filter(s => s.status === 'Work');
    if (workShifts.length > 1) {
      const [date, role] = key.split('-');
      const names = workShifts.map(s => userMap.get(s.userId) || s.userId).join(', ');
      const roleText = role === 'Manager' ? 'менеджеров' : 'горничных';
      throw new Error(`Невозможно одобрить смены на ${date} для ${roleText}: обнаружено более одной активной смены. Конфликтующие сотрудники: ${names}`);
    }
  }

  // Перед согласованием черновиков удаляем старые согласованные записи для тех же ячеек
  // Это предотвратит конфликт дубликатов при попытке сделать две записи согласованными
  const draftShifts = allShifts.filter(s => !s.isApproved);
  for (const draft of draftShifts) {
    // Находим и удаляем старую согласованную запись для той же ячейки
    await db.delete(shifts).where(and(
      eq(shifts.userId, draft.userId),
      eq(shifts.date, draft.date),
      eq(shifts.roleAtShift, draft.roleAtShift),
      eq(shifts.isApproved, true)
    ));
  }

  // Теперь обновляем черновики, делая их согласованными
  await db.update(shifts).set({ isApproved: true, updatedAt: new Date() })
    .where(and(
      gte(shifts.date, startDate),
      lte(shifts.date, endDate),
      eq(shifts.isApproved, false),
      isNull(shifts.archivedAt)
    ));

  // Удаление согласованных пустых смен, чтобы не засорять БД
  await db.delete(shifts)
    .where(and(
      gte(shifts.date, startDate),
      lte(shifts.date, endDate),
      eq(shifts.status, 'Blank'),
      eq(shifts.isApproved, true)
    ));

  // Физическое удаление обработанных заявок на согласование
  await db.delete(approvalRequests)
    .where(and(
      eq(approvalRequests.entityType, 'shift'),
      sql`json_extract(${approvalRequests.payload}, '$.month') = ${month}`,
      sql`json_extract(${approvalRequests.payload}, '$.year') = ${year}`
    ));

  // Уведомляем админов для пересчета агрегированных уведомлений
  const admins = await getActiveUsers();
  const adminIds = admins.filter(a => a.role === USER_ROLES.ADMIN).map(a => a.id);
  
  await notifyMultipleUsers({
    userIds: adminIds,
    event: 'approval:created',
    data: { month, year }
  });
}

export async function getPendingApprovalShifts(month: number, year: number): Promise<Shift[]> {
  // Используем dayjs.utc для консистентности с остальным проектом
  // Нормализуем даты: начало дня для startDate, конец дня для endDate
  const startDate = dayjs.utc().year(year).month(month - 1).startOf('month').startOf('day').toDate();
  const endDate = dayjs.utc().year(year).month(month - 1).endOf('month').endOf('day').toDate();

  const result = await db.select().from(shifts)
    .where(and(
      gte(shifts.date, startDate),
      lte(shifts.date, endDate),
      eq(shifts.isApproved, false),
      isNull(shifts.archivedAt)
    ));

  return result.map(s => ({
    ...s,
    date: s.date.toISOString(),
    createdAt: s.createdAt.toISOString(),
    updatedAt: s.updatedAt.toISOString(),
    archivedAt: s.archivedAt ? s.archivedAt.toISOString() : null,
    roleAtShift: s.roleAtShift as 'Manager' | 'Maid',
    status: s.status as 'Blank' | 'Work' | 'Holiday' | 'Unavailable',
  })) as Shift[];
}

export async function rejectShiftsForMonth(month: number, year: number): Promise<number> {
  // Используем dayjs.utc для консистентности с остальным проектом
  // Нормализуем даты: начало дня для startDate, конец дня для endDate
  const startDate = dayjs.utc().year(year).month(month - 1).startOf('month').startOf('day').toDate();
  const endDate = dayjs.utc().year(year).month(month - 1).endOf('month').endOf('day').toDate();

  const toReject = await db.select().from(shifts)
    .where(and(
      gte(shifts.date, startDate),
      lte(shifts.date, endDate),
      eq(shifts.isApproved, false),
      isNull(shifts.archivedAt)
    ));

  await db.delete(shifts)
    .where(and(
      gte(shifts.date, startDate),
      lte(shifts.date, endDate),
      eq(shifts.isApproved, false),
      isNull(shifts.archivedAt)
    ));

  // Физическое удаление обработанных заявок на согласование
  await db.delete(approvalRequests)
    .where(and(
      eq(approvalRequests.entityType, 'shift'),
      sql`json_extract(${approvalRequests.payload}, '$.month') = ${month}`,
      sql`json_extract(${approvalRequests.payload}, '$.year') = ${year}`
    ));

  // Уведомляем админов для пересчета агрегированных уведомлений
  const admins = await getActiveUsers();
  const adminIds = admins.filter(a => a.role === USER_ROLES.ADMIN).map(a => a.id);
  
  await notifyMultipleUsers({
    userIds: adminIds,
    event: 'approval:created',
    data: { month, year }
  });

  return toReject.length;
}

export async function getPendingCount(): Promise<number> {
  const result = await db.select().from(shifts)
    .where(and(eq(shifts.isApproved, false), isNull(shifts.archivedAt)));
  return result.length;
}

/**
 * Экспорт смен в CSV формат
 * @param month - Месяц (1-12)
 * @param year - Год
 * @returns Объект с CSV данными и именем файла
 */
export async function exportShiftsToCSV(month: number, year: number): Promise<{ csv: string; filename: string }> {
  // Получаем данные смен за месяц
  const startDate = dayjs.utc().year(year).month(month - 1).startOf('month').toDate();
  const endDate = dayjs.utc().year(year).month(month - 1).endOf('month').toDate();

  const shiftsResult = await db
    .select()
    .from(shifts)
    .where(and(
      between(shifts.date, startDate, endDate),
      isNull(shifts.archivedAt)
    ));

  // Получаем пользователей для отображения имен
  const userIds = [...new Set(shiftsResult.map(s => s.userId))];
  const usersResult = await db
    .select({
      id: employees.id,
      fullName: employees.fullName,
    })
    .from(employees)
    .where(inArray(employees.id, userIds));

  const userMap = new Map(usersResult.map(u => [u.id, u.fullName]));

  // Формируем данные для экспорта
  const exportData = shiftsResult.map(shift => ({
    date: dayjs.utc(shift.date).format('YYYY-MM-DD'),
    userId: shift.userId,
    userName: userMap.get(shift.userId) || 'Неизвестно',
    role: shift.roleAtShift === 'Manager' ? 'Менеджер' : 'Горничная',
    status: getShiftStatusLabel(shift.status),
    isApproved: shift.isApproved ? 'Да' : 'Нет',
    createdBy: shift.createdBy,
    createdAt: dayjs.utc(shift.createdAt).format('YYYY-MM-DD HH:mm:ss'),
  }));

  // Определяем колонки для экспорта
  const columns = [
    { key: 'date', label: 'Дата' },
    { key: 'userId', label: 'ID сотрудника' },
    { key: 'userName', label: 'Сотрудник' },
    { key: 'role', label: 'Роль' },
    { key: 'status', label: 'Статус' },
    { key: 'isApproved', label: 'Согласовано' },
    { key: 'createdBy', label: 'Кто создал' },
    { key: 'createdAt', label: 'Дата создания' },
  ];

  const csv = exportToCSV(exportData, columns);
  // Формат имени файла: schedule_MM_YYYY.csv
  const monthStr = String(month).padStart(2, '0');
  const filename = `schedule_${monthStr}_${year}.csv`;

  return { csv, filename };
}

/**
 * Возвращает русское название статуса смены
 */
function getShiftStatusLabel(status: string): string {
  const statusMap: Record<string, string> = {
    'Work': 'Работа',
    'Holiday': 'Отпуск',
    'Unavailable': 'Недоступен',
    'Blank': 'Пусто',
  };
  return statusMap[status] || status;
}