import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
  SdsClientService,
  SdsCategoryTreeNode,
  SdsProduct,
  SdsProductDetail,
} from './sds-client.service';
import { normalizeProductDetail } from './sds-product-detail.mapper';
import { FamilyRecomputeService } from '../product-families/family-recompute.service';
import { originGroupKey, parseOriginName } from '../product-families/origin-name.parser';

export interface CategorySyncResult {
  inserted: number;
  updated: number;
  total: number;
  deletedStale: number;
}

export interface ProductSyncResult {
  inserted: number;
  updated: number;
  total: number;
  leafCategories: number;
  delisted: number;
}

/**
 * Safety guards so a degenerate/partial SDS response never triggers a
 * destructive operation (stale category deletion / mass delist marking).
 * Upstream normally returns ~226 categories and ~150 leaf categories with
 * hundreds of products — the floors below only trigger on abnormal responses.
 */
export const SYNC_GUARDS = {
  MIN_CATEGORY_COUNT: 10,
  MIN_CATEGORY_RATIO: 0.5,
  MIN_LEAF_CATEGORIES: 10,
  MIN_SEEN_GOODS: 50,
} as const;

/**
 * True when the fetched category count is suspiciously small compared to the
 * categories already synced from SDS, i.e. the upstream response is likely
 * partial/degenerate. In that case stale deletion must be skipped.
 */
export function shouldSkipStaleDeletion(fetched: number, existingSds: number): boolean {
  if (existingSds <= 0) return false;
  return (
    fetched < SYNC_GUARDS.MIN_CATEGORY_COUNT ||
    fetched < SYNC_GUARDS.MIN_CATEGORY_RATIO * existingSds
  );
}

/**
 * True only when both the leaf-category count and the number of seen products
 * are healthy enough to trust the "not seen upstream => delisted" conclusion.
 */
export function shouldRunDelistDetection(leafCategories: number, seenGoods: number): boolean {
  return (
    leafCategories >= SYNC_GUARDS.MIN_LEAF_CATEGORIES &&
    seenGoods >= SYNC_GUARDS.MIN_SEEN_GOODS
  );
}

@Injectable()
export class SyncService {
  private readonly logger = new Logger(SyncService.name);
  private running = { categories: false, products: false, details: false };

  constructor(
    private readonly prisma: PrismaService,
    private readonly sds: SdsClientService,
    private readonly familyRecompute: FamilyRecomputeService,
  ) {}

  /**
   * Hourly full sync — runs `syncCategories` first (since product
   * sync depends on knowing which leaf categories exist) and then
   * `syncProducts`.
   */
  @Cron(CronExpression.EVERY_HOUR)
  async hourlyCron(): Promise<void> {
    try {
      await this.syncCategories();
      await this.syncProducts();
    } catch (err) {
      this.logger.error('Hourly cron sync failed', err as Error);
    }
  }

  /** Fire-and-forget wrappers for manual triggers via HTTP. */
  async startCategorySync(): Promise<{ message: string }> {
    if (this.running.categories) {
      return { message: 'Category sync already in progress' };
    }
    void this.syncCategories().catch((err) =>
      this.logger.error('Category sync failed', err as Error),
    );
    return { message: 'Category sync started' };
  }

  async startProductSync(): Promise<{ message: string }> {
    if (this.running.products) {
      return { message: 'Product sync already in progress' };
    }
    void this.syncProducts().catch((err) =>
      this.logger.error('Product sync failed', err as Error),
    );
    return { message: 'Product sync started' };
  }

  /** Refresh all active SDS product details once per day at 03:30. */
  @Cron('0 30 3 * * *', { timeZone: 'Asia/Shanghai' })
  async dailyProductDetailCron(): Promise<void> {
    try {
      await this.syncProductDetails();
    } catch (err) {
      this.logger.error('Daily product detail sync failed', err as Error);
    }
  }

  async startProductDetailSync(): Promise<{ message: string }> {
    if (this.running.details) {
      return { message: 'Product detail sync already in progress' };
    }
    void this.syncProductDetails().catch((err) =>
      this.logger.error('Product detail sync failed', err as Error),
    );
    return { message: 'Product detail sync started' };
  }

  /** Check if a sync type is currently running. */
  isRunning(type: 'categories' | 'products' | 'details'): boolean {
    return this.running[type];
  }

