import { ConsumeInventoryDto } from './dto/consume-inventory.dto';
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreateInventoryDto } from './dto/create-inventory.dto';
import { UpdateInventoryDto } from './dto/update-inventory.dto';
import { convertUnit, normalizeUnit } from '../common/utils/units';

type InventoryQuery = {
  location?: string;
  sort?: string;
};

type AdminInventoryQuery = {
  userId?: number;
  sort?: string;
};

@Injectable()
export class InventoryService {
  constructor(private prisma: PrismaService) {}

  private readonly productWithCategoryInclude = {
    include: {
      categoryRef: {
        include: {
          parent: {
            include: {
              parent: true,
            },
          },
        },
      },
    },
  };

  private throwInventoryItemNotFound(id: number): never {
    throw new NotFoundException(`Inventory item with id ${id} not found`);
  }

  private async findInventoryItemAnyByIdOrThrow(id: number) {
    const existing = await this.prisma.inventoryItem.findUnique({ where: { id } });
    if (!existing) {
      this.throwInventoryItemNotFound(id);
    }
    return existing;
  }

  private async findInventoryItemByIdOrThrow(id: number, userId: number) {
    const existing = await this.prisma.inventoryItem.findUnique({ where: { id } });
    if (!existing) {
      this.throwInventoryItemNotFound(id);
    }
    if (existing.userId !== userId) {
      throw new ForbiddenException(`Inventory item with id ${id} does not belong to current user`);
    }
    return existing;
  }

  private async ensureProductExists(productId: number, userId: number) {
    const product = await this.prisma.product.findFirst({ where: { id: productId, ownerId: userId } });
    if (!product) {
      throw new NotFoundException('Product not found for current user');
    }
    return product;
  }

  private async ensureProductExistsAny(productId: number) {
    const product = await this.prisma.product.findUnique({ where: { id: productId } });
    if (!product) {
      throw new NotFoundException('Product not found');
    }
    return product;
  }

