// src/server/features/reference_books/inventory.service.ts
// Business logic for inventory management with approval workflow

import { db } from '@serverShared/db/client';
import { inventoryTable, inventoryPendingTable } from './db/inventory.table';
import { employees } from '../personnel/db/employees.table';
import { eq, or, isNull, isNotNull, inArray } from 'drizzle-orm';
import { mapToInventoryEntry, mapToInventoryPendingEntry, mapToInventoryEntryWithMetadataStrict } from './lib/inventory.mapper';
import { createDraftInventory, createUpdateRequestInventory, updatePendingInventory, deleteDraftInventory, createDeleteRequestInventory } from './lib/inventory.drafts';
import { executeCreateInventory, executeUpdateInventory, executeDeleteInventory, executeApproveInventory, executeRejectInventory } from './lib/inventory.core';
import type { InventoryPendingEntry, InventoryEntryWithMetadata } from './inventory.schema';
import type { InventoryCreateRequest, InventoryUpdateRequest } from '@shared/contracts/inventory';
import type { UserInfo } from '@serverShared/lib/auth';
import { AppPermission } from '@shared/contracts/permissions';
import { hasPermission } from '@serverShared/plugins/rbac';
import { AppError } from '@serverShared/lib/errors';

type InventoryPendingEntryType = typeof inventoryPendingTable.$inferSelect;

interface InventoryAccess {
  canEditOwn: boolean;
  canEditOthers: boolean;
  requiresApproval: boolean;
}

async function getInventoryAccess(user: UserInfo): Promise<InventoryAccess> {
  const [canEditOwn, canEditOthers, requiresApproval] = await Promise.all([
    hasPermission(user, AppPermission.INVENTORY_EDIT_OWN),
    hasPermission(user, AppPermission.INVENTORY_EDIT_OTHERS),
    hasPermission(user, AppPermission.INVENTORY_REQUEST_APPROVAL),
  ]);
  return { canEditOwn, canEditOthers, requiresApproval };
}

function canEditByOwner(access: InventoryAccess, userId: string, ownerId: string | null): boolean {
  return ownerId !== null && ownerId === userId ? access.canEditOwn : access.canEditOthers;
}

function assertCanEditByOwner(access: InventoryAccess, userId: string, ownerId: string | null): void {
  if (!canEditByOwner(access, userId, ownerId)) {
    throw new AppError('У вас нет прав на изменение этой записи инвентаря', 403, 'FORBIDDEN');
  }
}

function throwApprovalConflict(error: unknown): never {
  if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ER_DUP_ENTRY') {
    throw new AppError('Запись уже находится на согласовании', 409, 'INVENTORY_APPROVAL_PENDING');
  }
  throw error;
}

async function findPendingByIdOrTarget(id: string): Promise<InventoryPendingEntryType | null> {
  const [pending] = await db.select()
    .from(inventoryPendingTable)
    .where(or(eq(inventoryPendingTable.id, id), eq(inventoryPendingTable.targetId, id)))
    .limit(1);
  return pending || null;
}

/**
 * Создать запись в списке инвентаря
 */
export async function createInventoryEntry(request: InventoryCreateRequest, user: UserInfo): Promise<InventoryEntryWithMetadata> {
  const access = await getInventoryAccess(user);
  if (!access.canEditOwn) {
    throw new AppError('У вас нет прав на создание записей инвентаря', 403, 'FORBIDDEN');
  }

  if (access.requiresApproval) {
    const draft = await createDraftInventory(request, user.id);
    return mapToInventoryPendingEntry(draft);
  }

  const created = await executeCreateInventory(request, user.id);
  return mapToInventoryEntryWithMetadataStrict(created);
}

/**
 * Обновить запись в списке инвентаря
 */
