// src/server/features/operations/operations.service.ts
// Business logic for operations management (check-in, linen, dispatch)

import { eq, and, isNull, gte, lte, inArray } from 'drizzle-orm';
import { randomUUID } from 'node:crypto';
import { db } from '../../shared/db/client';
import { dailyCheckins, invLinen, linenDispatchLog, dailyLinenUsage } from './db';
import { rooms } from '@server/features/rooms/db/rooms.table';
import { dayjs } from '../../shared/lib/dayjs';
import { AppError } from '../../shared/lib/errors';
import type {
  CreateDailyCheckinInput,
  PatchDailyCheckinInput,
  DailyCheckinResponse,
  InvLinenResponse,
  CreateLinenDispatchLogInput,
  LinenDispatchLogResponse,
  DispatchLaundryInput,
  LinenUsageReport,
  GetLinenUsageReportInput,
  DailyLinenUsageResponse,
  PatchDailyLinenUsageInput,
} from './operations.schema';

/**
 * Возвращает текущий операционный день
 * Если время до 10:00 МСК - возвращаем вчерашнюю дату, если после - сегодняшнюю
 */
function getOperationalDay(): Date {
  const now = dayjs().tz('Europe/Moscow');
  const hour = now.hour();
  
  // Если до 10:00 утра, операционный день - вчера
  if (hour < 9) {
    return now.subtract(1, 'day').startOf('day').toDate();
  }
  
  // Иначе - сегодня
  return now.startOf('day').toDate();
}

/**
 * Возвращает операционный день как строку в формате YYYY-MM-DD
 */
function getOperationalDayString(): string {
  return dayjs(getOperationalDay()).format('YYYY-MM-DD');
}

// Helper function to convert DB result to response format
function formatDailyCheckinResponse(result: any): DailyCheckinResponse {
  // Ensure linenData is an array (handle JSON string from MySQL)
  let parsedLinen: any[] = [];
  try {
    const raw = result.linenData;
    // 1. Если это строка, парсим
    let temp = typeof raw === 'string' ? JSON.parse(raw) : raw;
    
    // 2. CRITICAL FIX: Если после парсинга это снова строка (двойная сериализация), парсим еще раз
    if (typeof temp === 'string') {
      try { temp = JSON.parse(temp); } catch { temp = []; }
    }

    // 3. Финальная проверка на массив
    parsedLinen = Array.isArray(temp) ? temp : [];
  } catch (e) {
    parsedLinen = [];
  }

  return {
    id: result.id,
    roomId: result.roomId,
    roomType: result.roomType || 'Unknown',
    date: dayjs(result.date).format('YYYY-MM-DD'),
    isOccupied: Boolean(result.isOccupied),
    isPermanent: Boolean(result.isPermanent),
    bedsCount: Number(result.bedsCount),
    isCleaned: Boolean(result.isCleaned),
    cleanerName: result.cleanerName ?? null,
    linenData: parsedLinen,
    isDispatched: Boolean(result.isDispatched),
    createdAt: dayjs(result.createdAt).toISOString(),
    updatedAt: dayjs(result.updatedAt).toISOString(),
  };
}

function formatInvLinenResponse(result: any): InvLinenResponse {
  return {
    ...result,
    type: result.type as any,
    createdAt: dayjs(result.createdAt).toISOString(),
    updatedAt: dayjs(result.updatedAt).toISOString(),
    archivedAt: result.archivedAt ? dayjs(result.archivedAt).toISOString() : null,
  };
}

function formatLinenDispatchLogResponse(result: any): LinenDispatchLogResponse {
  return {
    ...result,
    dispatchDate: dayjs(result.dispatchDate).format('YYYY-MM-DD'),
    dispatchedAt: dayjs(result.dispatchedAt).toISOString(),
    createdAt: dayjs(result.createdAt).toISOString(),
  };
}

