import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { SdsClientService, SdsCategoryTreeNode, SdsProduct } from './sds-client.service';

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

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

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

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

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

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

      let inserted = 0;
      let updated = 0;
      for (const node of flat) {
        const existing = await this.prisma.category.findUnique({
          where: { sdsCategoryId: node.sdsId },
        });
        if (!existing) {
          await this.prisma.category.create({
            data: {
              sdsCategoryId: node.sdsId,
              categoryName: node.name,
              categoryIcon: node.icon ?? null,
            },
          });
          inserted++;
        } else {
          await this.prisma.category.update({
            where: { id: existing.id },
            data: {
              categoryName: node.name,
              categoryIcon: node.icon ?? null,
            },
          });
          updated++;
        }
      }

      // Second pass: wire up parents by sdsCategoryId.
      for (const node of flat) {
        if (!node.parentSdsId) continue;
        const child = await this.prisma.category.findUnique({
          where: { sdsCategoryId: node.sdsId },
        });
        const parent = await this.prisma.category.findUnique({
          where: { sdsCategoryId: node.parentSdsId },
        });
        if (child && parent && child.parentCategoryId !== parent.id) {
          await this.prisma.category.update({
            where: { id: child.id },
            data: { parentCategoryId: parent.id },
          });
        }
      }

      await this.prisma.syncLog.update({
        where: { id: log.id },
        data: {
          status: 'SUCCESS',
          finishedAt: new Date(),
          message: `inserted=${inserted} updated=${updated} total=${flat.length}`,
        },
      });
      return { inserted, updated, total: flat.length };
    } 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;

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

      await this.prisma.syncLog.update({
        where: { id: log.id },
        data: {
          status: 'SUCCESS',
          finishedAt: new Date(),
          message: `inserted=${inserted} updated=${updated} total=${total} leafCategories=${leafRows.length}`,
        },
      });
      return { inserted, updated, total, leafCategories: leafRows.length };
    } 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,
    });
  }

  /**
   * 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 data: Prisma.OriginGoodUncheckedUpdateInput = {
      sdsCategoryId,
      goodName,
      goodImage,
      goodPrice,
    };

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