// src/server/features/paymaster/paymaster.service.ts
// Business logic for paymaster (cash register) management

import { eq, sql, min, max, and, gte, lte } from 'drizzle-orm';
import { db } from '@serverShared/db/client';
import { paymasterRows } from './db';
import { dailyCheckins } from '../operations/db';
import { dayjs } from '@serverShared/lib/dayjs';
import { AppError } from '@serverShared/lib/errors';
import { hasPermission } from '@serverShared/plugins/rbac';
import type { UserInfo } from '@serverShared/lib/auth';
import { AppPermission } from '@shared/contracts/permissions';
import { PAYMASTER_TYPES } from '@shared/contracts/paymaster';
import type {
  CreatePaymasterRowInput,
  UpdatePaymasterRowInput,
  PaymasterRow,
  PaymasterTotals,
  DateBoundaries,
  PaymasterPeriodReport,
} from './paymaster.schema';

/**
 * Возвращает текущий операционный день
 * Если время до 10:00 МСК - возвращаем вчерашнюю дату, если после - сегодняшнюю
 */
export 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
 */
export function getOperationalDayString(): string {
  return dayjs(getOperationalDay()).format('YYYY-MM-DD');
}

/**
 * Получает границы доступных дат для навигации
 * minDate: самая старая запись в системе (из paymaster_rows или daily_checkins)
 * maxDate: всегда ограничивается текущим операционным днем
 */
export async function getDateBoundaries(): Promise<DateBoundaries> {
  // Получаем минимальную дату из paymaster_rows
  const paymasterMinResult = await db
    .select({ minDate: min(paymasterRows.operationalDay) })
    .from(paymasterRows);

  // Получаем минимальную дату из daily_checkins
  const checkinsMinResult = await db
    .select({ minDate: min(dailyCheckins.date) })
    .from(dailyCheckins);

  // Определяем самую раннюю дату
  const paymasterMin = paymasterMinResult[0]?.minDate;
  const checkinsMin = checkinsMinResult[0]?.minDate;

  let minDate: Date;
  if (paymasterMin && checkinsMin) {
    // Приводим к строкам для сравнения
    const paymasterMinStr = dayjs(paymasterMin).format('YYYY-MM-DD');
    const checkinsMinStr = dayjs(checkinsMin).format('YYYY-MM-DD');
    minDate = paymasterMinStr < checkinsMinStr ? paymasterMin : new Date(checkinsMin);
  } else if (paymasterMin) {
    minDate = paymasterMin;
  } else if (checkinsMin) {
    minDate = new Date(checkinsMin);
  } else {
    // Если данных нет, используем текущий операционный день
    minDate = getOperationalDay();
  }

  // maxDate всегда ограничивается текущим операционным днем
  const maxDate = getOperationalDay();

  return {
    minDate: dayjs(minDate).format('YYYY-MM-DD'),
    maxDate: dayjs(maxDate).format('YYYY-MM-DD'),
  };
}

/**
 * Получает записи кассы за указанный операционный день
 */
export async function getPaymasterRowsByDate(date: string): Promise<PaymasterRow[]> {
  // Форматируем дату в строку YYYY-MM-DD для сравнения с MySQL DATE типом
  const formattedDate = dayjs(date).format('YYYY-MM-DD');
  
  const results = await db
    .select()
    .from(paymasterRows)
    .where(sql`${paymasterRows.operationalDay} = ${formattedDate}`)
    .orderBy(paymasterRows.createdAt);

  // Map dates to ISO strings for API response
  return results.map(r => ({
    ...r,
    amount: Number(r.amount), // Обязательный каст в число (MySQL возвращает строку)
    type: r.type as PaymasterRow['type'], // Явное приведение типа
    operationalDay: dayjs(r.operationalDay).format('YYYY-MM-DD'),
    createdAt: r.createdAt.toISOString(),
    updatedAt: r.updatedAt.toISOString(),
  }));
}

/**
 * Получает записи за текущий операционный день
 */
export async function getCurrentPaymasterRows(): Promise<PaymasterRow[]> {
  return getPaymasterRowsByDate(getOperationalDayString());
}

/**
 * Получает запись кассы по ID
 */
export async function getPaymasterRowById(id: string): Promise<PaymasterRow | null> {
  const results = await db
    .select()
    .from(paymasterRows)
    .where(eq(paymasterRows.id, id));

  const row = results[0] || null;
  if (!row) return null;

  // Map dates to ISO strings for API response
  return {
    ...row,
    amount: Number(row.amount), // Обязательный каст в число (MySQL возвращает строку)
    type: row.type as PaymasterRow['type'], // Явное приведение типа
    operationalDay: dayjs(row.operationalDay).format('YYYY-MM-DD'),
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
  };
}

/**
 * Возвращает вчерашний операционный день
 */
export function getYesterdayOperationalDay(): Date {
  return dayjs(getOperationalDay()).subtract(1, 'day').startOf('day').toDate();
}

