import { Prisma } from '@prisma/client';
import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateGoodDto } from './dto/create-good.dto';
import { UpdateGoodDto } from './dto/update-good.dto';
import { QueryGoodDto } from './dto/query-good.dto';
import { BatchCreateGoodDto } from './dto/batch-create-good.dto';
import { BatchPriorityDto } from './dto/batch-priority.dto';
import { GoodDto, PaginatedGoods } from './dto/good.dto';

const GOOD_INCLUDE = {
  country: true,
  category: true,
  tag: true,
  position: true,
  originGood: true,
  goodTags: { include: { tag: true } },
} satisfies Prisma.GoodInclude;

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

  async findAll(query: QueryGoodDto): Promise<PaginatedGoods> {
    const { page, pageSize, countryId, categoryId, tagId, positionId, keyword } = query;
    const where: Prisma.GoodWhereInput = {};
    if (countryId !== undefined) where.countryId = BigInt(countryId);
    if (tagId !== undefined) where.goodTags = { some: { tagId: BigInt(tagId) } };
    if (positionId !== undefined) where.positionId = BigInt(positionId);
    if (keyword) {
      where.goodName = { contains: keyword, mode: 'insensitive' };
    }
    if (categoryId !== undefined) {
      const ids = await this.collectCategoryDescendants(BigInt(categoryId));
      where.categoryId = { in: ids };
    }

    const [total, rows] = await this.prisma.$transaction([
      this.prisma.good.count({ where }),
      this.prisma.good.findMany({
        where,
        include: GOOD_INCLUDE,
        orderBy: [{ goodPriority: 'desc' }, { createdAt: 'desc' }],
        skip: (page - 1) * pageSize,
        take: pageSize,
      }),
    ]);

    return {
      items: rows.map((g) => GoodDto.from(g, {
        country: g.country,
        category: g.category,
        tag: g.tag,
        position: g.position,
        originGood: g.originGood,
        goodTags: g.goodTags,
      })),
      total,
      page,
      pageSize,
    };
  }

  async findOne(id: bigint): Promise<GoodDto> {
    const good = await this.prisma.good.findUnique({
      where: { id },
      include: GOOD_INCLUDE,
    });
    if (!good) throw new NotFoundException(`Good ${id} not found`);
    return GoodDto.from(good, {
      country: good.country,
      category: good.category,
      tag: good.tag,
      position: good.position,
      originGood: good.originGood,
      goodTags: good.goodTags,
    });
  }

  async create(dto: CreateGoodDto): Promise<GoodDto> {
    await this.ensureReferences(dto);
    return this.prisma.$transaction(async (tx) => {
      const created = await tx.good.create({
        data: {
          goodName: dto.goodName,
          goodImage: dto.goodImage,
          originGoodId: BigInt(dto.originGoodId),
          countryId: BigInt(dto.countryId),
          categoryId: BigInt(dto.categoryId),
          positionId: dto.positionId === undefined ? null : BigInt(dto.positionId),
          goodPriority: dto.goodPriority ?? 0,
        },
      });
      if (dto.tagIds && dto.tagIds.length > 0) {
        await tx.goodTag.createMany({
          data: dto.tagIds.map((tagId) => ({
            goodId: created.id,
            tagId: BigInt(tagId),
          })),
        });
      }
      const result = await tx.good.findUniqueOrThrow({
        where: { id: created.id },
        include: GOOD_INCLUDE,
      });
      return GoodDto.from(result, {
        country: result.country,
        category: result.category,
        tag: result.tag,
        position: result.position,
        originGood: result.originGood,
        goodTags: result.goodTags,
      });
    });
  }

  async update(id: bigint, dto: UpdateGoodDto): Promise<GoodDto> {
    await this.findOne(id);
    const data: Prisma.GoodUpdateInput = {};
    if (dto.goodName !== undefined) data.goodName = dto.goodName;
    if (dto.originGoodId !== undefined) {
      await this.ensureOriginGood(dto.originGoodId);
      data.originGood = { connect: { id: BigInt(dto.originGoodId) } };
    }
    if (dto.countryId !== undefined) {
      await this.ensureCountry(dto.countryId);
      data.country = { connect: { id: BigInt(dto.countryId) } };
    }
    if (dto.categoryId !== undefined) {
      await this.ensureCategory(dto.categoryId);
      data.category = { connect: { id: BigInt(dto.categoryId) } };
    }
    if (dto.positionId !== undefined) {
      data.position =
        dto.positionId === null
          ? { disconnect: true }
          : { connect: { id: BigInt(dto.positionId) } };
    }
    if (dto.goodPriority !== undefined) data.goodPriority = dto.goodPriority;
    if (dto.goodImage !== undefined) data.goodImage = dto.goodImage;
    if (dto.tagIds !== undefined) {
      for (const tagId of dto.tagIds) {
        await this.ensureTag(tagId);
      }
    }

    return this.prisma.$transaction(async (tx) => {
      if (dto.tagIds !== undefined) {
        await tx.goodTag.deleteMany({ where: { goodId: id } });
        if (dto.tagIds.length > 0) {
          await tx.goodTag.createMany({
            data: dto.tagIds.map((tagId) => ({
              goodId: id,
              tagId: BigInt(tagId),
            })),
          });
        }
      }
      const updated = await tx.good.update({
        where: { id },
        data,
        include: GOOD_INCLUDE,
      });
      return GoodDto.from(updated, {
        country: updated.country,
        category: updated.category,
        tag: updated.tag,
        position: updated.position,
        originGood: updated.originGood,
        goodTags: updated.goodTags,
      });
    });
  }

  async remove(id: bigint): Promise<{ id: string }> {
    await this.findOne(id);
    await this.prisma.good.delete({ where: { id } });
    return { id: id.toString() };
  }

  /**
   * Updates priorities in a single transaction; either all rows update
   * or none do.
   */
  async batchUpdatePriority(dto: BatchPriorityDto): Promise<{ count: number }> {
    return this.prisma.$transaction(async (tx) => {
      for (const item of dto.items) {
        await tx.good.update({
          where: { id: BigInt(item.id) },
          data: { goodPriority: item.priority },
        });
      }
      return { count: dto.items.length };
    });
  }

  /**
   * Creates multiple goods atomically, sharing countryId/categoryId/tagIds/positionId
   * and a default priority that may be overridden per item.
   */
  async batchCreate(dto: BatchCreateGoodDto): Promise<GoodDto[]> {
    const defaultPriority = dto.defaultPriority ?? 0;
    if (dto.tagIds && dto.tagIds.length > 0) {
      for (const tagId of dto.tagIds) {
        await this.ensureTag(tagId);
      }
    }
    return this.prisma.$transaction(async (tx) => {
      const created: GoodDto[] = [];
      for (const item of dto.items) {
        const og = await tx.originGood.findUnique({
          where: { id: BigInt(item.originGoodId) },
        });
        if (!og) {
          throw new BadRequestException(
            `Origin good ${item.originGoodId} not found`,
          );
        }
        const row = await tx.good.create({
          data: {
            goodName: og.goodName ?? `Origin Good ${og.sdsGoodId}`,
            goodImage: og.goodImage,
            originGoodId: og.id,
            countryId: BigInt(dto.countryId),
            categoryId: BigInt(dto.categoryId),
            positionId: dto.positionId === undefined ? null : BigInt(dto.positionId),
            goodPriority: item.priority ?? defaultPriority,
          },
        });
        if (dto.tagIds && dto.tagIds.length > 0) {
          await tx.goodTag.createMany({
            data: dto.tagIds.map((tagId) => ({
              goodId: row.id,
              tagId: BigInt(tagId),
            })),
          });
        }
        const result = await tx.good.findUniqueOrThrow({
          where: { id: row.id },
          include: GOOD_INCLUDE,
        });
        created.push(GoodDto.from(result, {
          country: result.country,
          category: result.category,
          tag: result.tag,
          position: result.position,
          originGood: result.originGood,
          goodTags: result.goodTags,
        }));
      }
      return created;
    });
  }

  /**
   * Walk the category tree and return the requested id + all of its
   * descendants. We use a level-by-level BFS to keep the queries small
   * for the typical tree sizes we expect.
   */
  private async collectCategoryDescendants(rootId: bigint): Promise<bigint[]> {
    const ids: bigint[] = [rootId];
    let frontier: bigint[] = [rootId];
    while (frontier.length > 0) {
      const children = await this.prisma.category.findMany({
        where: { parentCategoryId: { in: frontier } },
        select: { id: true },
      });
      if (children.length === 0) break;
      const childIds = children.map((c) => c.id);
      ids.push(...childIds);
      frontier = childIds;
    }
    return ids;
  }

  private async ensureOriginGood(id: number) {
    const og = await this.prisma.originGood.findUnique({
      where: { id: BigInt(id) },
    });
    if (!og) throw new BadRequestException(`Origin good ${id} not found`);
  }

  private async ensureCountry(id: number) {
    const c = await this.prisma.country.findUnique({ where: { id: BigInt(id) } });
    if (!c) throw new BadRequestException(`Country ${id} not found`);
  }

  private async ensureCategory(id: number) {
    const c = await this.prisma.category.findUnique({ where: { id: BigInt(id) } });
    if (!c) throw new BadRequestException(`Category ${id} not found`);
  }

  private async ensureTag(id: number) {
    const t = await this.prisma.tag.findUnique({ where: { id: BigInt(id) } });
    if (!t) throw new BadRequestException(`Tag ${id} not found`);
  }

  private async ensureReferences(dto: CreateGoodDto) {
    await this.ensureOriginGood(dto.originGoodId);
    await this.ensureCountry(dto.countryId);
    await this.ensureCategory(dto.categoryId);
    if (dto.tagIds && dto.tagIds.length > 0) {
      for (const tagId of dto.tagIds) {
        await this.ensureTag(tagId);
      }
    }
    if (dto.positionId !== undefined) {
      const p = await this.prisma.position.findUnique({ where: { id: BigInt(dto.positionId) } });
      if (!p) throw new BadRequestException(`Position ${dto.positionId} not found`);
    }
  }
}