// Daily Check-in Functions
export async function getDailyCheckinsByDate(dateStr: string): Promise<DailyCheckinResponse[]> {
  // Use string format YYYY-MM-DD for exact date matching
  const formattedDate = dayjs(dateStr).tz('Europe/Moscow').format('YYYY-MM-DD');

  // Validation: Check if date is not in the future
  const currentOpDay = getOperationalDayString();
  if (formattedDate > currentOpDay) {
    throw new AppError('Нельзя открывать заезды на будущие даты', 400, 'INVALID_DATE');
  }

  // Query existing checkins for the date using exact string match
  const results = await db
    .select()
    .from(dailyCheckins)
    .where(eq(dailyCheckins.date, formattedDate))
    .orderBy(dailyCheckins.roomId);

  // Lazy Initialization: If no checkins exist, create snapshots from active rooms
  if (results.length === 0) {
    const activeRooms = await db
      .select()
      .from(rooms)
      .where(eq(rooms.isRepairing, false)) // Get active rooms (not in repair)
      .orderBy(rooms.id);

    if (activeRooms.length > 0) {
      const now = dayjs().tz('Europe/Moscow').toDate();
      const newCheckins = activeRooms.map((room) => ({
        id: randomUUID(),
        roomId: room.id,
        roomType: room.type, // Snapshot: room type (апарт/номер)
        date: formattedDate, // Use string format YYYY-MM-DD
        isOccupied: false,
        isPermanent: false,
        bedsCount: room.beds || 0, // Snapshot: beds from room
        isCleaned: false,
        cleanerName: null,
        linenData: [], // Drizzle handles JSON serialization automatically
        isDispatched: false,
        createdAt: now,
        updatedAt: now,
      }));

      // Use transaction to prevent duplicate inserts from parallel requests
      await db.transaction(async (tx) => {
        // Check if any checkins already exist for this date (prevent race condition)
        const existingCheckins = await tx
          .select()
          .from(dailyCheckins)
          .where(eq(dailyCheckins.date, formattedDate));

        // Only insert if no checkins exist for this date
        if (existingCheckins.length === 0) {
          try {
            await tx.insert(dailyCheckins).values(newCheckins);
          } catch (e) {
            // Если кто-то уже вставил данные между проверкой и вставкой — просто игнорируем
            // Это предотвращает ошибки дубликатов при параллельных запросах
            console.log('[OperationsService] Duplicate insert detected, ignoring:', e);
          }
        }
      });

      // Return the newly created records (or existing if they were created by another request)
      const finalResults = await db
        .select()
        .from(dailyCheckins)
        .where(eq(dailyCheckins.date, formattedDate))
        .orderBy(dailyCheckins.roomId);

      return finalResults.map(formatDailyCheckinResponse);
    }
  }

  return results.map(formatDailyCheckinResponse);
}

export async function getDailyCheckinById(id: string): Promise<DailyCheckinResponse | null> {
  const results = await db
    .select()
    .from(dailyCheckins)
    .where(eq(dailyCheckins.id, id));

  if (!results[0]) return null;
  return formatDailyCheckinResponse(results[0]);
}

export async function createDailyCheckin(data: CreateDailyCheckinInput): Promise<string> {
  const id = randomUUID();
  const now = dayjs().tz('Europe/Moscow').toDate();

  // Get room data for snapshot
  const room = await db.query.rooms.findFirst({
    where: eq(rooms.id, data.roomId),
    columns: {
      id: true,
      type: true,
      beds: true,
    }
  });

  if (!room) {
    throw new AppError('Room not found', 404);
  }

  await db.insert(dailyCheckins).values({
    id,
    roomId: data.roomId,
    roomType: room.type, // Snapshot data
    date: dayjs(data.date).format('YYYY-MM-DD'), // Use string format YYYY-MM-DD
    isOccupied: false,
    isPermanent: false,
    bedsCount: room.beds || 0, // Snapshot: beds from room
    isCleaned: data.isCleaned,
    cleanerName: data.cleanerName,
    linenData: data.linenData,
    isDispatched: data.isDispatched,
    createdAt: now,
    updatedAt: now,
  });

  return id;
}