  async syncCategories(): Promise<CategorySyncResult> {
    if (this.running.categories) {
      throw new Error('Category sync already in progress');
    }
    this.running.categories = true;
    const log = await this.prisma.syncLog.create({
      data: { type: 'CATEGORIES', status: 'RUNNING' },
    });
    try {
      const tree = await this.sds.fetchCategoryTree();
      const flat = this.flattenCategoryTree(tree);
      this.logger.log(`Fetched ${flat.length} SDS categories`);
      const seenSdsIds = new Set(flat.map((n) => n.sdsId));

      // Guard: if the upstream tree is suspiciously small vs what we already
      // have from SDS, skip stale deletion entirely — a partial response must
      // never wipe the category library.
      const existingSdsCount = await this.prisma.category.count({
        where: { sdsCategoryId: { not: null } },
      });
      const skipStaleDeletion = shouldSkipStaleDeletion(flat.length, existingSdsCount);
      if (skipStaleDeletion) {
        this.logger.warn(
          `Skipping stale category deletion: fetched=${flat.length} existingSds=${existingSdsCount} ` +
            `(below guard thresholds)`,
        );
      }

      // Single transaction: upsert + wire parents + delete stale.
      // SDS tree is the source of truth — anything not in the response gets deleted
      // (unless the response looks degenerate, see guard above).
      const { inserted, updated, deletedStale } = await this.prisma.$transaction(async (tx) => {
        let ins = 0;
        let upd = 0;

        // 1. Upsert all SDS categories
        for (const node of flat) {
          const existing = await tx.category.findUnique({
            where: { sdsCategoryId: node.sdsId },
          });
          if (!existing) {
            await tx.category.create({
              data: {
                sdsCategoryId: node.sdsId,
                categoryName: node.name,
                categoryIcon: node.icon ?? null,
              },
            });
            ins++;
          } else {
            await tx.category.update({
              where: { id: existing.id },
              data: {
                categoryName: node.name,
                categoryIcon: node.icon ?? null,
              },
            });
            upd++;
          }
        }

        // 2. Wire parent-child relationships
        for (const node of flat) {
          if (!node.parentSdsId) continue;
          const child = await tx.category.findUnique({
            where: { sdsCategoryId: node.sdsId },
          });
          const parent = await tx.category.findUnique({
            where: { sdsCategoryId: node.parentSdsId },
          });
          if (child && parent && child.parentCategoryId !== parent.id) {
            await tx.category.update({
              where: { id: child.id },
              data: { parentCategoryId: parent.id },
            });
          }
        }

        // 3. Delete stale categories (in DB but not in SDS response)
        //    Detach parent links first, then delete leaf-first to respect FK constraints.
        //    Skipped entirely when the response looks degenerate (see guard above).
        let deletedStale = 0;
        if (!skipStaleDeletion) {
          const staleCats = await tx.category.findMany({
            where: { sdsCategoryId: { notIn: [...seenSdsIds] } },
            select: { id: true },
          });
          const staleIds = staleCats.map((c) => c.id);

          // Protect categories that have configured goods — onDelete: Restrict
          const goodsInStale = await tx.good.groupBy({
            by: ['categoryId'],
            where: { categoryId: { in: staleIds } },
          });
          const protectedIds = new Set(goodsInStale.map((g) => g.categoryId));
          const deletableIds = staleIds.filter((id) => !protectedIds.has(id));
          deletedStale = deletableIds.length;

          // Detach all deletable categories from their parents
          if (deletableIds.length > 0) {
            await tx.category.updateMany({
              where: { id: { in: deletableIds } },
              data: { parentCategoryId: null },
            });
            // Also detach any non-deletable children pointing to deletable parents
            await tx.category.updateMany({
              where: { parentCategoryId: { in: deletableIds } },
              data: { parentCategoryId: null },
            });
            // Delete leaf-first (repeatedly remove nodes with no children)
            let remaining = [...deletableIds];
            while (remaining.length > 0) {
              const withChildren = await tx.category.findMany({
                where: { parentCategoryId: { in: remaining } },
                select: { parentCategoryId: true },
                distinct: ['parentCategoryId'],
              });
              const hasChildSet = new Set(
                withChildren.filter((c) => c.parentCategoryId).map((c) => c.parentCategoryId!.toString()),
              );
              const leaves = remaining.filter((id) => !hasChildSet.has(id.toString()));
              if (leaves.length === 0) break; // safety: circular dependency
              await tx.category.deleteMany({ where: { id: { in: leaves } } });
              remaining = remaining.filter((id) => !leaves.some((l) => l === id));
            }
          }
        }

        return { inserted: ins, updated: upd, deletedStale };
      });

      await this.prisma.syncLog.update({
        where: { id: log.id },
        data: {
          status: 'SUCCESS',
          finishedAt: new Date(),
          message: `inserted=${inserted} updated=${updated} total=${flat.length} staleDeleted=${deletedStale}`,
        },
      });
      return { inserted, updated, total: flat.length, deletedStale };
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      await this.prisma.syncLog.update({
        where: { id: log.id },
        data: {
          status: 'FAILED',
          finishedAt: new Date(),
          message,
        },
      });
      throw err;
    } finally {
      this.running.categories = false;
    }
  }

