import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Category as PrismaCategory, Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
  PublicHomeGoodsQueryDto,
  PublicQueryGoodDto,
  PublicTagFilterDto,
} from './dto/public-query-good.dto';
import { PublicCategoryNodeDto } from './dto/public-category.dto';
import { PublicCountryDto } from './dto/public-country.dto';
import { PublicTagDto } from './dto/public-tag.dto';
import { PublicTagGroupDto } from './dto/public-tag-group.dto';
import { PublicGoodDto } from './dto/public-good.dto';
import {
  PublicGoodDetailDto,
  PublicTagGroupFilterDto,
} from './dto/public-good-detail.dto';

export interface PublicPaginatedGoods {
  items: PublicGoodDto[];
  total: number;
  page: number;
  pageSize: number;
}

const PUBLIC_GOOD_INCLUDE = {
  country: true,
  category: true,
  tag: { include: { tagGroup: true } },
  position: true,
  originGood: {
    include: {
      detail: true,
      variants: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
      family: {
        select: {
          id: true,
          familyCode: true,
          familyName: true,
          sizeChart: true,
          packageSpecs: true,
          priceMatrix: true,
        },
      },
    },
  },
  mergedOriginGoods: {
    orderBy: { createdAt: 'asc' as const },
    include: {
      originGood: {
        include: {
          variants: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
        },
      },
    },
  },
  goodTags: { include: { tag: { include: { tagGroup: true } } } },
} satisfies Prisma.GoodInclude;