export async function updateInventoryEntry(
  id: string,
  request: Omit<InventoryUpdateRequest, 'id'>,
  user: UserInfo
): Promise<InventoryEntryWithMetadata> {
  const access = await getInventoryAccess(user);
  const pending = await findPendingByIdOrTarget(id);

  if (pending) {
    if (pending.createdBy !== user.id) {
      throw new AppError('Запись уже находится на согласовании', 409, 'INVENTORY_APPROVAL_PENDING');
    }

    if (pending.targetId) {
      const [original] = await db.select().from(inventoryTable).where(eq(inventoryTable.id, pending.targetId)).limit(1);
      if (!original) throw new AppError('Исходная запись инвентаря не найдена', 404, 'INVENTORY_NOT_FOUND');
      assertCanEditByOwner(access, user.id, original.createdBy);
    } else if (!access.canEditOwn) {
      throw new AppError('У вас нет прав на изменение этой заявки', 403, 'FORBIDDEN');
    }

    const updatedPending = await updatePendingInventory(pending.id, request);
    return mapToInventoryPendingEntry(updatedPending);
  }

  const [existing] = await db.select().from(inventoryTable).where(eq(inventoryTable.id, id)).limit(1);
  if (!existing) throw new AppError('Запись инвентаря не найдена', 404, 'INVENTORY_NOT_FOUND');
  assertCanEditByOwner(access, user.id, existing.createdBy);

  if (!access.requiresApproval) {
    const result = await executeUpdateInventory(id, request);
    return mapToInventoryEntryWithMetadataStrict(result);
  }

  try {
    const result = await createUpdateRequestInventory(id, request, existing, user.id);
    return mapToInventoryPendingEntry(result);
  } catch (error) {
    return throwApprovalConflict(error);
  }
}

/**
 * Удалить запись из списка инвентаря (soft delete)
 */
export async function deleteInventoryEntry(id: string, user: UserInfo): Promise<void> {
  const access = await getInventoryAccess(user);
  const [pendingById] = await db.select().from(inventoryPendingTable).where(eq(inventoryPendingTable.id, id)).limit(1);
  if (pendingById) {
    if (pendingById.createdBy !== user.id) {
      throw new AppError('Можно удалить только собственную заявку инвентаря', 403, 'FORBIDDEN');
    }
    await deleteDraftInventory(id);
    return;
  }

  const [existing] = await db.select().from(inventoryTable).where(eq(inventoryTable.id, id)).limit(1);
  if (!existing) throw new AppError('Запись инвентаря не найдена', 404, 'INVENTORY_NOT_FOUND');
  assertCanEditByOwner(access, user.id, existing.createdBy);

  const pending = await findPendingByIdOrTarget(id);
  if (pending) throw new AppError('Запись уже находится на согласовании', 409, 'INVENTORY_APPROVAL_PENDING');

  if (!access.requiresApproval) {
    await executeDeleteInventory(id);
    return;
  }
  try {
    await createDeleteRequestInventory(existing, user.id);
  } catch (error) {
    throwApprovalConflict(error);
  }
}

/**
 * Получить все записи инвентаря
 * @param archived - если false, возвращает только активные записи; если true, возвращает только архивные; если undefined, возвращает только активные записи
 * @param status - 'all' (все записи), 'pending' (только на согласовании), 'approved' (только одобренные), undefined (только одобренные)
 */