  async syncProducts(): Promise<ProductSyncResult> {
    if (this.running.products) {
      throw new Error('Product sync already in progress');
    }
    this.running.products = true;
    const log = await this.prisma.syncLog.create({
      data: { type: 'PRODUCTS', status: 'RUNNING' },
    });
    try {
      // Identify leaf categories — those with no children.
      const all = await this.prisma.category.findMany({
        select: { id: true, sdsCategoryId: true },
      });
      const parents = await this.prisma.category.findMany({
        where: { parent: { isNot: null } },
        select: { parentCategoryId: true },
      });
      const parentIds = new Set(parents.map((p) => p.parentCategoryId!));
      const leafRows = all.filter((c) => !parentIds.has(c.id) && c.sdsCategoryId);

      let inserted = 0;
      let updated = 0;
      let total = 0;
      const seenSdsGoodIds = new Set<string>();

      for (const leaf of leafRows) {
        const sdsCategoryId = leaf.sdsCategoryId!;
        let page = 1;
        // eslint-disable-next-line no-constant-condition
        while (true) {
          const resp = await this.sds.fetchProductsPage(sdsCategoryId, page, 50);
          const products = resp.items ?? resp.content ?? [];
          if (products.length === 0) break;
          for (const product of products) {
            seenSdsGoodIds.add(String(product.id));
            const upserted = await this.upsertOriginGood(product, sdsCategoryId);
            if (upserted === 'inserted') inserted++;
            else updated++;
            total++;
          }
          if (products.length < 50) break;
          page++;
          if (page > 200) {
            // Safety net — at most 10k products per category.
            this.logger.warn(`Reached 200-page safety cap for ${sdsCategoryId}`);
            break;
          }
        }
      }

      // Detect delisted products: mark origin goods not seen in upstream as delisted,
      // and re-activate any previously delisted goods that reappeared.
      // Guard: only trust this conclusion when the sync covered a healthy number of
      // leaf categories and saw a healthy number of products — otherwise a partial
      // sync must never mass-delist the product library.
      let delistedCount = 0;
      let reactivatedCount = 0;
      const runDelist = shouldRunDelistDetection(leafRows.length, seenSdsGoodIds.size);
      if (runDelist) {
        const delistedResult = await this.prisma.originGood.updateMany({
          where: {
            source: 'SDS',
            sdsGoodId: { notIn: [...seenSdsGoodIds] },
            delisted: false,
          },
          data: { delisted: true },
        });
        const reactivatedResult = await this.prisma.originGood.updateMany({
          where: {
            source: 'SDS',
            sdsGoodId: { in: [...seenSdsGoodIds] },
            delisted: true,
          },
          data: { delisted: false },
        });
        delistedCount = delistedResult.count;
        reactivatedCount = reactivatedResult.count;
      } else {
        this.logger.warn(
          `Skipping delist detection: leafCategories=${leafRows.length} seenGoods=${seenSdsGoodIds.size} ` +
            `(below guard thresholds)`,
        );
      }

      await this.prisma.syncLog.update({
        where: { id: log.id },
        data: {
          status: 'SUCCESS',
          finishedAt: new Date(),
          message: `inserted=${inserted} updated=${updated} total=${total} delisted=${delistedCount} reactivated=${reactivatedCount} leafCategories=${leafRows.length}`,
        },
      });
      return {
        inserted,
        updated,
        total,
        leafCategories: leafRows.length,
        delisted: delistedCount,
      };
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      await this.prisma.syncLog.update({
        where: { id: log.id },
        data: {
          status: 'FAILED',
          finishedAt: new Date(),
          message,
        },
      });
      throw err;
    } finally {
      this.running.products = false;
    }
  }

  async getStatus(limit = 20) {
    return this.prisma.syncLog.findMany({
      orderBy: { startedAt: 'desc' },
      take: limit,
    });
  }