type PublicGoodRow = Prisma.GoodGetPayload<{ include: typeof PUBLIC_GOOD_INCLUDE }>;

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

  async getCategoriesTree(countryId?: string): Promise<PublicCategoryNodeDto[]> {
    const goodsWhere: Prisma.GoodWhereInput = {
      originGood: { delisted: false },
      ...(countryId ? { countryId: BigInt(countryId) } : {}),
    };
    const leafCategories = await this.prisma.category.findMany({
      where: { goods: { some: goodsWhere } },
      orderBy: { id: 'asc' },
    });
    const ancestorIds = new Set<bigint>();
    for (const leaf of leafCategories) {
      let cursor: bigint | null = leaf.parentCategoryId;
      while (cursor !== null && !ancestorIds.has(cursor)) {
        ancestorIds.add(cursor);
        const parent = await this.prisma.category.findUnique({
          where: { id: cursor },
          select: { id: true, parentCategoryId: true },
        });
        if (!parent) break;
        cursor = parent.parentCategoryId;
      }
    }
    const ancestorRows = ancestorIds.size
      ? await this.prisma.category.findMany({
          where: { id: { in: [...ancestorIds] } },
          orderBy: { id: 'asc' },
        })
      : [];
    const allRows = [...leafCategories, ...ancestorRows].filter(
      (row, index, rows) => rows.findIndex((item) => item.id === row.id) === index,
    );
    allRows.sort((a, b) => Number(a.id - b.id));
    const directCounts = await this.prisma.good.groupBy({
      by: ['categoryId'],
      where: goodsWhere,
      _count: { _all: true },
    });
    return this.buildTree(
      allRows,
      new Map(directCounts.map((row) => [row.categoryId, row._count._all])),
    );
  }

  async getCountries(): Promise<PublicCountryDto[]> {
    const rows = await this.prisma.country.findMany({
      where: { goods: { some: { originGood: { delisted: false } } } },
      orderBy: { id: 'asc' },
    });
    return rows.map(PublicCountryDto.from);
  }

  async getTags(): Promise<PublicTagDto[]> {
    const rows = await this.prisma.tag.findMany({
      where: { goodTags: { some: { good: { originGood: { delisted: false } } } } },
      orderBy: [
        { tagGroup: { sortOrder: 'asc' } },
        { sortOrder: 'asc' },
        { id: 'asc' },
      ],
      include: { tagGroup: true },
    });
    return rows.map(PublicTagDto.from);
  }

  async getTagGroups(countryId?: string): Promise<PublicTagGroupFilterDto[]> {
    const goodWhere: Prisma.GoodWhereInput = {
      originGood: { delisted: false },
      ...(countryId ? { countryId: BigInt(countryId) } : {}),
    };
    const rows = await this.prisma.tagGroup.findMany({
      where: { tags: { some: { goodTags: { some: { good: goodWhere } } } } },
      orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
      include: {
        tags: {
          where: { goodTags: { some: { good: goodWhere } } },
          orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
          include: {
            _count: { select: { goodTags: { where: { good: goodWhere } } } },
          },
        },
      },
    });
    return rows.map((group) => ({
      ...PublicTagGroupDto.from(group),
      tags: group.tags.map((tag) => ({
        id: tag.id.toString(),
        tagName: tag.tagName,
        tagColor: tag.tagColor,
        tagFontColor: tag.tagFontColor,
        sortOrder: tag.sortOrder,
        productCount: tag._count.goodTags,
      })),
    }));
  }

  async getGoods(query: PublicQueryGoodDto): Promise<PublicPaginatedGoods> {
    const where: Prisma.GoodWhereInput = { originGood: { delisted: false } };
    if (query.countryId) where.countryId = BigInt(query.countryId);
    if (query.keyword) where.goodName = { contains: query.keyword, mode: 'insensitive' };
    if (query.categoryId) {
      where.categoryId = { in: await this.collectCategoryDescendants(BigInt(query.categoryId)) };
    }

    const tagFilters = await this.buildTagGroupFilters(query.tags ?? []);
    if (tagFilters.length) where.AND = tagFilters;

    const minPrice = this.parsePrice(query.minPrice, 'minPrice');
    const maxPrice = this.parsePrice(query.maxPrice, 'maxPrice');
    if (minPrice !== null && maxPrice !== null && minPrice > maxPrice) {
      throw new BadRequestException('minPrice 不能大于 maxPrice');
    }
    if (minPrice !== null || maxPrice !== null) {
      where.originGood = {
        delisted: false,
        goodPrice: {
          ...(minPrice !== null ? { gte: minPrice } : {}),
          ...(maxPrice !== null ? { lte: maxPrice } : {}),
        },
      };
    }

    const orderBy: Prisma.GoodOrderByWithRelationInput[] =
      query.sort === 'PRICE_ASC'
        ? [{ originGood: { goodPrice: 'asc' } }, { id: 'asc' }]
        : query.sort === 'PRICE_DESC'
          ? [{ originGood: { goodPrice: 'desc' } }, { id: 'asc' }]
          : query.sort === 'NEWEST'
            ? [{ createdAt: 'desc' }, { id: 'asc' }]
            : [
                { goodPriority: 'desc' },
                { position: { indexVal: 'asc' } },
                { createdAt: 'desc' },
                { id: 'asc' },
              ];

    const [total, rows] = await this.prisma.$transaction([
      this.prisma.good.count({ where }),
      this.prisma.good.findMany({
        where,
        include: PUBLIC_GOOD_INCLUDE,
        orderBy,
        skip: (query.page - 1) * query.pageSize,
        take: query.pageSize,
      }),
    ]);
    return {
      items: rows.map((good) => this.toPublicGood(good)),
      total,
      page: query.page,
      pageSize: query.pageSize,
    };
  }

  async getGood(goodId: string): Promise<PublicGoodDetailDto> {
    const good = await this.prisma.good.findFirst({
      where: {
        OR: [
          { originGood: { sdsGoodId: goodId, delisted: false } },
          // 族内任何成员链接均可命中同一商品（替代旧副源关联的可达性语义）
          { family: { originGoods: { some: { sdsGoodId: goodId, delisted: false } } } },
          // 历史副源关联（good_origin_goods）只读保留，仍可命中
          {
            mergedOriginGoods: {
              some: { originGood: { sdsGoodId: goodId, delisted: false } },
            },
          },
        ],
      },
      include: PUBLIC_GOOD_INCLUDE,
      orderBy: [{ goodPriority: 'desc' }, { id: 'asc' }],
    });
    if (!good) {
      throw new NotFoundException({ message: '不存在商品', error: 'PRODUCT_NOT_FOUND' });
    }
    // 族机制（新）：变体并集 = 主链接 ∪ 族成员 ∪ 旧副源（过渡期），按 (链接, 变体) 去重
    let familyVariants: Array<{
      originGoodId: bigint;
      variant: PublicGoodRow['originGood']['variants'][number];
    }> = [];
    const familyId = good.originGood.family?.id;
    if (familyId) {
      const members = await this.prisma.originGood.findMany({
        where: { familyId, delisted: false },
        select: {
          id: true,
          variants: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] },
        },
      });
      familyVariants = members
        .filter((m) => m.id !== good.originGoodId)
        .flatMap((m) => m.variants.map((variant) => ({ originGoodId: m.id, variant })));
    }
    const dto = this.toPublicGoodDetail(good, familyVariants);
    dto.category.categoryIcon = await this.resolveCategoryIcon(good.category);
    return dto;
  }

  async getHomeGoods(query: PublicHomeGoodsQueryDto): Promise<PublicGoodDto[]> {
    const rows = await this.prisma.good.findMany({
      where: {
        positionId: { not: null },
        originGood: { delisted: false },
        ...(query.countryId ? { countryId: BigInt(query.countryId) } : {}),
      },
      include: PUBLIC_GOOD_INCLUDE,
      orderBy: [
        { position: { indexVal: 'asc' } },
        { goodPriority: 'desc' },
        { id: 'asc' },
      ],
      take: query.limit,
    });
    return rows.map((good) => this.toPublicGood(good));
  }

  private toPublicGood(good: PublicGoodRow): PublicGoodDto {
    const formatGroup = (group: { id: bigint; groupName: string; sortOrder: number } | null) =>
      group
        ? { id: group.id.toString(), groupName: group.groupName, sortOrder: group.sortOrder }
        : null;
    return {
      goodId: good.originGood.sdsGoodId,
      goodName: good.goodName,
      goodPriority: good.goodPriority,
      country: {
        id: good.country.id.toString(),
        countryName: good.country.countryName,
        countryIcon: good.country.countryIcon,
      },
      category: {
        id: good.category.id.toString(),
        categoryName: good.category.categoryName,
        categoryIcon: good.category.categoryIcon,
      },
      tag: good.tag
        ? {
            id: good.tag.id.toString(),
            tagName: good.tag.tagName,
            tagColor: good.tag.tagColor,
            tagFontColor: good.tag.tagFontColor,
            group: formatGroup(good.tag.tagGroup),
          }
        : null,
      tags: good.goodTags.map(({ tag }) => ({
        id: tag.id.toString(),
        tagName: tag.tagName,
        tagColor: tag.tagColor,
        tagFontColor: tag.tagFontColor,
        group: formatGroup(tag.tagGroup),
      })),
      position: good.position
        ? { id: good.position.id.toString(), indexVal: good.position.indexVal }
        : null,
      image: good.goodImage ?? good.originGood.goodImage,
      price: good.originGood.goodPrice?.toString() ?? null,
      createdAt: good.createdAt.toISOString(),
    };
  }

  /** Group distinct variant images by color so the frontend can switch media per color.
   *  Only color-specific photos are included (main / result / detail images);
   *  design-layer素材图 and the product-level blank garment photo are excluded
   *  because they are not per-color gallery photos. */
  private groupImagesByColor(
    variants: Array<PublicGoodRow['originGood']['variants'][number]>,
  ): Array<{ colorId: string | null; colorName: string | null; colorHex: string | null; images: string[] }> {
    const groups = new Map<string, {
      colorId: string | null;
      colorName: string | null;
      colorHex: string | null;
      images: string[];
    }>();
    for (const variant of variants) {
      const key = variant.colorId ?? `variant:${variant.sdsVariantId}`;
      let group = groups.get(key);
      if (!group) {
        group = {
          colorId: variant.colorId,
          colorName: variant.colorName,
          colorHex: variant.colorHex,
          images: [],
        };
        groups.set(key, group);
      }
      const design = (variant.designData ?? {}) as {
        detailImgUrls?: Array<{ imageUrl?: unknown }>;
        prototypeResultGroups?: Array<{ resultImage?: unknown }>;
      };
      const urls: unknown[] = [
        variant.imageUrl,
        ...(design.prototypeResultGroups ?? []).map((item) => item?.resultImage),
        ...(design.detailImgUrls ?? []).map((image) => image?.imageUrl),
      ];
      for (const url of urls) {
        const value = typeof url === 'string' ? url.trim() : '';
        if (value && !group.images.includes(value)) {
          group.images.push(value);
        }
      }
    }
    return [...groups.values()];
  }

  /** Leaf categories often have no icon upstream; fall back to the nearest ancestor that has one. */
  private async resolveCategoryIcon(category: PublicGoodRow['category']): Promise<string | null> {
    if (category.categoryIcon) return category.categoryIcon;
    let cursor = category.parentCategoryId;
    for (let depth = 0; cursor !== null && depth < 10; depth++) {
      const parent = await this.prisma.category.findUnique({
        where: { id: cursor },
        select: { categoryIcon: true, parentCategoryId: true },
      });
      if (!parent) break;
      if (parent.categoryIcon) return parent.categoryIcon;
      cursor = parent.parentCategoryId;
    }
    return null;
  }

  private toPublicGoodDetail(
    good: PublicGoodRow,
    familyVariants: Array<{
      originGoodId: bigint;
      variant: PublicGoodRow['originGood']['variants'][number];
    }> = [],
  ): PublicGoodDetailDto {
    const base = this.toPublicGood(good);
    const detail = good.originGood.detail;
    // 变体并集：主链接 ∪ 族成员（新机制）∪ 旧副源（过渡期）；
    // 同一链接可能既是族成员又挂旧副源，按 `${originGoodId}:${sdsVariantId}` 去重。
    const seen = new Set<string>(['']);
    const dedupe = (originGoodId: bigint, variant: PublicGoodRow['originGood']['variants'][number]) => {
      const key = `${originGoodId}:${variant.sdsVariantId}`;
      if (seen.has(key)) return null;
      seen.add(key);
      return variant;
    };
    good.originGood.variants.forEach((v) => dedupe(good.originGoodId, v));
    const allVariants = [
      ...good.originGood.variants,
      ...familyVariants
        .map(({ originGoodId, variant }) => dedupe(originGoodId, variant))
        .filter((v): v is PublicGoodRow['originGood']['variants'][number] => v !== null),
      ...good.mergedOriginGoods.flatMap((m) =>
        m.originGood.variants
          .map((variant) => dedupe(m.originGoodId, variant))
          .filter((v): v is PublicGoodRow['originGood']['variants'][number] => v !== null),
      ),
    ];
    return {
      ...base,
      productCode: detail?.productCode ?? null,
      englishName: detail?.englishName ?? null,
      productionCycleHours: detail?.productionCycleHours ?? null,
      minWeightG: detail?.minWeightG?.toString() ?? null,
      details: {
        reminder: detail?.reminder ?? null,
        productionProcess: detail?.productionProcess ?? null,
        materialDescription: detail?.materialDescription ?? null,
        productPerformance: detail?.productPerformance ?? null,
        applicableScenarios: detail?.applicableScenarios ?? null,
        washingInstructions: detail?.washingInstructions ?? null,
        specialDescription: detail?.specialDescription ?? null,
        designExplanation: detail?.designExplanation ?? null,
        designArea: detail?.designArea ?? null,
        pictureRequest: detail?.pictureRequest ?? null,
      },
      media: (detail?.media as Record<string, unknown> | null) ?? null,
      mediaByColor: this.groupImagesByColor(allVariants),
      options: (detail?.options as Record<string, unknown> | null) ?? null,
      sizeChart: (detail?.sizeChart as Record<string, unknown> | null) ?? null,
      packageSpecs: (detail?.packageSpecs as Record<string, unknown> | null) ?? null,
      variants: allVariants.map((variant) => ({
        id: variant.sdsVariantId,
        sku: variant.sku,
        sizeId: variant.sizeId,
        sizeName: variant.sizeName,
        colorId: variant.colorId,
        colorName: variant.colorName,
        colorHex: variant.colorHex,
        imageUrl: variant.imageUrl,
        price: variant.price?.toString() ?? null,
        originalPrice: variant.originalPrice?.toString() ?? null,
        weightG: variant.weightG?.toString() ?? null,
        boxLengthCm: variant.boxLengthCm?.toString() ?? null,
        boxWidthCm: variant.boxWidthCm?.toString() ?? null,
        boxHeightCm: variant.boxHeightCm?.toString() ?? null,
        enabled: variant.enabled,
        sortOrder: variant.sortOrder,
      })),
      detailSyncedAt: detail?.syncedAt.toISOString() ?? null,
      ...this.familyBlock(good),
    };
  }

  /**
   * 族块（设计 D4：零新增公开端点）：默认输出；仅当 PUBLIC_DETAIL_FROM_FAMILY
   * 显式设为 'false' 时关闭（应急回退开关）。数据全部来自 ProductFamily 的物化 JSON。
   */
  private familyBlock(
    good: PublicGoodRow,
  ): Pick<PublicGoodDetailDto, 'family'> | Record<string, never> {
    if (process.env.PUBLIC_DETAIL_FROM_FAMILY === 'false') return {};
    const family = good.originGood.family;
    if (!family || !family.priceMatrix) return {};
    const matrix = family.priceMatrix as {
      sizes: Array<{ key: string; name: string | null }>;
      colors: Array<{ key: string; name: string | null; hex: string | null; imageUrl: string | null }>;
      crafts: string[];
      logistics: string[];
      rows: Array<{ price: string }>;
    };
    const prices = matrix.rows.map((r) => Number(r.price)).filter((n) => Number.isFinite(n));
    return {
      family: {
        familyId: family.id.toString(),
        familyCode: family.familyCode,
        familyName: family.familyName,
        sizes: matrix.sizes,
        colors: matrix.colors,
        crafts: matrix.crafts,
        logistics: matrix.logistics,
        sizeChart: (family.sizeChart as Record<string, unknown> | null) ?? null,
        packageSpecs: (family.packageSpecs as Record<string, unknown> | null) ?? null,
        priceMatrix: matrix as unknown as Record<string, unknown>,
        minPrice: prices.length ? String(Math.min(...prices)) : null,
      },
    };
  }

  private async buildTagGroupFilters(
    selectedGroups: PublicTagFilterDto[],
  ): Promise<Prisma.GoodWhereInput[]> {
    const selections = selectedGroups.flatMap((group) => {
      if (!/^\d+$/.test(group.tagGroupId) || !Array.isArray(group.tagIds)) {
        throw new BadRequestException(
          'tags 每个元素必须包含合法的 tagGroupId 和 tagIds',
        );
      }
      return group.tagIds.map((tagId) => {
        if (!/^\d+$/.test(tagId)) {
          throw new BadRequestException('tagIds 必须全部为数字字符串');
        }
        return { tagGroupId: group.tagGroupId, tagId };
      });
    });
    const uniqueTagIds = [...new Set(selections.map((item) => item.tagId))];
    const selected = uniqueTagIds.length
      ? await this.prisma.tag.findMany({
          where: { id: { in: uniqueTagIds.map((id) => BigInt(id)) } },
          select: { id: true, tagGroupId: true },
        })
      : [];
    if (selected.length !== uniqueTagIds.length) {
      throw new BadRequestException('包含不存在的标签 ID');
    }
    const actualGroups = new Map(
      selected.map((tag) => [
        tag.id.toString(),
        tag.tagGroupId?.toString() ?? null,
      ]),
    );
    for (const selection of selections) {
      if (actualGroups.get(selection.tagId) !== selection.tagGroupId) {
        throw new BadRequestException(
          `标签 ${selection.tagId} 不属于标签组 ${selection.tagGroupId}`,
        );
      }
    }
    const byGroup = new Map<string, bigint[]>();
    for (const selection of selections) {
      const key = selection.tagGroupId;
      const ids = byGroup.get(key) ?? [];
      const id = BigInt(selection.tagId);
      if (!ids.includes(id)) ids.push(id);
      byGroup.set(key, ids);
    }
    return [...byGroup.values()].map((ids) => ({
      goodTags: { some: { tagId: { in: ids } } },
    }));
  }

  private parsePrice(value: string | undefined, field: string): number | null {
    if (value === undefined || value === '') return null;
    const parsed = Number(value);
    if (!Number.isFinite(parsed) || parsed < 0) {
      throw new BadRequestException(`${field} 必须是大于等于 0 的金额`);
    }
    return parsed;
  }

  private async collectCategoryDescendants(rootId: bigint): Promise<bigint[]> {
    const ids: bigint[] = [rootId];
    let frontier: bigint[] = [rootId];
    while (frontier.length) {
      const children = await this.prisma.category.findMany({
        where: { parentCategoryId: { in: frontier } },
        select: { id: true },
      });
      if (!children.length) break;
      frontier = children.map((child) => child.id);
      ids.push(...frontier);
    }
    return ids;
  }

  private buildTree(
    rows: PrismaCategory[],
    directCounts: Map<bigint, number>,
  ): PublicCategoryNodeDto[] {
    const byId = new Map<bigint, PublicCategoryNodeDto>();
    for (const row of rows) {
      const node = PublicCategoryNodeDto.from(row, []);
      node.productCount = directCounts.get(row.id) ?? 0;
      byId.set(row.id, node);
    }
    const roots: PublicCategoryNodeDto[] = [];
    for (const row of rows) {
      const node = byId.get(row.id)!;
      const parent = row.parentCategoryId === null ? null : byId.get(row.parentCategoryId);
      if (parent) parent.children.push(node);
      else roots.push(node);
    }
    const total = (node: PublicCategoryNodeDto): number => {
      node.productCount += node.children.reduce((sum, child) => sum + total(child), 0);
      return node.productCount;
    };
    roots.forEach(total);
    return roots;
  }
}