export async function getAllInventoryEntries(archived?: boolean, status?: 'all' | 'pending' | 'approved'): Promise<InventoryEntryWithMetadata[]> {
  // Если запрашиваем только pending записи - используем отдельную функцию
  if (status === 'pending') {
    const pendingEntries = await getInventoryPendingEntries();
    // Преобразуем InventoryPendingEntry в InventoryEntryWithMetadata
    return pendingEntries.map(entry => ({
      id: entry.id,
      name: entry.name,
      category: entry.category,
      quantity: entry.quantity,
      unit: entry.unit,
      location: entry.location,
      comment: entry.comment,
      weightPerUnit: entry.weightPerUnit,
      manufacturer: entry.manufacturer,
      model: entry.model,
      serialNumber: entry.serialNumber,
      orderingLink: entry.orderingLink,
      articleNumber: entry.articleNumber,
      isApproved: false,
      status: entry.status,
      targetId: entry.targetId,
      createdBy: entry.createdBy,
      creatorName: entry.creatorName,
      reviewedBy: entry.reviewedBy,
      reviewedAt: entry.reviewedAt,
      rejectionReason: entry.rejectionReason,
      createdAt: entry.createdAt,
      updatedAt: entry.updatedAt,
      archivedAt: null, // Pending записи не архивируются
    }));
  }

  // Формируем условие для фильтрации по archivedAt
  const archivedCondition = archived === true
    ? isNotNull(inventoryTable.archivedAt) // archivedAt IS NOT NULL
    : isNull(inventoryTable.archivedAt); // archivedAt IS NULL (по умолчанию)

  // Получаем одобренные записи из основной таблицы
  const whereCondition = archivedCondition;

  const approvedEntries = await db.select()
    .from(inventoryTable)
    .where(whereCondition)
    .orderBy(inventoryTable.createdAt);

  const creatorIds = [...new Set(approvedEntries.map((entry) => entry.createdBy).filter((id): id is string => Boolean(id)))];
  const creators = creatorIds.length > 0
    ? await db.select({ id: employees.id, fullName: employees.fullName }).from(employees).where(inArray(employees.id, creatorIds))
    : [];
  const creatorNames = new Map(creators.map((creator) => [creator.id, creator.fullName]));
  const mappedApprovedEntries = approvedEntries.map((entry) => mapToInventoryEntry({
    ...entry,
    creatorName: entry.createdBy ? creatorNames.get(entry.createdBy) || null : null,
  }));

  // Если запрашиваем все записи (all), добавляем pending записи
  if (status === 'all') {
    const pendingEntries = await getInventoryPendingEntries();
    const mappedPendingEntries = pendingEntries.map(entry => ({
      id: entry.id,
      name: entry.name,
      category: entry.category,
      quantity: entry.quantity,
      unit: entry.unit,
      location: entry.location,
      comment: entry.comment,
      weightPerUnit: entry.weightPerUnit,
      manufacturer: entry.manufacturer,
      model: entry.model,
      serialNumber: entry.serialNumber,
      orderingLink: entry.orderingLink,
      articleNumber: entry.articleNumber,
      isApproved: false,
      status: entry.status,
      targetId: entry.targetId,
      createdBy: entry.createdBy,
      creatorName: entry.creatorName,
      reviewedBy: entry.reviewedBy,
      reviewedAt: entry.reviewedAt,
      rejectionReason: entry.rejectionReason,
      createdAt: entry.createdAt,
      updatedAt: entry.updatedAt,
      archivedAt: null, // Pending записи не архивируются
    }));

    // Объединяем и сортируем по createdAt
    return [...mappedPendingEntries, ...mappedApprovedEntries]
      .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
  }

  // Если запрашиваем только одобренные (approved/undefined)
  return mappedApprovedEntries;
}

/**
 * Получить запись по ID
 */
export async function getInventoryEntryById(id: string): Promise<InventoryEntryWithMetadata | null> {
  const entry = await db.select()
    .from(inventoryTable)
    .where(eq(inventoryTable.id, id))
    .limit(1);

  if (!entry[0]) {
    return null;
  }

  let creatorName: string | null = null;
  if (entry[0].createdBy) {
    const [creator] = await db.select({ fullName: employees.fullName })
      .from(employees)
      .where(eq(employees.id, entry[0].createdBy))
      .limit(1);
    creatorName = creator?.fullName || null;
  }
  return mapToInventoryEntry({ ...entry[0], creatorName });
}

/**
 * Получить все записи из таблицы inventory_pending со статусом pending или to_delete
 */
export async function getInventoryPendingEntries(): Promise<InventoryPendingEntry[]> {
  const entries = await db.select({
    id: inventoryPendingTable.id,
    name: inventoryPendingTable.name,
    category: inventoryPendingTable.category,
    quantity: inventoryPendingTable.quantity,
    unit: inventoryPendingTable.unit,
    location: inventoryPendingTable.location,
    comment: inventoryPendingTable.comment,
    weightPerUnit: inventoryPendingTable.weightPerUnit,
    manufacturer: inventoryPendingTable.manufacturer,
    model: inventoryPendingTable.model,
    serialNumber: inventoryPendingTable.serialNumber,
    orderingLink: inventoryPendingTable.orderingLink,
    articleNumber: inventoryPendingTable.articleNumber,
    status: inventoryPendingTable.status,
    targetId: inventoryPendingTable.targetId,
    createdBy: inventoryPendingTable.createdBy,
    reviewedBy: inventoryPendingTable.reviewedBy,
    reviewedAt: inventoryPendingTable.reviewedAt,
    rejectionReason: inventoryPendingTable.rejectionReason,
    createdAt: inventoryPendingTable.createdAt,
    updatedAt: inventoryPendingTable.updatedAt,
    creatorName: employees.fullName,
  })
    .from(inventoryPendingTable)
    .leftJoin(employees, eq(inventoryPendingTable.createdBy, employees.id))
    .where(or(
      eq(inventoryPendingTable.status, 'pending'),
      eq(inventoryPendingTable.status, 'to_delete')
    ))
    .orderBy(inventoryPendingTable.createdAt);

  return entries.map(mapToInventoryPendingEntry);
}