  async syncProductDetails(): Promise<{
    total: number;
    synced: number;
    failed: number;
  }> {
    if (this.running.details) {
      throw new Error('Product detail sync already in progress');
    }
    this.running.details = true;
    const log = await this.prisma.syncLog.create({
      data: { type: 'PRODUCT_DETAILS', status: 'RUNNING' },
    });
    try {
      const result = await this.syncAllProductDetails(async (progress) => {
        await this.prisma.syncLog.update({
          where: { id: log.id },
          data: {
            message: `processed=${progress.processed}/${progress.total} synced=${progress.synced} failed=${progress.failed}`,
          },
        });
      });
      await this.prisma.syncLog.update({
        where: { id: log.id },
        data: {
          status: 'SUCCESS',
          finishedAt: new Date(),
          message: `total=${result.total} synced=${result.synced} failed=${result.failed}`,
        },
      });
      return result;
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      await this.prisma.syncLog.update({
        where: { id: log.id },
        data: { status: 'FAILED', finishedAt: new Date(), message },
      });
      throw error;
    } finally {
      this.running.details = false;
    }
  }

  async syncOneProductDetail(goodId: string): Promise<{
    goodId: string;
    variants: number;
    detailSyncedAt: string;
  }> {
    const originGood = await this.prisma.originGood.findUnique({
      where: { sdsGoodId: goodId },
      select: { id: true, source: true },
    });
    if (!originGood) {
      throw new NotFoundException(`SDS product ${goodId} not found locally`);
    }
    if (originGood.source !== 'SDS') {
      throw new BadRequestException('自定义商品不支持从 SDS 同步详情');
    }
    const upstream = await this.sds.fetchProductDetail(goodId);
    const normalized = normalizeProductDetail(upstream);
    await this.persistProductDetail(originGood.id, upstream);
    return {
      goodId,
      variants: normalized.variants.length,
      detailSyncedAt: new Date().toISOString(),
    };
  }

  queueProductDetailSync(goodId: string): void {
    void this.syncOneProductDetail(goodId).catch((error) => {
      const message = error instanceof Error ? error.message : String(error);
      this.logger.warn(`Queued detail sync failed for ${goodId}: ${message}`);
    });
  }

  async syncConfiguredProductDetails(): Promise<{ synced: number; failed: number }> {
    const result = await this.syncMatchingProductDetails({
      delisted: false,
      source: 'SDS',
      goods: { some: {} },
    });
    return { synced: result.synced, failed: result.failed };
  }

  async syncAllProductDetails(
    onProgress?: (progress: {
      processed: number;
      total: number;
      synced: number;
      failed: number;
    }) => Promise<void>,
  ): Promise<{ total: number; synced: number; failed: number }> {
    return this.syncMatchingProductDetails(
      { delisted: false, source: 'SDS' },
      onProgress,
      2,
    );
  }

  private async syncMatchingProductDetails(
    where: Prisma.OriginGoodWhereInput,
    onProgress?: (progress: {
      processed: number;
      total: number;
      synced: number;
      failed: number;
    }) => Promise<void>,
    attempts = 1,
  ): Promise<{ total: number; synced: number; failed: number }> {
    const originGoods = await this.prisma.originGood.findMany({
      where,
      select: { id: true, sdsGoodId: true },
      orderBy: { id: 'asc' },
    });
    let synced = 0;
    let failed = 0;
    let processed = 0;
    for (const originGood of originGoods) {
      let lastError: unknown;
      let succeeded = false;
      for (let attempt = 1; attempt <= attempts; attempt++) {
        try {
          const upstream = await this.sds.fetchProductDetail(originGood.sdsGoodId);
          await this.persistProductDetail(originGood.id, upstream);
          synced++;
          succeeded = true;
          break;
        } catch (error) {
          lastError = error;
        }
      }
      if (!succeeded) {
        failed++;
        const message = lastError instanceof Error ? lastError.message : String(lastError);
        this.logger.warn(`Failed to sync SDS detail ${originGood.sdsGoodId}: ${message}`);
      }
      processed++;
      if (onProgress && (processed % 10 === 0 || processed === originGoods.length)) {
        await onProgress({ processed, total: originGoods.length, synced, failed });
      }
    }
    return { total: originGoods.length, synced, failed };
  }