  private async ensureUserExists(userId: number) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true },
    });
    if (!user) {
      throw new NotFoundException(`User with id ${userId} not found`);
    }
  }

  private buildCreateData(userId: number, data: CreateInventoryDto): Prisma.InventoryItemUncheckedCreateInput {
    return {
      ...data,
      userId,
      quantity: new Prisma.Decimal(data.quantity),
      location: data.location?.trim() || undefined,
      brand: data.brand?.trim() || undefined,
      origin: data.origin?.trim() || undefined,
      receiptName: data.receiptName?.trim() || undefined,
      suitableFor: data.suitableFor?.trim() || undefined,
      comment: data.comment?.trim() || undefined,
      purchaseDate: data.purchaseDate
        ? new Date(data.purchaseDate)
        : undefined,
      bestBeforeDate: data.bestBeforeDate
        ? new Date(data.bestBeforeDate)
        : undefined,
    };
  }

  private buildUpdateData(data: UpdateInventoryDto): Prisma.InventoryItemUpdateInput {
    const updateData: Prisma.InventoryItemUpdateInput = {};

    if (typeof data.productId === 'number') {
      updateData.product = {
        connect: { id: data.productId },
      };
    }

    if (typeof data.quantity === 'number') {
      updateData.quantity = new Prisma.Decimal(data.quantity);
    }

    if (typeof data.unit === 'string') {
      updateData.unit = data.unit.trim();
    }

    if (typeof data.location === 'string') {
      updateData.location = data.location.trim();
    }

    if (typeof data.brand === 'string') {
      updateData.brand = data.brand.trim();
    }

    if (typeof data.receiptName === 'string') {
      updateData.receiptName = data.receiptName.trim();
    }

    if (typeof data.purchaseDate === 'string') {
      updateData.purchaseDate = data.purchaseDate
        ? new Date(data.purchaseDate)
        : null;
    }

    if (typeof data.bestBeforeDate === 'string') {
      updateData.bestBeforeDate = data.bestBeforeDate
        ? new Date(data.bestBeforeDate)
        : null;
    }

    if (typeof data.opened === 'boolean') {
      updateData.opened = data.opened;
    }

    if (typeof data.suitableFor === 'string') {
      updateData.suitableFor = data.suitableFor.trim();
    }

    if (typeof data.comment === 'string') {
      updateData.comment = data.comment.trim();
    }

    return updateData;
  }

  async findAll(userId: number, query?: InventoryQuery) {
    const where: Prisma.InventoryItemWhereInput = { userId };
    const orderBy: Prisma.InventoryItemOrderByWithRelationInput[] = [];

    if (query?.location) {
      where.location = query.location;
    }

    if (query?.sort === 'bestBeforeAsc') {
      orderBy.push({ bestBeforeDate: 'asc' });
    } else if (query?.sort === 'bestBeforeDesc') {
      orderBy.push({ bestBeforeDate: 'desc' });
    } else if (query?.sort === 'nameAsc') {
      orderBy.push({ product: { name: 'asc' } } as any);
    } else if (query?.sort === 'purchaseDateAsc') {
      orderBy.push({ purchaseDate: 'asc' });
    } else if (query?.sort === 'purchaseDateDesc') {
      orderBy.push({ purchaseDate: 'desc' });
    } else {
      orderBy.push({ createdAt: 'desc' });
    }

    return this.prisma.inventoryItem.findMany({
      where,
      include: {
        product: this.productWithCategoryInclude,
      },
      orderBy,
    });
  }

  async findAllAdmin(query?: AdminInventoryQuery) {
    const where: Prisma.InventoryItemWhereInput = {};
    const orderBy: Prisma.InventoryItemOrderByWithRelationInput[] = [];

    if (typeof query?.userId === 'number' && Number.isFinite(query.userId)) {
      where.userId = query.userId;
    }

    if (query?.sort === 'nameAsc') {
      orderBy.push({ product: { name: 'asc' } } as any);
    } else if (query?.sort === 'nameDesc') {
      orderBy.push({ product: { name: 'desc' } } as any);
    } else if (query?.sort === 'quantityDesc') {
      orderBy.push({ quantity: 'desc' });
    } else if (query?.sort === 'quantityAsc') {
      orderBy.push({ quantity: 'asc' });
    } else {
      orderBy.push({ createdAt: 'desc' });
    }

    return this.prisma.inventoryItem.findMany({
      where,
      include: {
        user: {
          select: {
            id: true,
            username: true,
            email: true,
          },
        },
        product: this.productWithCategoryInclude,
      },
      orderBy,
    });
  }
  
  async consume(id: number, userId: number, data: ConsumeInventoryDto) {
  const existing = await this.findInventoryItemByIdOrThrow(id, userId);

  const currentQuantity = Number(existing.quantity);
  const newQuantity = Math.max(0, currentQuantity - data.amountUsed);

  return this.prisma.$transaction(async (tx) => {
    const updatedItem = await tx.inventoryItem.update({
      where: { id },
      data: {
        quantity: new Prisma.Decimal(newQuantity),
      },
      include: {
          product: this.productWithCategoryInclude,
      },
    });

    await tx.inventoryConsumption.create({
      data: {
        inventoryItemId: id,
        amountUsed: new Prisma.Decimal(data.amountUsed),
        comment: data.comment?.trim() || null,
      },
    });

    return updatedItem;
  });
}

  async findConsumptionHistory(id: number, userId: number) {
    await this.findInventoryItemByIdOrThrow(id, userId);

  return this.prisma.inventoryConsumption.findMany({
    where: {
      inventoryItemId: id,
    },
    select: {
      id: true,
      inventoryItemId: true,
      amountUsed: true,
      comment: true,
      createdAt: true,
      inventoryItem: {
        select: { unit: true },
      },
    },
    orderBy: {
      createdAt: 'desc',
    },
  });
}
  async findExpiring(userId: number) {
    const now = new Date();

    return this.prisma.inventoryItem.findMany({
      where: {
        userId,
        bestBeforeDate: {
          not: null,
          gte: now,
        },
      },
      include: {
        product: true,
      },
      orderBy: [{ bestBeforeDate: 'asc' }, { createdAt: 'desc' }],
    });
  }

  async create(userId: number, data: CreateInventoryDto) {
    await this.ensureProductExists(data.productId, userId);

  return this.prisma.inventoryItem.create({
    data: this.buildCreateData(userId, data),
    include: {
      product: this.productWithCategoryInclude,
    },
  });
}

  async createAdmin(adminUserId: number, data: CreateInventoryDto, targetUserId?: number) {
    const effectiveUserId = typeof targetUserId === 'number' ? targetUserId : adminUserId;
    await this.ensureUserExists(effectiveUserId);
    await this.ensureProductExistsAny(data.productId);

    return this.prisma.inventoryItem.create({
      data: this.buildCreateData(effectiveUserId, data),
      include: {
        user: {
          select: {
            id: true,
            username: true,
            email: true,
          },
        },
        product: this.productWithCategoryInclude,
      },
    });
  }

  async update(id: number, userId: number, data: UpdateInventoryDto) {
    await this.findInventoryItemByIdOrThrow(id, userId);

    if (typeof data.productId === 'number') {
      await this.ensureProductExists(data.productId, userId);
    }

    const updateData = this.buildUpdateData(data);

    return this.prisma.inventoryItem.update({
      where: { id },
      data: updateData,
      include: {
        product: this.productWithCategoryInclude,
      },
    });
  }

  async remove(id: number, userId: number) {
    await this.findInventoryItemByIdOrThrow(id, userId);
    return this.prisma.inventoryItem.delete({ where: { id } });
  }

  async bulkDelete(userId: number, ids: number[]) {
    const uniqueIds = [...new Set(ids)];
    if (uniqueIds.length === 0) {
      throw new BadRequestException('No inventory ids supplied');
    }

    const items = await this.prisma.inventoryItem.findMany({
      where: { id: { in: uniqueIds }, userId },
      select: { id: true },
    });

    if (items.length !== uniqueIds.length) {
      throw new ForbiddenException('One or more inventory items are missing or do not belong to current user');
    }

    const result = await this.prisma.inventoryItem.deleteMany({
      where: { id: { in: uniqueIds }, userId },
    });

    return { deletedCount: result.count };
  }

  async mergeMany(userId: number, ids: number[], targetUnit?: string) {
    const uniqueIds = [...new Set(ids)];
    if (uniqueIds.length < 2) {
      throw new BadRequestException('At least two inventory items are required to merge');
    }

    const items = await this.prisma.inventoryItem.findMany({
      where: { id: { in: uniqueIds }, userId },
      include: {
        product: this.productWithCategoryInclude,
      },
      orderBy: { createdAt: 'asc' },
    });

    if (items.length !== uniqueIds.length) {
      throw new ForbiddenException('One or more inventory items are missing or do not belong to current user');
    }

    const firstProductId = items[0].productId;
    if (items.some((item) => item.productId !== firstProductId)) {
      throw new BadRequestException('Selected inventory items must belong to the same product');
    }

    const normalizedUnits = new Set(items.map((item) => normalizeUnit(item.unit)));
    const resolvedTargetUnitRaw = targetUnit?.trim();

    if (!resolvedTargetUnitRaw && normalizedUnits.size > 1) {
      throw new BadRequestException('targetUnit is required when merging different units');
    }

    const resolvedTargetUnit = normalizeUnit(
      resolvedTargetUnitRaw && resolvedTargetUnitRaw.length > 0
        ? resolvedTargetUnitRaw
        : items[0].unit,
    );

    if (!items.some((item) => normalizeUnit(item.unit) === resolvedTargetUnit)) {
      throw new BadRequestException('targetUnit must match one of the selected item units');
    }

    let mergedQuantity = 0;
    for (const item of items) {
      const quantity = Number(item.quantity);
      try {
        mergedQuantity += convertUnit(quantity, item.unit, resolvedTargetUnit);
      } catch {
        throw new BadRequestException(
          `Cannot merge item ${item.id}: incompatible unit "${item.unit}" for target unit "${resolvedTargetUnit}"`,
        );
      }
    }

    const target =
      items.find((item) => normalizeUnit(item.unit) === resolvedTargetUnit) ??
      items[0];
    const sourceItems = items.filter((item) => item.id !== target.id);

    const firstNonNull = <T>(values: (T | null | undefined)[]): T | null => {
      for (const value of values) {
        if (value !== null && value !== undefined) {
          return value;
        }
      }
      return null;
    };

    return this.prisma.$transaction(async (tx) => {
      const updated = await tx.inventoryItem.update({
        where: { id: target.id },
        data: {
          quantity: new Prisma.Decimal(mergedQuantity),
          unit: resolvedTargetUnit,
          location: target.location ?? firstNonNull(sourceItems.map((s) => s.location)),
          brand: target.brand ?? firstNonNull(sourceItems.map((s) => s.brand)),
          origin: target.origin ?? firstNonNull(sourceItems.map((s) => s.origin)),
          receiptName: target.receiptName ?? firstNonNull(sourceItems.map((s) => s.receiptName)),
          purchaseDate: target.purchaseDate ?? firstNonNull(sourceItems.map((s) => s.purchaseDate)),
          opened: target.opened,
          suitableFor: target.suitableFor ?? firstNonNull(sourceItems.map((s) => s.suitableFor)),
          bestBeforeDate: target.bestBeforeDate ?? firstNonNull(sourceItems.map((s) => s.bestBeforeDate)),
          comment: target.comment ?? firstNonNull(sourceItems.map((s) => s.comment)),
        },
        include: {
          product: this.productWithCategoryInclude,
        },
      });

      const sourceIds = sourceItems.map((item) => item.id);

      if (sourceIds.length > 0) {
        await tx.inventoryConsumption.updateMany({
          where: { inventoryItemId: { in: sourceIds } },
          data: { inventoryItemId: target.id },
        });

        await tx.inventoryItem.deleteMany({
          where: { id: { in: sourceIds }, userId },
        });
      }

      return updated;
    });
  }

  private async moveInventoryItemToPantryCore(item: {
    id: number;
    userId: number;
    productId: number;
    location: string | null;
  }) {
    const existingPantryItem = await this.prisma.pantryItem.findUnique({
      where: {
        userId_productId: {
          userId: item.userId,
          productId: item.productId,
        },
      },
    });

    if (existingPantryItem) {
      throw new BadRequestException('Produkten finns redan i baslagret');
    }

    return this.prisma.$transaction(async (tx) => {
      const pantryItem = await tx.pantryItem.create({
        data: {
          userId: item.userId,
          productId: item.productId,
          location: item.location?.trim() || null,
        },
        include: { product: true },
      });

      await tx.inventoryItem.delete({ where: { id: item.id } });

      return pantryItem;
    });
  }

  async moveToPantry(id: number, userId: number) {
    const item = await this.findInventoryItemByIdOrThrow(id, userId);
    return this.moveInventoryItemToPantryCore(item);
  }

  async moveToPantryAdmin(id: number) {
    const item = await this.findInventoryItemAnyByIdOrThrow(id);
    return this.moveInventoryItemToPantryCore(item);
  }

  async updateAdmin(id: number, data: UpdateInventoryDto) {
    await this.findInventoryItemAnyByIdOrThrow(id);

    if (typeof data.productId === 'number') {
      await this.ensureProductExistsAny(data.productId);
    }

    const updateData = this.buildUpdateData(data);

    return this.prisma.inventoryItem.update({
      where: { id },
      data: updateData,
      include: {
        user: {
          select: {
            id: true,
            username: true,
            email: true,
          },
        },
        product: this.productWithCategoryInclude,
      },
    });
  }

  async removeAdmin(id: number) {
    await this.findInventoryItemAnyByIdOrThrow(id);
    return this.prisma.inventoryItem.delete({ where: { id } });
  }

  private validateAdminMergeEligibility(
    source: { id: number; userId: number; productId: number; unit: string },
    target: { id: number; userId: number; productId: number; unit: string },
  ): string | null {
    if (source.userId !== target.userId) {
      return 'Cannot merge inventory items from different users';
    }

    if (source.productId !== target.productId) {
      return 'Cannot merge inventory items with different products';
    }

    if (source.unit.trim().toLowerCase() !== target.unit.trim().toLowerCase()) {
      return 'Cannot merge inventory items with different units';
    }

    return null;
  }

  async previewMergeAdmin(sourceInventoryId: number, targetInventoryId: number) {
    if (sourceInventoryId === targetInventoryId) {
      return {
        canMerge: false,
        reason: 'sourceInventoryId and targetInventoryId cannot be the same',
      };
    }

    const [source, target] = await Promise.all([
      this.findInventoryItemAnyByIdOrThrow(sourceInventoryId),
      this.findInventoryItemAnyByIdOrThrow(targetInventoryId),
    ]);

    const reason = this.validateAdminMergeEligibility(source, target);
    const mergedQuantity = Number(source.quantity) + Number(target.quantity);

    return {
      canMerge: reason == null,
      reason,
      source: {
        id: source.id,
        userId: source.userId,
        productId: source.productId,
        quantity: Number(source.quantity),
        unit: source.unit,
      },
      target: {
        id: target.id,
        userId: target.userId,
        productId: target.productId,
        quantity: Number(target.quantity),
        unit: target.unit,
      },
      outcome: {
        mergedQuantity,
        mergedUnit: target.unit,
      },
    };
  }

  async mergeAdmin(sourceInventoryId: number, targetInventoryId: number) {
    if (sourceInventoryId === targetInventoryId) {
      throw new BadRequestException('sourceInventoryId and targetInventoryId cannot be the same');
    }

    const [source, target] = await Promise.all([
      this.findInventoryItemAnyByIdOrThrow(sourceInventoryId),
      this.findInventoryItemAnyByIdOrThrow(targetInventoryId),
    ]);

    const reason = this.validateAdminMergeEligibility(source, target);
    if (reason) {
      throw new BadRequestException(reason);
    }

    const mergedQuantity = Number(source.quantity) + Number(target.quantity);

    return this.prisma.$transaction(async (tx) => {
      const updated = await tx.inventoryItem.update({
        where: { id: target.id },
        data: {
          quantity: new Prisma.Decimal(mergedQuantity),
          location: target.location ?? source.location,
          brand: target.brand ?? source.brand,
          origin: target.origin ?? source.origin,
          receiptName: target.receiptName ?? source.receiptName,
          purchaseDate: target.purchaseDate ?? source.purchaseDate,
          opened: target.opened ?? source.opened,
          suitableFor: target.suitableFor ?? source.suitableFor,
          bestBeforeDate: target.bestBeforeDate ?? source.bestBeforeDate,
          comment: target.comment ?? source.comment,
        },
        include: {
          user: {
            select: {
              id: true,
              username: true,
              email: true,
            },
          },
          product: this.productWithCategoryInclude,
        },
      });

      await tx.inventoryConsumption.updateMany({
        where: { inventoryItemId: source.id },
        data: { inventoryItemId: target.id },
      });

      await tx.inventoryItem.delete({ where: { id: source.id } });
      return updated;
    });
  }
}