import { Prisma } from '@prisma/client';
import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { parseOriginName } from '../product-families/origin-name.parser';
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 { FamilyRecomputeService } from '../product-families/family-recompute.service';
import { isAutoTagGroupName } from '../product-families/auto-tag-rules';
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 } },
      family: { select: { id: true, familyCode: true, familyName: true, stale: true } },
    },
  },
  goodTags: { include: { tag: true } },
  mergedOriginGoods: {
    orderBy: { createdAt: 'asc' },
    include: {
      originGood: {
        include: {
          detail: true,
          variants: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] },
          _count: { select: { variants: true } },
        },
      },
    },
  },
} satisfies Prisma.GoodInclude;

/**
 * 商品名（公开展示名）规范化：结构完整的原始链接名（`国家（物流）品名-型号-…`，
 * 含半角/全角括号混用）一律存为「品名 型号」；已解析名与自由命名的自定义名原样保留。
 * 防的是绕过前端弹窗（预填解析名）的写入路径——API 直调、批量工具、旧客户端。
 */
export function normalizeGoodName(name: string): string {
  const parsed = parseOriginName(name);
  if (parsed.productName && parsed.skuCode) {
    return `${parsed.productName} ${parsed.skuCode}`;
  }
  return name;
}

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

  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,
        mergedOriginGoods: g.mergedOriginGoods,
      })),
      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,
      mergedOriginGoods: good.mergedOriginGoods,
    });
  }

  async create(dto: CreateGoodDto): Promise<GoodDto> {
    await this.ensureReferences(dto);
    const mergedIds = this.dedupeMergedIds(
      BigInt(dto.originGoodId),
      dto.mergedOriginGoodIds,
    );
    await this.ensureMergedOriginGoods(mergedIds);
    const result = await this.prisma.$transaction(async (tx) => {
      // Good 的族是派生数据：主链接所属族
      const primary = await tx.originGood.findUnique({
        where: { id: BigInt(dto.originGoodId) },
        select: { familyId: true },
      });
      const tagIds = await this.stripAutoGroupTags(dto.tagIds ?? [], primary?.familyId ?? null);
      const created = await tx.good.create({
        data: {
          goodName: normalizeGoodName(dto.goodName),
          goodImage: dto.goodImage,
          originGoodId: BigInt(dto.originGoodId),
          familyId: primary?.familyId ?? null,
          countryId: BigInt(dto.countryId),
          categoryId: BigInt(dto.categoryId),
          positionId: dto.positionId === undefined ? null : BigInt(dto.positionId),
          goodPriority: dto.goodPriority ?? 0,
        },
      });
      if (tagIds.length > 0) {
        await tx.goodTag.createMany({
          data: tagIds.map((tagId) => ({
            goodId: created.id,
            tagId: BigInt(tagId),
          })),
        });
      }
      if (mergedIds.length > 0) {
        await tx.goodOriginGood.createMany({
          data: mergedIds.map((originGoodId) => ({
            goodId: created.id,
            originGoodId,
          })),
        });
      }
      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,
        mergedOriginGoods: result.mergedOriginGoods,
      });
    });
    if (result.originGood) {
      // 建商品后把链接有效标签镜像到该商品（纯聚合，不派生——派生在整理流程）
      await this.familyRecompute.mirrorLinkTagsToGoods(BigInt(result.originGoodId));
    }
    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);
    let family: { id: bigint } | null = null;
    if (dto.familyId !== undefined) {
      family = await this.prisma.productFamily.findUnique({
        where: { id: BigInt(dto.familyId) },
        select: { id: true },
      });
      if (!family) throw new NotFoundException(`product family ${dto.familyId} not found`);
    }

    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),
          ...(family ? { familyId: family.id } : {}),
          ...(dto.logisticsLabel !== undefined || dto.craftLabel !== undefined
            ? {
                logisticsLabel: dto.logisticsLabel ?? null,
                craftLabel: dto.craftLabel ?? null,
                skuCode: dto.skuCode ?? null,
                warehouseLabel: dto.warehouseLabel ?? null,
              }
            : {}),
          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,
          familyId: family?.id ?? null,
          countryId: BigInt(dto.countryId),
          categoryId: BigInt(dto.categoryId),
          positionId:
            dto.positionId === undefined ? null : BigInt(dto.positionId),
          goodName: normalizeGoodName(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;
    });
    if (family) this.familyRecompute.enqueue(family.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 = normalizeGoodName(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);
    let mergedIds: bigint[] | undefined;
    if (dto.mergedOriginGoodIds !== undefined || dto.originGoodId !== undefined) {
      const current = await this.prisma.good.findUniqueOrThrow({
        where: { id },
        select: { originGoodId: true },
      });
      const primaryId =
        dto.originGoodId !== undefined ? BigInt(dto.originGoodId) : current.originGoodId;
      mergedIds = this.dedupeMergedIds(primaryId, dto.mergedOriginGoodIds);
      await this.ensureMergedOriginGoods(mergedIds);
    }
    const data: Prisma.GoodUpdateInput = {};
    if (dto.goodName !== undefined) data.goodName = normalizeGoodName(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.familyId !== undefined) {
      if (dto.familyId !== null) {
        const family = await this.prisma.productFamily.findUnique({
          where: { id: BigInt(dto.familyId) },
          select: { id: true },
        });
        if (!family) throw new NotFoundException(`product family ${dto.familyId} not found`);
        data.family = { connect: { id: family.id } };
      } else {
        data.family = { disconnect: true };
      }
    }
    if (dto.tagIds !== undefined) {
      for (const tagId of dto.tagIds) {
        await this.ensureTag(tagId);
      }
    }

    // 有族商品的自动组（物流/工艺/位置）标签为族派生数据，不接受手动写入
    let familyIdForTags: bigint | null;
    if (dto.familyId !== undefined) {
      familyIdForTags = dto.familyId === null ? null : BigInt(dto.familyId);
    } else {
      familyIdForTags =
        (
          await this.prisma.good.findUnique({
            where: { id },
            select: { familyId: true },
          })
        )?.familyId ?? null;
    }
    const updateTagIds =
      dto.tagIds !== undefined
        ? await this.stripAutoGroupTags(dto.tagIds, familyIdForTags)
        : undefined;

    const result = await this.prisma.$transaction(async (tx) => {
      if (updateTagIds !== undefined) {
        await tx.goodTag.deleteMany({ where: { goodId: id } });
        if (updateTagIds.length > 0) {
          await tx.goodTag.createMany({
            data: updateTagIds.map((tagId) => ({
              goodId: id,
              tagId: BigInt(tagId),
            })),
          });
        }
      }
      if (mergedIds !== undefined) {
        await tx.goodOriginGood.deleteMany({ where: { goodId: id } });
        if (mergedIds.length > 0) {
          await tx.goodOriginGood.createMany({
            data: mergedIds.map((originGoodId) => ({
              goodId: id,
              originGoodId,
            })),
          });
        }
      }
      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,
        mergedOriginGoods: updated.mergedOriginGoods,
      });
    });
    if (result.originGood) {
      // 更新后把链接有效标签镜像到该商品（纯聚合，不派生）
      await this.familyRecompute.mirrorLinkTagsToGoods(BigInt(result.originGoodId));
    }
    if (familyIdForTags) {
      // 归族联动后重算矩阵（成员变化 → 自动重算，结构化聚合）
      await this.familyRecompute.recomputeFamily(familyIdForTags);
    }
    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,
            familyId: og.familyId,
            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 itemMerged = this.dedupeMergedIds(og.id, item.mergedOriginGoodIds);
        if (itemMerged.length > 0) {
          const existRows = await tx.originGood.findMany({
            where: { id: { in: itemMerged } },
            select: { id: true },
          });
          if (existRows.length !== itemMerged.length) {
            const found = new Set(existRows.map((r) => r.id.toString()));
            const missing = itemMerged.find((mid) => !found.has(mid.toString()));
            throw new BadRequestException(`Origin good ${missing} not found`);
          }
          await tx.goodOriginGood.createMany({
            data: itemMerged.map((originGoodId) => ({
              goodId: row.id,
              originGoodId,
            })),
          });
        }
        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,
          mergedOriginGoods: result.mergedOriginGoods,
        }));
      }
      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`);
  }

  /** Dedupe merged ids and reject any that equals the primary source. */
  private dedupeMergedIds(primaryId: bigint, ids?: number[]): bigint[] {
    if (!ids || ids.length === 0) return [];
    const unique = [...new Set(ids.map((id) => BigInt(id)))];
    if (unique.includes(primaryId)) {
      throw new BadRequestException(
        'mergedOriginGoodIds 不能包含主源 originGoodId',
      );
    }
    return unique;
  }

  private async ensureMergedOriginGoods(ids: bigint[]) {
    if (ids.length === 0) return;
    const rows = await this.prisma.originGood.findMany({
      where: { id: { in: ids } },
      select: { id: true },
    });
    if (rows.length !== ids.length) {
      const found = new Set(rows.map((r) => r.id.toString()));
      const missing = ids.find((id) => !found.has(id.toString()));
      throw new BadRequestException(`Origin good ${missing} 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 stripAutoGroupTags(
    tagIds: number[],
    familyId: bigint | null,
  ): Promise<number[]> {
    if (!familyId || !tagIds.length) return tagIds;
    const groups = await this.prisma.tagGroup.findMany({
      select: { id: true, groupName: true },
    });
    const autoIds = new Set(
      groups.filter((g) => isAutoTagGroupName(g.groupName)).map((g) => g.id.toString()),
    );
    const tags = await this.prisma.tag.findMany({
      where: { id: { in: tagIds.map((id) => BigInt(id)) } },
      select: { id: true, tagGroupId: true },
    });
    const blocked = new Set(
      tags
        .filter((t) => t.tagGroupId && autoIds.has(t.tagGroupId.toString()))
        .map((t) => t.id.toString()),
    );
    return blocked.size ? tagIds.filter((id) => !blocked.has(String(id))) : tagIds;
  }

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