  async importProductDetail(upstream: SdsProductDetail): Promise<{
    goodId: string;
    variants: number;
    sizeRows: number;
    packageRows: number;
    configuredGoods: number;
  }> {
    const goodId = String(upstream.id);
    const normalized = normalizeProductDetail(upstream);
    const originGood = await this.prisma.originGood.upsert({
      where: { sdsGoodId: goodId },
      create: {
        sdsGoodId: goodId,
        goodName: String(upstream.name ?? goodId),
        goodImage: String(upstream.psd_img_url ?? upstream.img_url ?? upstream.blankDesignUrl ?? '') || null,
        goodPrice:
          upstream.min_price === undefined || upstream.min_price === null
            ? null
            : new Prisma.Decimal(Number(upstream.min_price)),
      },
      update: {
        goodName: upstream.name ? String(upstream.name) : undefined,
        goodImage: String(upstream.psd_img_url ?? upstream.img_url ?? upstream.blankDesignUrl ?? '') || undefined,
        goodPrice:
          upstream.min_price === undefined || upstream.min_price === null
            ? undefined
            : new Prisma.Decimal(Number(upstream.min_price)),
      },
    });
    await this.persistProductDetail(originGood.id, upstream);
    const configuredGoods = await this.prisma.good.count({
      where: { originGoodId: originGood.id },
    });
    const sizeChart = normalized.sizeChart as { rows?: unknown[] } | null;
    const packageSpecs = normalized.packageSpecs as { rows?: unknown[] } | null;
    return {
      goodId,
      variants: normalized.variants.length,
      sizeRows: sizeChart?.rows?.length ?? 0,
      packageRows: packageSpecs?.rows?.length ?? 0,
      configuredGoods,
    };
  }

  private async persistProductDetail(originGoodId: bigint, upstream: SdsProductDetail): Promise<void> {
    const normalized = normalizeProductDetail(upstream);
    const { variants, ...detail } = normalized;
    await this.prisma.$transaction(async (tx) => {
      const json = (value: Prisma.InputJsonValue | null) => value ?? Prisma.DbNull;
      // Backfill the origin good's price from upstream min_price when present
      if (upstream.min_price !== undefined && upstream.min_price !== null) {
        await tx.originGood.update({
          where: { id: originGoodId },
          data: { goodPrice: new Prisma.Decimal(Number(upstream.min_price)) },
        });
      }
      await tx.originGoodDetail.upsert({
        where: { originGoodId },
        create: {
          originGoodId,
          ...detail,
          sizeChart: json(detail.sizeChart),
          packageSpecs: json(detail.packageSpecs),
          options: json(detail.options),
          media: json(detail.media),
        },
        update: {
          ...detail,
          sizeChart: json(detail.sizeChart),
          packageSpecs: json(detail.packageSpecs),
          options: json(detail.options),
          media: json(detail.media),
          syncedAt: new Date(),
        },
      });
      const seenVariantIds: string[] = [];
      for (const variant of variants) {
        seenVariantIds.push(variant.sdsVariantId);
        const { designData, ...data } = variant;
        await tx.originGoodVariant.upsert({
          where: {
            originGoodId_sdsVariantId: {
              originGoodId,
              sdsVariantId: variant.sdsVariantId,
            },
          },
          create: { originGoodId, ...data, designData: json(designData) },
          update: { ...data, designData: json(designData) },
        });
      }
      await tx.originGoodVariant.deleteMany({
        where: {
          originGoodId,
          ...(seenVariantIds.length ? { sdsVariantId: { notIn: seenVariantIds } } : {}),
        },
      });
    });
    // 族成员的详情/变体变化 → 异步重算该族（进程内去重）
    await this.maybeEnqueueFamilyRecompute(originGoodId);
  }

  /** 详情同步后的族重算钩子：链接有族归属才入队 */
  private async maybeEnqueueFamilyRecompute(originGoodId: bigint): Promise<void> {
    const og = await this.prisma.originGood.findUnique({
      where: { id: originGoodId },
      select: { familyId: true },
    });
    if (og?.familyId) this.familyRecompute.enqueue(og.familyId);
  }