export async function patchCheckin(id: string, data: PatchDailyCheckinInput): Promise<DailyCheckinResponse | null> {
  // Get the existing checkin to validate date
  const existing = await getDailyCheckinById(id);
  if (!existing) {
    return null;
  }

  // Validation: Check if date is not earlier than current operational day
  const currentOpDay = getOperationalDayString();
  const checkinDate = existing.date;

  if (checkinDate < currentOpDay) {
    throw new AppError('Доступ запрещен: нельзя редактировать прошлые операционные дни', 403, 'FORBIDDEN');
  }

  const now = dayjs().tz('Europe/Moscow').toDate();

  // Filter out undefined values to avoid database issues
  const updateData: any = {};
  if (data.isOccupied !== undefined) {
    updateData.isOccupied = data.isOccupied;
  }
  if (data.isPermanent !== undefined) {
    updateData.isPermanent = data.isPermanent;
  }
  if (data.bedsCount !== undefined) {
    updateData.bedsCount = data.bedsCount;
  }
  if (data.isCleaned !== undefined) {
    updateData.isCleaned = data.isCleaned;
  }
  if (data.cleanerName !== undefined) {
    updateData.cleanerName = data.cleanerName;
  }
  if (data.linenData !== undefined) {
    updateData.linenData = data.linenData;
  }
  updateData.updatedAt = now;

  await db
    .update(dailyCheckins)
    .set(updateData)
    .where(eq(dailyCheckins.id, id));

  return getDailyCheckinById(id);
}

// Inventory Linen Functions
export async function getLinenTypes(): Promise<InvLinenResponse[]> {
  const results = await db
    .select()
    .from(invLinen)
    .where(isNull(invLinen.archivedAt))
    .orderBy(invLinen.name);

  return results.map(formatInvLinenResponse);
}

export async function getInvLinenById(id: string): Promise<InvLinenResponse | null> {
  const results = await db
    .select()
    .from(invLinen)
    .where(and(eq(invLinen.id, id), isNull(invLinen.archivedAt)));

  if (!results[0]) return null;
  return formatInvLinenResponse(results[0]);
}

// Linen Dispatch Functions
export async function getLinenDispatchLogByDate(dateStr: string): Promise<LinenDispatchLogResponse | null> {
  const formattedDate = dayjs(dateStr).tz('Europe/Moscow').format('YYYY-MM-DD');

  const results = await db
    .select()
    .from(linenDispatchLog)
    .where(eq(linenDispatchLog.dispatchDate, formattedDate));

  if (!results[0]) return null;
  return formatLinenDispatchLogResponse(results[0]);
}

export async function createLinenDispatchLog(data: CreateLinenDispatchLogInput): Promise<string> {
  const id = randomUUID();
  const now = dayjs().tz('Europe/Moscow').toDate();

  await db.insert(linenDispatchLog).values({
    id,
    dispatchDate: dayjs(data.dispatchDate).format('YYYY-MM-DD'), // Use string format YYYY-MM-DD
    totalWeight: data.totalWeight,
    details: data.details,
    dispatchedAt: now,
    createdAt: now,
  });

  return id;
}

export async function dispatchLaundry(dateStr: string): Promise<string | null> {
  // Check if already dispatched today
  const existingLog = await getLinenDispatchLogByDate(dateStr);
  if (existingLog) {
    return null; // Already dispatched
  }

  // Get all checkins for today with linen data and not dispatched
  const checkins = await getDailyCheckinsByDate(dateStr);
  const checkinsWithLinen = checkins.filter(
    (c) => c.linenData && c.linenData.length > 0 && !c.isDispatched
  );

  if (checkinsWithLinen.length === 0) {
    return null; // No linen to dispatch
  }

  // Aggregate linen items and calculate total weight
  const dispatchDetails = [];
  let totalWeight = 0;

  for (const checkin of checkinsWithLinen) {
    if (checkin.linenData) {
      const roomTotalWeight = checkin.linenData.reduce((sum, item) => sum + (item.weight * item.quantity), 0);
      totalWeight += roomTotalWeight;
      
      dispatchDetails.push({
        roomId: checkin.roomId, // Room ID (e.g., "A1", "B12")
        items: checkin.linenData,
        totalWeight: roomTotalWeight,
      });
    }
  }

  // Create dispatch log
  const dispatchId = await createLinenDispatchLog({
    dispatchDate: dateStr,
    totalWeight,
    details: dispatchDetails,
  });

  // Mark checkins as dispatched
  for (const checkin of checkinsWithLinen) {
    await db
      .update(dailyCheckins)
      .set({ isDispatched: true })
      .where(eq(dailyCheckins.id, checkin.id));
  }

  return dispatchId;
}

