import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreateFlyerSelectionDto } from './dto/create-flyer-selection.dto';
import {
  ReceiptMatchCommitResponse,
  ReceiptMatchPreviewResponse,
} from './dto/receipt-match.response';
import { ReceiptMatchDto } from './dto/receipt-match.dto';
import { FlyerSelectionResponse } from './dto/flyer-selection.response';
import { UpdateFlyerSelectionDto } from './dto/update-flyer-selection.dto';
import {
  CandidateSelection,
  FlyerSelectionMatcherService,
} from './flyer-selection-matcher.service';
import { ShoppingListService } from '../shopping-list/shopping-list.service';

@Injectable()
export class FlyerSelectionService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly matcher: FlyerSelectionMatcherService,
    private readonly shoppingListService: ShoppingListService,
  ) {}

  async listBySession(sessionId: number, userId: number): Promise<FlyerSelectionResponse[]> {
    await this.assertSessionOwnership(sessionId, userId);
    const rows = await this.prisma.flyerSelection.findMany({
      where: { sessionId, userId },
      include: {
        item: {
          select: {
            id: true,
            rawName: true,
            normalizedName: true,
            price: true,
            priceUnit: true,
            matchedProductId: true,
            matchedProductName: true,
          },
        },
      },
      orderBy: { createdAt: 'desc' },
    });

    return rows.map((row) => this.toResponse(row));
  }

  async create(
    sessionId: number,
    userId: number,
    dto: CreateFlyerSelectionDto,
  ): Promise<FlyerSelectionResponse> {
    await this.assertSessionOwnership(sessionId, userId);

    const item = await this.prisma.flyerItem.findUnique({
      where: { id: dto.itemId },
      select: { id: true, sessionId: true },
    });
    if (!item || item.sessionId !== sessionId) {
      throw new BadRequestException('Vald flyer-rad tillhör inte sessionen.');
    }

    const created = await this.prisma.flyerSelection.upsert({
      where: {
        sessionId_itemId: {
          sessionId,
          itemId: dto.itemId,
        },
      },
      update: {
        plannedQuantity:
          dto.plannedQuantity == null ? undefined : new Prisma.Decimal(dto.plannedQuantity),
        plannedUnit: dto.plannedUnit,
        priority: dto.priority ?? 'normal',
        note: dto.note,
      },
      create: {
        sessionId,
        itemId: dto.itemId,
        userId,
        plannedQuantity:
          dto.plannedQuantity == null ? null : new Prisma.Decimal(dto.plannedQuantity),
        plannedUnit: dto.plannedUnit ?? null,
        priority: dto.priority ?? 'normal',
        note: dto.note ?? null,
      },
      include: {
        item: {
          select: {
            id: true,
            rawName: true,
            normalizedName: true,
            price: true,
            priceUnit: true,
            matchedProductId: true,
            matchedProductName: true,
          },
        },
      },
    });

    return this.toResponse(created);
  }

  async createMany(
    sessionId: number,
    userId: number,
    items: CreateFlyerSelectionDto[],
  ): Promise<FlyerSelectionResponse[]> {
    if (items.length === 0) return [];

    await this.assertSessionOwnership(sessionId, userId);

    const existingItems = await this.prisma.flyerItem.findMany({
      where: {
        sessionId,
        id: {
          in: items.map((item) => item.itemId),
        },
      },
      select: { id: true },
    });
    const validItemIds = new Set(existingItems.map((item) => item.id));
    const invalidItem = items.find((item) => !validItemIds.has(item.itemId));
    if (invalidItem) {
      throw new BadRequestException(`Flyer-rad ${invalidItem.itemId} tillhör inte sessionen.`);
    }

    const existingSelections = await this.prisma.flyerSelection.findMany({
      where: {
        sessionId,
        itemId: { in: items.map((item) => item.itemId) },
      },
      select: { itemId: true },
    });
    const existingItemIds = new Set(existingSelections.map((item) => item.itemId));

    await this.prisma.$transaction(async (tx) => {
      const toCreate = items.filter((item) => !existingItemIds.has(item.itemId));
      if (toCreate.length > 0) {
        await tx.flyerSelection.createMany({
          data: toCreate.map((item) => ({
            sessionId,
            itemId: item.itemId,
            userId,
            plannedQuantity:
              item.plannedQuantity == null ? null : new Prisma.Decimal(item.plannedQuantity),
            plannedUnit: item.plannedUnit ?? null,
            priority: item.priority ?? 'normal',
            note: item.note ?? null,
          })),
        });
      }

      const toUpdate = items.filter((item) => existingItemIds.has(item.itemId));
      await Promise.all(
        toUpdate.map((item) =>
          tx.flyerSelection.update({
            where: {
              sessionId_itemId: {
                sessionId,
                itemId: item.itemId,
              },
            },
            data: {
              plannedQuantity:
                item.plannedQuantity == null ? undefined : new Prisma.Decimal(item.plannedQuantity),
              plannedUnit: item.plannedUnit,
              priority: item.priority ?? 'normal',
              note: item.note,
            },
          }),
        ),
      );
    });

    const result = await this.prisma.flyerSelection.findMany({
      where: {
        sessionId,
        userId,
        itemId: { in: items.map((item) => item.itemId) },
      },
      include: {
        item: {
          select: {
            id: true,
            rawName: true,
            normalizedName: true,
            price: true,
            priceUnit: true,
            matchedProductId: true,
            matchedProductName: true,
          },
        },
      },
      orderBy: { createdAt: 'desc' },
    });

    return result.map((row) => this.toResponse(row));
  }

  async update(
    sessionId: number,
    selectionId: number,
    userId: number,
    dto: UpdateFlyerSelectionDto,
  ): Promise<FlyerSelectionResponse> {
    await this.assertSessionOwnership(sessionId, userId);

    const existing = await this.prisma.flyerSelection.findUnique({
      where: { id: selectionId },
      select: { id: true, sessionId: true, userId: true },
    });
    if (!existing || existing.sessionId !== sessionId) {
      throw new NotFoundException('FlyerSelection hittades inte.');
    }
    if (existing.userId !== userId) {
      throw new ForbiddenException('Du saknar åtkomst till denna selection.');
    }

    const updated = await this.prisma.flyerSelection.update({
      where: { id: selectionId },
      data: {
        plannedQuantity:
          dto.plannedQuantity == null ? undefined : new Prisma.Decimal(dto.plannedQuantity),
        plannedUnit: dto.plannedUnit,
        priority: dto.priority,
        status: dto.status,
        note: dto.note,
      },
      include: {
        item: {
          select: {
            id: true,
            rawName: true,
            normalizedName: true,
            price: true,
            priceUnit: true,
            matchedProductId: true,
            matchedProductName: true,
          },
        },
      },
    });

    return this.toResponse(updated);
  }

  async remove(sessionId: number, selectionId: number, userId: number): Promise<void> {
    await this.assertSessionOwnership(sessionId, userId);

    const existing = await this.prisma.flyerSelection.findUnique({
      where: { id: selectionId },
      select: { id: true, sessionId: true, userId: true },
    });
    if (!existing || existing.sessionId !== sessionId) {
      throw new NotFoundException('FlyerSelection hittades inte.');
    }
    if (existing.userId !== userId) {
      throw new ForbiddenException('Du saknar åtkomst till denna selection.');
    }

    await this.prisma.flyerSelection.delete({ where: { id: selectionId } });
  }

  async listOpen(userId: number, weekKey?: string): Promise<FlyerSelectionResponse[]> {
    const rows = await this.prisma.flyerSelection.findMany({
      where: {
        userId,
        status: 'planned',
        session: {
          ...(weekKey ? { weekKey } : {}),
        },
      },
      include: {
        item: {
          select: {
            id: true,
            rawName: true,
            normalizedName: true,
            price: true,
            priceUnit: true,
            matchedProductId: true,
            matchedProductName: true,
          },
        },
      },
      orderBy: { createdAt: 'desc' },
    });

    return rows.map((row) => this.toResponse(row));
  }

  async planToShoppingList(
    sessionId: number,
    userId: number,
    itemIds?: number[],
  ): Promise<{ created: number; updated: number; processedSelectionIds: number[] }> {
    await this.assertSessionOwnership(sessionId, userId);
    return this.shoppingListService.upsertFromFlyerSelections(sessionId, userId, itemIds);
  }

  async previewReceiptMatches(userId: number, dto: ReceiptMatchDto): Promise<ReceiptMatchPreviewResponse> {
    const candidates = await this.loadCandidateSelections(userId, dto.sessionId, dto.weekKey);
    const rows = this.matcher.matchRows(dto.items, candidates);
    return this.matcher.toPreviewResponse(rows, candidates.length);
  }

  async commitReceiptMatches(userId: number, dto: ReceiptMatchDto): Promise<ReceiptMatchCommitResponse> {
    const candidates = await this.loadCandidateSelections(userId, dto.sessionId, dto.weekKey);
    const previewRows = this.matcher.matchRows(dto.items, candidates);
    const overrideByIndex = new Map((dto.overrides ?? []).map((o) => [o.rowIndex, o.selectionId]));
    const candidateBySelectionId = new Map(candidates.map((c) => [c.id, c]));
    const usedSelectionIds = new Set<number>();
    const toUpdateSelectionIds: number[] = [];

    for (const row of previewRows) {
      if (row.status === 'auto' && row.selectionId != null && !usedSelectionIds.has(row.selectionId)) {
        usedSelectionIds.add(row.selectionId);
        toUpdateSelectionIds.push(row.selectionId);
        continue;
      }

      const overrideSelectionId = overrideByIndex.get(row.rowIndex);
      if (!overrideSelectionId || usedSelectionIds.has(overrideSelectionId)) {
        continue;
      }
      if (!candidateBySelectionId.has(overrideSelectionId)) {
        continue;
      }
      usedSelectionIds.add(overrideSelectionId);
      toUpdateSelectionIds.push(overrideSelectionId);
    }

    if (toUpdateSelectionIds.length > 0) {
      await this.prisma.flyerSelection.updateMany({
        where: {
          id: { in: toUpdateSelectionIds },
          userId,
          status: 'planned',
        },
        data: {
          status: 'bought',
        },
      });
    }

    const summary = this.matcher.toPreviewResponse(previewRows, candidates.length);
    return {
      boughtCount: toUpdateSelectionIds.length,
      ambiguousCount: summary.ambiguousCount,
      unmatchedCount: summary.unmatchedCount,
      updatedSelectionIds: toUpdateSelectionIds,
    };
  }

  private async assertSessionOwnership(sessionId: number, userId: number): Promise<void> {
    const session = await this.prisma.flyerSession.findUnique({
      where: { id: sessionId },
      select: { id: true, userId: true },
    });
    if (!session) {
      throw new NotFoundException('FlyerSession hittades inte.');
    }
    if (session.userId !== userId) {
      throw new ForbiddenException('Du saknar åtkomst till denna session.');
    }
  }

  private async loadCandidateSelections(
    userId: number,
    sessionId?: number,
    weekKey?: string,
  ): Promise<CandidateSelection[]> {
    if (sessionId != null && weekKey != null) {
      throw new BadRequestException('Ange antingen sessionId eller weekKey, inte båda.');
    }

    return this.prisma.flyerSelection.findMany({
      where: {
        userId,
        status: 'planned',
        ...(sessionId != null ? { sessionId } : {}),
        ...(weekKey ? { session: { weekKey } } : {}),
      },
      select: {
        id: true,
        sessionId: true,
        itemId: true,
        plannedQuantity: true,
        plannedUnit: true,
        item: {
          select: {
            rawName: true,
            normalizedName: true,
            matchedProductId: true,
            matchedProductName: true,
          },
        },
      },
      orderBy: { createdAt: 'desc' },
    });
  }

  private toResponse(row: any): FlyerSelectionResponse {
    return {
      id: row.id,
      sessionId: row.sessionId,
      itemId: row.itemId,
      userId: row.userId,
      plannedQuantity: row.plannedQuantity == null ? null : Number(row.plannedQuantity),
      plannedUnit: row.plannedUnit ?? null,
      priority: row.priority,
      note: row.note ?? null,
      status: row.status,
      createdAt: row.createdAt.toISOString(),
      updatedAt: row.updatedAt.toISOString(),
      item: {
        id: row.item.id,
        rawName: row.item.rawName,
        normalizedName: row.item.normalizedName,
        price: row.item.price == null ? null : Number(row.item.price),
        priceUnit: row.item.priceUnit ?? null,
        matchedProductId: row.item.matchedProductId ?? null,
        matchedProductName: row.item.matchedProductName ?? null,
      },
    };
  }
}