  /**
   * 新链接自动挂族：优先按 SDS 分类（=产品模型）匹配已有族的成员；
   * 无分类时回退名称 3 段键。恰好命中唯一族才挂载（多族/零族留给管理员裁决）。
   * 锁定族（autoManaged=false）不吸收新成员，只置 stale 提示。
   */
  private async tryAutoAttachToFamily(originGoodId: bigint, goodName: string): Promise<void> {
    const self = await this.prisma.originGood.findUnique({
      where: { id: originGoodId },
      select: { sdsCategoryId: true },
    });
    let familyIds: Set<string>;
    if (self?.sdsCategoryId) {
      const siblings = await this.prisma.originGood.findMany({
        where: { sdsCategoryId: self.sdsCategoryId, familyId: { not: null } },
        select: { familyId: true },
        distinct: ['familyId'],
      });
      familyIds = new Set(siblings.map((s) => s.familyId!.toString()));
    } else {
      const key = originGroupKey(goodName);
      if (!key) return;
      const candidates = await this.prisma.originGood.findMany({
        where: { familyId: { not: null }, goodName: { startsWith: key } },
        select: { familyId: true, goodName: true },
      });
      familyIds = new Set(
        candidates
          .filter((c) => originGroupKey(c.goodName) === key && c.familyId !== null)
          .map((c) => c.familyId!.toString()),
      );
    }
    if (familyIds.size !== 1) return;
    const familyId = BigInt([...familyIds][0]);
    const family = await this.prisma.productFamily.findUnique({
      where: { id: familyId },
      select: { autoManaged: true },
    });
    if (!family) return;
    if (family.autoManaged) {
      await this.prisma.originGood.update({
        where: { id: originGoodId },
        data: { familyId },
      });
      // Good 的族随主链接联动
      await this.prisma.good.updateMany({
        where: { originGoodId },
        data: { familyId },
      });
      this.familyRecompute.enqueue(familyId);
    } else {
      await this.prisma.productFamily.update({
        where: { id: familyId },
        data: { stale: true },
      });
    }
  }

  /**
   * Flattens the SDS nested tree into a list of `{ sdsId, parentSdsId?, name, icon? }`.
   */
  flattenCategoryTree(
    nodes: SdsCategoryTreeNode[],
    parentSdsId?: string,
  ): Array<{ sdsId: string; parentSdsId?: string; name: string; icon?: string }> {
    const out: Array<{ sdsId: string; parentSdsId?: string; name: string; icon?: string }> = [];
    const walk = (node: SdsCategoryTreeNode, parent?: string) => {
      const sdsId = String(node.id);
      if (sdsId === '' || sdsId === 'undefined' || sdsId === 'null') return;
      out.push({
        sdsId,
        parentSdsId: parent,
        name: String(node.name ?? node.title ?? sdsId),
        icon: node.icon ? String(node.icon) : undefined,
      });
      if (Array.isArray(node.children)) {
        for (const child of node.children) walk(child, sdsId);
      }
    };
    for (const root of nodes) walk(root, parentSdsId);
    return out;
  }

  private async upsertOriginGood(
    product: SdsProduct,
    sdsCategoryId: string,
  ): Promise<'inserted' | 'updated'> {
    const sdsGoodId = String(product.id);
    const existing = await this.prisma.originGood.findUnique({
      where: { sdsGoodId },
    });
    const goodName = String(product.name ?? product.title ?? sdsGoodId);
    const goodImage = product.psd_img_url
      ? String(product.psd_img_url)
      : product.blankDesignUrl
        ? String(product.blankDesignUrl)
        : product.thumbImgUrl
          ? String(product.thumbImgUrl)
          : product.show_img
            ? String(product.show_img)
            : product.img_url
              ? String(product.img_url)
              : product.pic
                ? String(product.pic)
                : product.image
                  ? String(product.image)
                  : null;
    const priceValue = product.currentPrice ?? product.price;
    let goodPrice: Prisma.Decimal | null = null;
    if (priceValue !== undefined && priceValue !== null) {
      const n = typeof priceValue === 'string' ? Number(priceValue) : priceValue;
      if (Number.isFinite(n)) {
        goodPrice = new Prisma.Decimal(n);
      }
    }
    // 链接名结构化解析列（镜像纯度：全量覆盖，含空值）
    const parsed = parseOriginName(goodName);
    const parsedData = {
      skuCode: parsed.skuCode,
      logisticsLabel: parsed.logisticsLabel,
      craftLabel: parsed.craftLabel,
      warehouseLabel: parsed.warehouseLabel,
    };
    const data: Prisma.OriginGoodUncheckedUpdateInput = {
      sdsCategoryId,
      goodName,
      goodImage,
      goodPrice,
      ...parsedData,
    };

    if (!existing) {
      const created = await this.prisma.originGood.create({
        data: {
          sdsGoodId,
          sdsCategoryId,
          goodName,
          goodImage,
          goodPrice,
          ...parsedData,
        },
        select: { id: true },
      });
      await this.tryAutoAttachToFamily(created.id, goodName);
      return 'inserted';
    }
    await this.prisma.originGood.update({
      where: { id: existing.id },
      data,
    });
    return 'updated';
  }
}