/**
 * Проверяет права доступа для редактирования/удаления записи
 * Уровни доступа (каскадные):
 *   paymaster:any_date — полная свобода
 *   paymaster:yesterday_too — текущий + вчерашний опердень
 *   paymaster:today_only — только текущий опердень
 *   paymaster:read_only — просмотр без редактирования
 * @param user - пользователь
 * @param rowOperationalDay - операционный день записи (Date или строка YYYY-MM-DD)
 * @throws AppError с кодом 403 если нет прав
 */
export async function validateEditPermissions(user: UserInfo, rowOperationalDay: Date | string): Promise<void> {
  const currentOpDay = getOperationalDay();
  const yesterdayOpDay = getYesterdayOperationalDay();

  const canAny = await hasPermission(user, AppPermission.PAYMASTER_ANY_DATE);
  if (canAny) return;

  const canYesterdayToo = await hasPermission(user, AppPermission.PAYMASTER_YESTERDAY_TOO);
  const canToday = await hasPermission(user, AppPermission.PAYMASTER_TODAY_ONLY);

  if (!canYesterdayToo && !canToday) {
    throw new AppError('Нет прав на редактирование кассы', 403, 'FORBIDDEN');
  }

  const rowOpDay = typeof rowOperationalDay === 'string'
    ? dayjs(rowOperationalDay).tz('Europe/Moscow').startOf('day').toDate()
    : dayjs(rowOperationalDay).tz('Europe/Moscow').startOf('day').toDate();

  if (canYesterdayToo && (rowOpDay < yesterdayOpDay || rowOpDay > currentOpDay)) {
    throw new AppError('Доступ запрещен: можно редактировать только текущий и вчерашний операционный день', 403, 'FORBIDDEN');
  }

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

/**
 * Создает новую запись кассы
 */
export async function createPaymasterRow(data: CreatePaymasterRowInput, user: UserInfo): Promise<string> {
  const operationalDay = data.operationalDay 
    ? new Date(data.operationalDay) 
    : getOperationalDay();

  // Проверка прав для создания записей на прошлые даты
  await validateEditPermissions(user, operationalDay);

  const result = await db.insert(paymasterRows).values({
    roomNumber: data.roomNumber,
    contractor: data.contractor,
    stayDates: data.stayDates,
    amount: String(data.amount),
    type: data.type,
    comment: data.comment,
    operationalDay,
    createdAt: new Date(),
    updatedAt: new Date(),
  }).$returningId();

  return result[0].id;
}

/**
 * Обновляет запись кассы
 */
export async function updatePaymasterRow(id: string, data: UpdatePaymasterRowInput, user: UserInfo): Promise<PaymasterRow> {
  // Сначала получаем текущую запись для проверки прав
  const existingRow = await getPaymasterRowById(id);
  if (!existingRow) {
    throw new AppError('Запись не найдена', 404, 'NOT_FOUND');
  }

  // Проверяем права на редактирование
  await validateEditPermissions(user, new Date(existingRow.operationalDay));

  const updateData: any = {
    ...data,
    updatedAt: new Date(),
  };

  // Преобразуем amount в строку если указан
  if (data.amount !== undefined) {
    updateData.amount = String(data.amount);
  }

  // Преобразуем operationalDay в Date если указан
  if (data.operationalDay !== undefined) {
    updateData.operationalDay = new Date(data.operationalDay);
    await validateEditPermissions(user, updateData.operationalDay);
  }

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

  return getPaymasterRowById(id) as Promise<PaymasterRow>;
}

/**
 * Удаляет запись кассы
 */
export async function deletePaymasterRow(id: string, user: UserInfo): Promise<void> {
  // Сначала получаем текущую запись для проверки прав
  const existingRow = await getPaymasterRowById(id);
  if (!existingRow) {
    throw new AppError('Запись не найдена', 404, 'NOT_FOUND');
  }

  // Проверяем права на удаление
  await validateEditPermissions(user, new Date(existingRow.operationalDay));

  await db
    .delete(paymasterRows)
    .where(eq(paymasterRows.id, id));
}

/**
 * Расчитывает итоги по типам операций для указанного операционного дня
 * Возвращает данные в формате, соответствующем контракту @shared/contracts/paymaster
 */
export async function calculateTotalsByDate(date: string): Promise<PaymasterTotals> {
  const rows = await getPaymasterRowsByDate(date);

  const totals = {
    cash: 0,
    card: 0,
    advance: 0,
    expense: 0,
    other: 0,
  };

  const defaultTypes = Object.values(PAYMASTER_TYPES); // ['нал', 'безнал', 'аванс', 'расход']

  for (const row of rows) {
    const amount = Number(row.amount); // Обязательный каст в число (MySQL возвращает строку)
    const type = row.type;

    switch (type) {
      case PAYMASTER_TYPES.CASH:
        totals.cash += amount;
        break;
      case PAYMASTER_TYPES.CARD:
        totals.card += amount;
        break;
      case PAYMASTER_TYPES.ADVANCE:
        totals.advance += amount;
        break;
      case PAYMASTER_TYPES.EXPENSE:
        totals.expense += amount;
        break;
      default:
        // Суммируем все кастомные операции (типы не входящие в дефолтные)
        totals.other += amount;
        break;
    }
  }

  // Возвращаем строгий формат из 5 полей: cash, card, advance, other, expense
  return {
    cash: totals.cash,
    card: totals.card,
    advance: totals.advance,
    expense: totals.expense,
    other: totals.other,
  };
}

/**
 * Расчитывает итоги для текущего операционного дня
 */
export async function calculateCurrentTotals(): Promise<PaymasterTotals> {
  return calculateTotalsByDate(getOperationalDayString());
}

// Legacy functions for backward compatibility
export async function getActivePaymasterRows() {
  return getCurrentPaymasterRows();
}

export async function getArchivedPaymasterRows() {
  // Эта функция больше не нужна, так как у нас нет archivedAt
  // Возвращаем пустой массив для совместимости
  return [];
}

export async function archiveAllPaymasterRows() {
  // Эта функция больше не нужна
  // Ничего не делаем для совместимости
}

/**
 * Получает уникальные значения полей из всей таблицы paymaster_rows для автокомплита
 */
export async function getPaymasterSuggestions() {
  const rows = await db.select({
    roomNumber: paymasterRows.roomNumber,
    contractor: paymasterRows.contractor,
    type: paymasterRows.type,
  }).from(paymasterRows);

  return {
    rooms: [...new Set(rows.map(r => r.roomNumber).filter((v): v is string => Boolean(v)))],
    contractors: [...new Set(rows.map(r => r.contractor).filter((v): v is string => Boolean(v)))],
    types: [...new Set(rows.map(r => r.type).filter((v): v is string => Boolean(v)))],
  };
}

/**
 * Получает агрегированный отчет за период операционных дней
 * @param startDate - Начальная дата периода (YYYY-MM-DD)
 * @param endDate - Конечная дата периода (YYYY-MM-DD)
 * @returns Объект с днями, промежуточными и финальными итогами
 */
export async function getPeriodReport(startDate: string, endDate: string): Promise<PaymasterPeriodReport> {
  // Нормализуем даты в формате YYYY-MM-DD для сравнения с MySQL DATE типом
  const formattedStartDate = dayjs(startDate).tz('Europe/Moscow').format('YYYY-MM-DD');
  const formattedEndDate = dayjs(endDate).tz('Europe/Moscow').format('YYYY-MM-DD');

  // Фильтруем записи по operationalDay в указанном диапазоне
  const results = await db
    .select()
    .from(paymasterRows)
    .where(
      sql`${paymasterRows.operationalDay} BETWEEN ${formattedStartDate} AND ${formattedEndDate}`
    )
    .orderBy(paymasterRows.operationalDay, paymasterRows.createdAt);

  // Map dates to ISO strings for API response
  const rows = results.map(r => ({
    ...r,
    amount: Number(r.amount), // Обязательный каст в число (MySQL возвращает строку)
    type: r.type as PaymasterRow['type'], // Явное приведение типа
    operationalDay: dayjs(r.operationalDay).format('YYYY-MM-DD'),
    createdAt: r.createdAt.toISOString(),
    updatedAt: r.updatedAt.toISOString(),
  }));

  // Группируем записи по операционным дням
  const daysMap = new Map<string, PaymasterRow[]>();
  for (const row of rows) {
    const opDay = row.operationalDay;
    if (!daysMap.has(opDay)) {
      daysMap.set(opDay, []);
    }
    daysMap.get(opDay)!.push(row);
  }

  // Рассчитываем итоги для каждого дня и формируем массив дней
  const days: Array<{ date: string; rows: PaymasterRow[]; totals: PaymasterTotals }> = [];
  const grandTotals = {
    cash: 0,
    card: 0,
    advance: 0,
    expense: 0,
    other: 0,
  };

  const defaultTypes = Object.values(PAYMASTER_TYPES); // ['нал', 'безнал', 'аванс', 'расход']

  // Сортируем даты и обрабатываем каждый день
  const sortedDates = Array.from(daysMap.keys()).sort();
  for (const date of sortedDates) {
    const dayRows = daysMap.get(date)!;
    
    // Рассчитываем итоги для текущего дня
    const dayTotals = {
      cash: 0,
      card: 0,
      advance: 0,
      expense: 0,
      other: 0,
    };

    for (const row of dayRows) {
      const amount = Number(row.amount);
      const type = row.type;

      switch (type) {
        case PAYMASTER_TYPES.CASH:
          dayTotals.cash += amount;
          grandTotals.cash += amount;
          break;
        case PAYMASTER_TYPES.CARD:
          dayTotals.card += amount;
          grandTotals.card += amount;
          break;
        case PAYMASTER_TYPES.ADVANCE:
          dayTotals.advance += amount;
          grandTotals.advance += amount;
          break;
        case PAYMASTER_TYPES.EXPENSE:
          dayTotals.expense += amount;
          grandTotals.expense += amount;
          break;
        default:
          dayTotals.other += amount;
          grandTotals.other += amount;
          break;
      }
    }

    days.push({
      date,
      rows: dayRows,
      totals: dayTotals,
    });
  }

  return {
    days,
    grandTotals,
  };
}
