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 { GoodDetailDto, GoodDto, PaginatedGoods } from './dto/good.dto';
import { SyncService } from '../sync/sync.service';
import { randomUUID } from 'crypto';
import {
  CreateCustomGoodDto,
  CustomGoodDetailDto,
  CustomGoodVariantDto,
  UpdateCustomGoodContentDto,
} from './dto/custom-good.dto';

const GOOD_INCLUDE = {
  country: true,
  category: true,
  tag: true,
  position: true,
  originGood: {
    include: {
      detail: true,
      variants: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] },
      _count: { select: { variants: true } },
    },
  },
  goodTags: { include: { tag: true } },
} satisfies Prisma.GoodInclude;

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

  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<GoodDetailDto> {
    const good = await this.prisma.good.findUnique({
      where: { id },
      include: GOOD_INCLUDE,
    });
    if (!good) throw new NotFoundException(`Good ${id} not found`);
    return GoodDetailDto.fromGood(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);
    const result = await 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,
      });
    });
    if (
      result.originGood?.source === 'SDS' &&
      result.originGood.sdsGoodId &&
      !result.originGood.hasDetail
    ) {
      this.syncService.queueProductDetailSync(result.originGood.sdsGoodId);
    }
    return result;
  }

  async createCustom(dto: CreateCustomGoodDto): Promise<GoodDetailDto> {
    await this.ensureCountry(dto.countryId);
    await this.ensureCategory(dto.categoryId);
    if (dto.positionId !== undefined) await this.ensurePosition(dto.positionId);
    for (const tagId of dto.tagIds ?? []) await this.ensureTag(tagId);

    const goodId = await this.prisma.$transaction(async (tx) => {
      const originGood = await tx.originGood.create({
        data: {
          source: 'CUSTOM',
          sdsGoodId: `custom-${randomUUID()}`,
          goodName: dto.goodName,
          goodImage: dto.goodImage ?? null,
          goodPrice: this.decimal(dto.goodPrice),
          detail: {
            create: this.customDetailData(
              dto.detail ?? {},
            ) as Prisma.OriginGoodDetailUncheckedCreateWithoutOriginGoodInput,
          },
        },
      });
      if (dto.variants?.length) {
        await this.replaceCustomVariants(tx, originGood.id, dto.variants);
      }
      const good = await tx.good.create({
        data: {
          originGoodId: originGood.id,
          countryId: BigInt(dto.countryId),
          categoryId: BigInt(dto.categoryId),
          positionId:
            dto.positionId === undefined ? null : BigInt(dto.positionId),
          goodName: dto.goodName,
          goodImage: dto.goodImage ?? null,
          goodPriority: dto.goodPriority ?? 0,
        },
      });
      if (dto.tagIds?.length) {
        await tx.goodTag.createMany({
          data: dto.tagIds.map((tagId) => ({
            goodId: good.id,
            tagId: BigInt(tagId),
          })),
        });
      }
      return good.id;
    });
    return this.findOne(goodId);
  }

  async updateCustomContent(
    id: bigint,
    dto: UpdateCustomGoodContentDto,
  ): Promise<GoodDetailDto> {
    const existing = await this.prisma.good.findUnique({
      where: { id },
      include: { originGood: true },
    });
    if (!existing) throw new NotFoundException(`Good ${id} not found`);
    if (existing.originGood.source !== 'CUSTOM') {
      throw new BadRequestException('SDS 映射商品的上游信息不可修改');
    }

    await this.prisma.$transaction(async (tx) => {
      await tx.originGood.update({
        where: { id: existing.originGoodId },
        data: {
          goodName: dto.goodName,
          goodImage: dto.goodImage,
          goodPrice:
            dto.goodPrice === undefined ? undefined : this.decimal(dto.goodPrice),
        },
      });
      if (dto.detail !== undefined) {
        await tx.originGoodDetail.upsert({
          where: { originGoodId: existing.originGoodId },
          create: {
            originGoodId: existing.originGoodId,
            ...(this.customDetailData(
              dto.detail,
            ) as Prisma.OriginGoodDetailUncheckedCreateWithoutOriginGoodInput),
          },
          update: this.customDetailData(dto.detail, true),
        });
      }
      if (dto.variants !== undefined) {
        await this.replaceCustomVariants(
          tx,
          existing.originGoodId,
          dto.variants,
        );
      }
      const goodData: Prisma.GoodUpdateInput = {};
      if (dto.goodName !== undefined) goodData.goodName = dto.goodName;
      if (dto.goodImage !== undefined) goodData.goodImage = dto.goodImage;
      if (Object.keys(goodData).length) {
        await tx.good.update({ where: { id }, data: goodData });
      }
    });
    return this.findOne(id);
  }

  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);
      }
    }

    const result = await 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,
      });
    });
    if (
      result.originGood?.source === 'SDS' &&
      result.originGood.sdsGoodId &&
      !result.originGood.hasDetail
    ) {
      this.syncService.queueProductDetailSync(result.originGood.sdsGoodId);
    }
    return result;
  }

  async remove(id: bigint): Promise<{ id: string }> {
    const good = await this.prisma.good.findUnique({
      where: { id },
      include: { originGood: true },
    });
    if (!good) throw new NotFoundException(`Good ${id} not found`);
    await this.prisma.$transaction(async (tx) => {
      await tx.good.delete({ where: { id } });
      if (good.originGood.source === 'CUSTOM') {
        const remaining = await tx.good.count({
          where: { originGoodId: good.originGoodId },
        });
        if (remaining === 0) {
          await tx.originGood.delete({ where: { id: good.originGoodId } });
        }
      }
    });
    return { id: id.toString() };
  }

  /**
   * Updates priorities in a single transaction; either all rows update
   * or none do.
   */
  async batchUpdatePriority(dto: BatchPriorityDto): Promise<{ count: number }> {
    const result = await 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 };
    });
    return result;
  }

  /**
   * 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);
      }
    }
    const result = await 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;
    });
    for (const goodId of new Set(
      result
        .filter(
          (item) =>
            item.originGood?.source === 'SDS' &&
            !item.originGood.hasDetail,
        )
        .map((item) => item.originGood?.sdsGoodId)
        .filter((id): id is string => Boolean(id)),
    )) {
      this.syncService.queueProductDetailSync(goodId);
    }
    return result;
  }

  /**
   * 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`);
    }
  }

  private async ensurePosition(id: number) {
    const position = await this.prisma.position.findUnique({
      where: { id: BigInt(id) },
    });
    if (!position) throw new BadRequestException(`Position ${id} not found`);
  }

  private decimal(value: string | null | undefined): Prisma.Decimal | null {
    return value === undefined || value === null || value === ''
      ? null
      : new Prisma.Decimal(value);
  }

  private customDetailData(
    detail: CustomGoodDetailDto,
    preserveMissing = false,
  ): Prisma.OriginGoodDetailUncheckedUpdateInput {
    const nullable = <T>(value: T | null | undefined): T | null | undefined =>
      preserveMissing && value === undefined ? undefined : value ?? null;
    const decimal = (value: string | null | undefined) =>
      preserveMissing && value === undefined ? undefined : this.decimal(value);
    const json = (
      value: Record<string, unknown> | null | undefined,
    ): Prisma.InputJsonValue | Prisma.NullTypes.DbNull | undefined =>
      preserveMissing && value === undefined
        ? undefined
        : value === null || value === undefined
          ? Prisma.DbNull
          : (value as Prisma.InputJsonValue);
    return {
      productCode: nullable(detail.productCode),
      englishName: nullable(detail.englishName),
      blankDesignUrl: nullable(detail.blankDesignUrl),
      detailsPageVideoUrl: nullable(detail.detailsPageVideoUrl),
      textureName: nullable(detail.textureName),
      productionCycleHours: nullable(detail.productionCycleHours),
      minWeightG: decimal(detail.minWeightG),
      reminder: nullable(detail.reminder),
      productionProcess: nullable(detail.productionProcess),
      materialDescription: nullable(detail.materialDescription),
      productPerformance: nullable(detail.productPerformance),
      applicableScenarios: nullable(detail.applicableScenarios),
      washingInstructions: nullable(detail.washingInstructions),
      specialDescription: nullable(detail.specialDescription),
      designExplanation: nullable(detail.designExplanation),
      designArea: nullable(detail.designArea),
      pictureRequest: nullable(detail.pictureRequest),
      sizeChart: json(detail.sizeChart),
      packageSpecs: json(detail.packageSpecs),
      options: json(detail.options),
      media: json(detail.media),
    };
  }

  private async replaceCustomVariants(
    tx: Prisma.TransactionClient,
    originGoodId: bigint,
    variants: CustomGoodVariantDto[],
  ): Promise<void> {
    await tx.originGoodVariant.deleteMany({ where: { originGoodId } });
    for (const variant of variants) {
      await tx.originGoodVariant.create({
        data: {
          originGoodId,
          sdsVariantId: `custom-${randomUUID()}`,
          sku: variant.sku,
          sizeId: variant.sizeId ?? null,
          sizeName: variant.sizeName ?? null,
          colorId: variant.colorId ?? null,
          colorName: variant.colorName ?? null,
          colorHex: variant.colorHex ?? null,
          imageUrl: variant.imageUrl ?? null,
          price: this.decimal(variant.price),
          originalPrice: this.decimal(variant.originalPrice),
          weightG: this.decimal(variant.weightG),
          boxLengthCm: this.decimal(variant.boxLengthCm),
          boxWidthCm: this.decimal(variant.boxWidthCm),
          boxHeightCm: this.decimal(variant.boxHeightCm),
          enabled: variant.enabled ?? true,
          sortOrder: variant.sortOrder ?? 0,
          designData:
            variant.designData === null || variant.designData === undefined
              ? Prisma.DbNull
              : (variant.designData as Prisma.InputJsonValue),
        },
      });
    }
  }
}