/**
 * Получить запись на согласовании по ID
 */
export async function getInventoryPendingEntryById(id: string): Promise<InventoryPendingEntry | null> {
  const entry = await db.select({
    id: inventoryPendingTable.id,
    name: inventoryPendingTable.name,
    category: inventoryPendingTable.category,
    quantity: inventoryPendingTable.quantity,
    unit: inventoryPendingTable.unit,
    location: inventoryPendingTable.location,
    comment: inventoryPendingTable.comment,
    weightPerUnit: inventoryPendingTable.weightPerUnit,
    manufacturer: inventoryPendingTable.manufacturer,
    model: inventoryPendingTable.model,
    serialNumber: inventoryPendingTable.serialNumber,
    orderingLink: inventoryPendingTable.orderingLink,
    articleNumber: inventoryPendingTable.articleNumber,
    status: inventoryPendingTable.status,
    targetId: inventoryPendingTable.targetId,
    createdBy: inventoryPendingTable.createdBy,
    reviewedBy: inventoryPendingTable.reviewedBy,
    reviewedAt: inventoryPendingTable.reviewedAt,
    rejectionReason: inventoryPendingTable.rejectionReason,
    createdAt: inventoryPendingTable.createdAt,
    updatedAt: inventoryPendingTable.updatedAt,
    creatorName: employees.fullName,
  })
    .from(inventoryPendingTable)
    .leftJoin(employees, eq(inventoryPendingTable.createdBy, employees.id))
    .where(eq(inventoryPendingTable.id, id))
    .limit(1);

  return entry[0] ? mapToInventoryPendingEntry(entry[0]) : null;
}

/**
 * Approve inventory entry (Admin only)
 */
export async function approveInventoryEntry(id: string, user: UserInfo): Promise<InventoryEntryWithMetadata | null> {
  const canApprove = await hasPermission(user, AppPermission.INVENTORY_APPROVE);

  if (!canApprove) {
    throw new AppError('У вас нет прав на согласование записей инвентаря', 403, 'FORBIDDEN');
  }

  const result = await executeApproveInventory(id, user.id);
  return result ? mapToInventoryEntryWithMetadataStrict(result) : null;
}

/**
 * Reject inventory entry (Admin or Manager - Manager can only reject their own)
 */
export async function rejectInventoryEntry(id: string, reason: string, user: UserInfo): Promise<void> {
  // 1. Find the pending entry (check both ID and TargetID)
  const pendingEntries = await db.select()
    .from(inventoryPendingTable)
    .where(or(
      eq(inventoryPendingTable.id, id),
      eq(inventoryPendingTable.targetId, id)
    ))
    .limit(1);

  if (!pendingEntries[0]) {
    throw new Error('Pending entry not found');
  }

  const entry = pendingEntries[0];
  const canApprove = await hasPermission(user, AppPermission.INVENTORY_APPROVE);

  // 2. Permission Check
  // Пользователь с EDIT может отклонять любую запись. Иначе — только свои.
  if (!canApprove && entry.createdBy !== user.id) {
    throw new AppError('У вас нет прав на отмену этого запроса', 403, 'FORBIDDEN');
  }

  // 3. Execute
  await executeRejectInventory(id, reason, user.id);
}

// Service object for routes
export const inventoryService = {
  createInventoryEntry,
  updateInventoryEntry,
  deleteInventoryEntry,
  getAllInventoryEntries,
  getInventoryEntryById,
  getInventoryPendingEntries,
  approveInventoryEntry,
  rejectInventoryEntry,
};