// Linen Usage Report Functions
export async function getLinenUsageReport(
  input: GetLinenUsageReportInput
): Promise<LinenUsageReport> {
  const startDate = dayjs(input.from).tz('Europe/Moscow').format('YYYY-MM-DD');
  const endDate = dayjs(input.to).tz('Europe/Moscow').format('YYYY-MM-DD');

  // Query all daily checkins within the date range (string comparison works for YYYY-MM-DD)
  const checkins = await db
    .select()
    .from(dailyCheckins)
    .where(
      and(
        gte(dailyCheckins.date, startDate),
        lte(dailyCheckins.date, endDate)
      )
    );

  // Aggregate linen data by inventory item
  const linenMap = new Map<string, { name: string; totalQuantity: number; totalWeight: number }>();

  for (const checkin of checkins) {
    if (checkin.linenData && Array.isArray(checkin.linenData)) {
      for (const item of checkin.linenData) {
        const key = item.inventoryId || item.name; // Use inventoryId or fallback to name
        const existing = linenMap.get(key);

        if (existing) {
          existing.totalQuantity += item.quantity || 0;
          existing.totalWeight += (item.weight || 0) * (item.quantity || 0);
        } else {
          linenMap.set(key, {
            name: item.name,
            totalQuantity: item.quantity || 0,
            totalWeight: (item.weight || 0) * (item.quantity || 0),
          });
        }
      }
    }
  }

  // Convert map to array
  const items = Array.from(linenMap.values());
  const grandTotalWeight = items.reduce((sum, item) => sum + item.totalWeight, 0);

  return {
    items,
    grandTotalWeight,
  };
}

// Global Daily Linen Usage Functions
function formatDailyLinenUsageResponse(result: any): DailyLinenUsageResponse {
  // Ensure linenData is an array (handle JSON string from MySQL)
  let parsedLinen: any[] = [];
  try {
    const raw = result.linenData;
    // 1. Если это строка, парсим
    let temp = typeof raw === 'string' ? JSON.parse(raw) : raw;
    
    // 2. CRITICAL FIX: Если после парсинга это снова строка (двойная сериализация), парсим еще раз
    if (typeof temp === 'string') {
      try { temp = JSON.parse(temp); } catch { temp = []; }
    }

    // 3. Финальная проверка на массив
    parsedLinen = Array.isArray(temp) ? temp : [];
  } catch (e) {
    parsedLinen = [];
  }

  return {
    id: result.id,
    date: dayjs(result.date).format('YYYY-MM-DD'),
    linenData: parsedLinen,
    updatedAt: dayjs(result.updatedAt).toISOString(),
  };
}

export async function getDailyLinenUsageByDate(dateStr: string): Promise<DailyLinenUsageResponse> {
  const formattedDate = dayjs(dateStr).tz('Europe/Moscow').format('YYYY-MM-DD');

  // Validation: Check if date is not in the future
  const currentOpDay = getOperationalDayString();
  if (formattedDate > currentOpDay) {
    throw new AppError('Нельзя открывать данные о белье на будущие даты', 400, 'INVALID_DATE');
  }

  const results = await db
    .select()
    .from(dailyLinenUsage)
    .where(eq(dailyLinenUsage.date, formattedDate));

  if (!results[0]) {
    // Return empty array structure if no record exists
    return {
      id: null,
      date: dateStr,
      linenData: [],
      updatedAt: dayjs().toISOString(),
    };
  }

  return formatDailyLinenUsageResponse(results[0]);
}

