import { Injectable, NotFoundException } from '@nestjs/common';
import { Category as PrismaCategory, Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { PublicQueryGoodDto } 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';

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: true,
  goodTags: { include: { tag: { include: { tagGroup: true } } } },
} satisfies Prisma.GoodInclude;

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

  async getCategoriesTree(): Promise<PublicCategoryNodeDto[]> {
    const leafCategories = await this.prisma.category.findMany({
      where: { goods: { some: {} } },
      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 > 0
      ? await this.prisma.category.findMany({
          where: { id: { in: [...ancestorIds] } },
          orderBy: { id: 'asc' },
        })
      : [];
    const allRows = [...leafCategories, ...ancestorRows].filter(
      (row, idx, arr) => arr.findIndex((r) => r.id === row.id) === idx,
    );
    allRows.sort((a, b) => Number(a.id - b.id));
    return this.buildTree(allRows);
  }

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

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

  async getTagGroups(): Promise<PublicTagGroupDto[]> {
    const rows = await this.prisma.tagGroup.findMany({
      where: { tags: { some: { goodTags: { some: {} } } } },
      orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
    });
    return rows.map(PublicTagGroupDto.from);
  }

  async getGoods(query: PublicQueryGoodDto): Promise<PublicPaginatedGoods> {
    const where: Prisma.GoodWhereInput = {};
    if (query.countryId !== undefined) where.countryId = BigInt(query.countryId);
    if (query.tagIds) {
      const ids = query.tagIds
        .split(',')
        .map((s) => s.trim())
        .filter(Boolean)
        .map((s) => BigInt(s));
      if (ids.length > 0) {
        // AND logic: 商品必须同时具备所有选中的 tag
        where.AND = ids.map((id) => ({ goodTags: { some: { tagId: id } } }));
      }
    }
    if (query.keyword) {
      where.goodName = { contains: query.keyword, mode: 'insensitive' };
    }
    if (query.categoryId !== undefined) {
      const ids = await this.collectCategoryDescendants(BigInt(query.categoryId));
      where.categoryId = { in: ids };
    }

    const [total, rows] = await this.prisma.$transaction([
      this.prisma.good.count({ where }),
      this.prisma.good.findMany({
        where,
        include: PUBLIC_GOOD_INCLUDE,
        // Server-side primary sort; PublicGoodDto retains original indexes
        // for stable pagination but the final ORDER BY is mirrored below.
        orderBy: [
          { goodPriority: 'desc' },
          { position: { indexVal: 'asc' } },
          { createdAt: 'desc' },
        ],
        skip: (query.page - 1) * query.pageSize,
        take: query.pageSize,
      }),
    ]);

    return {
      items: rows.map((g) => this.toPublicGood(g)),
      total,
      page: query.page,
      pageSize: query.pageSize,
    };
  }

  async getGood(id: bigint): Promise<PublicGoodDto> {
    const good = await this.prisma.good.findUnique({
      where: { id },
      include: PUBLIC_GOOD_INCLUDE,
    });
    if (!good) throw new NotFoundException(`Good ${id} not found`);
    return this.toPublicGood(good);
  }

  private toPublicGood(good: {
    id: bigint;
    goodName: string;
    goodImage: string | null;
    goodPriority: number;
    country: { id: bigint; countryName: string; countryIcon: string | null };
    category: { id: bigint; categoryName: string; categoryIcon: string | null };
    tag: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null; tagGroup: { id: bigint; groupName: string; sortOrder: number } | null } | null;
    position: { id: bigint; indexVal: number } | null;
    originGood: {
      goodImage: string | null;
      goodPrice: { toString(): string } | null;
    } | null;
    goodTags: { tag: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null; tagGroup: { id: bigint; groupName: string; sortOrder: number } | null } }[];
    createdAt: Date;
  }): PublicGoodDto {
    const formatGroup = (g: { id: bigint; groupName: string; sortOrder: number } | null) =>
      g
        ? {
            id: g.id.toString(),
            groupName: g.groupName,
            sortOrder: g.sortOrder,
          }
        : null;
    return {
      id: good.id.toString(),
      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((gt) => ({
        id: gt.tag.id.toString(),
        tagName: gt.tag.tagName,
        tagColor: gt.tag.tagColor,
        tagFontColor: gt.tag.tagFontColor,
        group: formatGroup(gt.tag.tagGroup),
      })),
      position: good.position
        ? {
            id: good.position.id.toString(),
            indexVal: good.position.indexVal,
          }
        : null,
      image: good.goodImage ?? good.originGood?.goodImage ?? null,
      price:
        good.originGood?.goodPrice === null ||
        good.originGood?.goodPrice === undefined
          ? null
          : good.originGood.goodPrice.toString(),
      createdAt: good.createdAt.toISOString(),
    };
  }

  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 buildTree(
    rows: PrismaCategory[],
  ): PublicCategoryNodeDto[] {
    const byId = new Map<bigint, PublicCategoryNodeDto>();
    for (const row of rows) {
      byId.set(row.id, PublicCategoryNodeDto.from(row, []));
    }
    const roots: PublicCategoryNodeDto[] = [];
    for (const row of rows) {
      const node = byId.get(row.id)!;
      if (row.parentCategoryId === null) {
        roots.push(node);
      } else {
        const parent = byId.get(row.parentCategoryId);
        if (parent) parent.children.push(node);
        else roots.push(node);
      }
    }
    return roots;
  }
}