export async function upsertDailyLinenUsage(data: PatchDailyLinenUsageInput, userRole: string = 'GUEST'): Promise<DailyLinenUsageResponse> {
  const formattedDate = dayjs(data.date).tz('Europe/Moscow').format('YYYY-MM-DD');
  const now = dayjs().tz('Europe/Moscow').toDate();

  // Validation: Check if date is not earlier than current operational day
  const currentOpDay = getOperationalDayString();
  if (formattedDate < currentOpDay && userRole !== 'ADMIN') {
    throw new AppError('Доступ запрещен: нельзя менять данные о белье за прошлые дни', 403, 'FORBIDDEN');
  }

  // Check if record exists for the date
  const existing = await db
    .select()
    .from(dailyLinenUsage)
    .where(eq(dailyLinenUsage.date, formattedDate));

  if (existing[0]) {
    // Update existing record
    await db
      .update(dailyLinenUsage)
      .set({
        linenData: data.linenData,
        updatedAt: now,
      })
      .where(eq(dailyLinenUsage.id, existing[0].id));

    const updated = await db
      .select()
      .from(dailyLinenUsage)
      .where(eq(dailyLinenUsage.id, existing[0].id));

    return formatDailyLinenUsageResponse(updated[0]);
  } else {
    // Insert new record
    const id = randomUUID();
    await db.insert(dailyLinenUsage).values({
      id,
      date: formattedDate, // Use string format YYYY-MM-DD
      linenData: data.linenData,
      updatedAt: now,
    });

    const inserted = await db
      .select()
      .from(dailyLinenUsage)
      .where(eq(dailyLinenUsage.id, id));

    return formatDailyLinenUsageResponse(inserted[0]);
  }
}

/**
 * Синхронизирует daily_checkins с актуальным списком номеров из rooms
 * Добавляет новые номера и удаляет устаревшие снапшоты (Hard Delete)
 */
export async function syncDailyCheckins(dateStr: string): Promise<{ added: number; deleted: number }> {
  const formattedDate = dayjs(dateStr).tz('Europe/Moscow').format('YYYY-MM-DD');
  const currentOpDay = getOperationalDayString();

  // Validation: Sync only allowed for current operational day
  if (formattedDate !== currentOpDay) {
    throw new AppError('Синхронизация возможна только для текущего операционного дня', 400, 'INVALID_DATE');
  }

  let addedCount = 0;
  let deletedCount = 0;

  await db.transaction(async (tx) => {
    // Get active rooms (not in repair)
    const activeRooms = await tx
      .select()
      .from(rooms)
      .where(eq(rooms.isRepairing, false))
      .orderBy(rooms.id);

    // Get existing checkins for the date
    const existingCheckins = await tx
      .select()
      .from(dailyCheckins)
      .where(eq(dailyCheckins.date, formattedDate));

    const activeRoomIds = activeRooms.map((r) => r.id);
    const existingRoomIds = existingCheckins.map((c) => c.roomId);

    // 1. Add new rooms that don't have checkins yet
    const roomsToAdd = activeRooms.filter((r) => !existingRoomIds.includes(r.id));
    if (roomsToAdd.length > 0) {
      const now = new Date();
      const newEntries = roomsToAdd.map((room) => ({
        id: randomUUID(),
        roomId: room.id,
        roomType: room.type,
        date: formattedDate,
        isOccupied: false,
        isPermanent: false,
        bedsCount: room.beds || 0,
        isCleaned: false,
        cleanerName: null,
        linenData: [], // Empty array, not null
        isDispatched: false,
        createdAt: now,
        updatedAt: now,
      }));
      await tx.insert(dailyCheckins).values(newEntries);
      addedCount = roomsToAdd.length;
    }

    // 2. Hard delete checkins for rooms that are no longer active (in repair or deleted)
    // Important: Do NOT delete records that have data (e.g., isOccupied: true)
    // to preserve administrator's work
    const idsToDelete = existingCheckins
      .filter((c) => {
        // Room is no longer active
        const roomInactive = !activeRoomIds.includes(c.roomId);
        // Check if record has meaningful data
        const hasData = c.isOccupied || 
                       c.isPermanent || 
                       (c.linenData && Array.isArray(c.linenData) && c.linenData.length > 0) ||
                       (c.cleanerName && c.cleanerName.trim() !== '');
        
        // Delete only if room is inactive AND record has no data
        return roomInactive && !hasData;
      })
      .map((c) => c.id);

    if (idsToDelete.length > 0) {
      await tx
        .delete(dailyCheckins)
        .where(inArray(dailyCheckins.id, idsToDelete));
      deletedCount = idsToDelete.length;
    }
  });

  return { added: addedCount, deleted: deletedCount };
}

