import { randomUUID } from 'node:crypto';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Not, Repository } from 'typeorm';
import { OrderItem } from '../orders/entities/order-item.entity';
import {
  CatalogSyncJob,
  CatalogSyncTriggerType,
} from './entities/catalog-sync-job.entity';
import { Category } from './entities/category.entity';
import { Course, OrderFormField } from './entities/course.entity';
import { ThirdPartyClientService } from './third-party-client.service';

const PROVIDER = 'biedawo';
const API_ACCOUNT_ID = 'env-biedawo';

type UnknownRecord = Record<string, unknown>;

type NormalizedCategory = {
  externalId: string;
  name: string;
  sortOrder: number;
  rawPayload: UnknownRecord;
};

type NormalizedCourse = {
  externalId: string;
  remoteCategoryId: string | null;
  name: string;
  price: string;
  content: string | null;
  rawPayload: UnknownRecord;
};

type CatalogSyncOptions = {
  triggerType: CatalogSyncTriggerType;
  remoteCategoryId?: string | null;
};

type SyncStats = {
  categoryTotal: number;
  productTotal: number;
  categoryCreated: number;
  categoryUpdated: number;
  productCreated: number;
  productUpdated: number;
  productRemoved: number;
};

@Injectable()
export class ThirdPartyCatalogService {
  private readonly logger = new Logger(ThirdPartyCatalogService.name);
  private catalogSyncRunning = false;

  constructor(
    private readonly thirdPartyClientService: ThirdPartyClientService,
    @InjectRepository(CatalogSyncJob)
    private readonly catalogSyncJobsRepository: Repository<CatalogSyncJob>,
    @InjectRepository(Category)
    private readonly categoriesRepository: Repository<Category>,
    @InjectRepository(Course)
    private readonly coursesRepository: Repository<Course>,
    @InjectRepository(OrderItem)
    private readonly orderItemsRepository: Repository<OrderItem>,
  ) {}

  async syncThirdPartyCatalog(options: CatalogSyncOptions) {
    if (this.catalogSyncRunning) {
      return {
        provider: PROVIDER,
        skipped: true,
        reason: 'catalog sync is already running',
      };
    }

    this.catalogSyncRunning = true;
    const now = new Date();
    const job = this.catalogSyncJobsRepository.create({
      id: randomUUID(),
      provider: PROVIDER,
      triggerType: options.triggerType,
      status: 'running',
      startedAt: now,
      finishedAt: null,
      categoryTotal: 0,
      productTotal: 0,
      categoryCreated: 0,
      categoryUpdated: 0,
      productCreated: 0,
      productUpdated: 0,
      productRemoved: 0,
      errorMessage: null,
    });
    await this.catalogSyncJobsRepository.save(job);

    try {
      const stats = await this.syncCatalogSnapshot(
        job.id,
        now,
        options.remoteCategoryId,
      );
      Object.assign(job, {
        ...stats,
        status: 'success',
        finishedAt: new Date(),
      });
      await this.catalogSyncJobsRepository.save(job);
      return {
        provider: PROVIDER,
        jobId: job.id,
        triggerType: job.triggerType,
        status: job.status,
        ...stats,
        startedAt: job.startedAt,
        finishedAt: job.finishedAt,
      };
    } catch (error) {
      job.status = 'failed';
      job.finishedAt = new Date();
      job.errorMessage =
        error instanceof Error ? error.message : String(error);
      await this.catalogSyncJobsRepository.save(job);
      throw error;
    } finally {
      this.catalogSyncRunning = false;
    }
  }

  async syncCategories() {
    const items = await this.fetchCategories();
    const syncedAt = new Date();
    let created = 0;
    let updated = 0;

    for (const item of items) {
      const existing = await this.categoriesRepository.findOne({
        where: {
          sourceType: 'third_party',
          provider: PROVIDER,
          externalId: item.externalId,
        },
      });
      const entity = this.categoriesRepository.create({
        id: existing?.id ?? randomUUID(),
        sourceType: 'third_party',
        apiAccountId: API_ACCOUNT_ID,
        provider: PROVIDER,
        externalId: item.externalId,
        remoteCategoryId: item.externalId,
        name: item.name,
        sortOrder: item.sortOrder,
        enabled: true,
        rawPayload: item.rawPayload,
        lastSyncedAt: syncedAt,
      });
      await this.categoriesRepository.save(entity);
      if (existing) {
        updated += 1;
      } else {
        created += 1;
      }
    }

    return {
      provider: PROVIDER,
      total: items.length,
      created,
      updated,
      syncedAt,
    };
  }

  async syncCourses(remoteCategoryId?: string) {
    const items = await this.fetchCourses(remoteCategoryId);
    const syncedAt = new Date();
    let created = 0;
    let updated = 0;

    for (const item of items) {
      const category = item.remoteCategoryId
        ? await this.categoriesRepository.findOne({
            where: {
              sourceType: 'third_party',
              provider: PROVIDER,
              externalId: item.remoteCategoryId,
            },
          })
        : null;
      const existing = await this.coursesRepository.findOne({
        where: {
          sourceType: 'third_party',
          provider: PROVIDER,
          externalId: item.externalId,
          remoteCategoryId: item.remoteCategoryId ?? '',
        },
      });
      const entity = this.coursesRepository.create({
        id: existing?.id ?? randomUUID(),
        sourceType: 'third_party',
        apiAccountId: API_ACCOUNT_ID,
        provider: PROVIDER,
        externalId: item.externalId,
        categoryId: category?.id ?? existing?.categoryId ?? null,
        remoteCategoryId: item.remoteCategoryId ?? '',
        remoteCourseId: item.externalId,
        name: item.name,
        price: item.price,
        content: item.content,
        isFavorite: existing?.isFavorite ?? false,
        enabled: true,
        syncStatus: 'active',
        rawPayload: item.rawPayload,
        lastSyncedAt: syncedAt,
        lastSeenAt: syncedAt,
        removedAt: null,
      });
      await this.coursesRepository.save(entity);
      if (existing) {
        updated += 1;
      } else {
        created += 1;
      }
    }

    return {
      provider: PROVIDER,
      remoteCategoryId: remoteCategoryId || null,
      total: items.length,
      created,
      updated,
      syncedAt,
    };
  }

  async listCategories(query: Record<string, unknown>) {
    const page = Math.max(Number(query.page || 1), 1);
    const pageSize = Math.min(Math.max(Number(query.pageSize || 10), 1), 100);
    const keyword = String(query.keyword || '').trim();

    const builder = this.categoriesRepository
      .createQueryBuilder('category')
      .where('category.sourceType = :sourceType', { sourceType: 'third_party' })
      .andWhere('category.provider = :provider', { provider: PROVIDER })
      .orderBy('category.sortOrder', 'ASC')
      .addOrderBy('category.updatedAt', 'DESC')
      .skip((page - 1) * pageSize)
      .take(pageSize);

    if (keyword) {
      builder.andWhere(
        '(category.name LIKE :keyword OR category.externalId LIKE :keyword)',
        { keyword: `%${keyword}%` },
      );
    }

    const [list, total] = await builder.getManyAndCount();
    return { list, total, page, pageSize };
  }

  async listSyncJobs(query: Record<string, unknown>) {
    const page = Math.max(Number(query.page || 1), 1);
    const pageSize = Math.min(Math.max(Number(query.pageSize || 10), 1), 100);

    const [list, total] = await this.catalogSyncJobsRepository.findAndCount({
      order: { startedAt: 'DESC' },
      skip: (page - 1) * pageSize,
      take: pageSize,
    });

    return { list, total, page, pageSize };
  }

  private async syncCatalogSnapshot(
    jobId: string,
    syncedAt: Date,
    remoteCategoryId?: string | null,
  ): Promise<SyncStats> {
    const categoryItems = await this.fetchCategories();
    const scopedRemoteCategoryId = String(remoteCategoryId || '').trim();
    const syncCategoryItems = scopedRemoteCategoryId
      ? categoryItems.filter((item) => item.externalId === scopedRemoteCategoryId)
      : categoryItems;
    const stats: SyncStats = {
      categoryTotal: syncCategoryItems.length,
      productTotal: 0,
      categoryCreated: 0,
      categoryUpdated: 0,
      productCreated: 0,
      productUpdated: 0,
      productRemoved: 0,
    };

    const categoryMap = new Map<string, Category>();
    for (const [index, item] of categoryItems.entries()) {
      const existing = await this.categoriesRepository.findOne({
        where: {
          sourceType: 'third_party',
          provider: PROVIDER,
          externalId: item.externalId,
        },
      });
      const changed =
        !existing ||
        existing.name !== item.name ||
        existing.sortOrder !== item.sortOrder ||
        JSON.stringify(existing.rawPayload ?? null) !==
          JSON.stringify(item.rawPayload ?? null);
      const category = this.categoriesRepository.create({
        id: existing?.id ?? randomUUID(),
        sourceType: 'third_party',
        apiAccountId: API_ACCOUNT_ID,
        provider: PROVIDER,
        externalId: item.externalId,
        remoteCategoryId: item.externalId,
        name: item.name,
        sortOrder: item.sortOrder ?? index,
        enabled: true,
        rawPayload: item.rawPayload,
        lastSyncedAt: syncedAt,
      });
      const saved = await this.categoriesRepository.save(category);
      categoryMap.set(item.externalId, saved);
      if (existing) {
        if (changed) stats.categoryUpdated += 1;
      } else {
        stats.categoryCreated += 1;
      }
    }

    const seenKeys = new Set<string>();
    for (const category of syncCategoryItems) {
      const courses = await this.fetchCourses(category.externalId);
      stats.productTotal += courses.length;
      for (const item of courses) {
        const remoteCategoryId = item.remoteCategoryId || category.externalId;
        const key = this.getProductKey(remoteCategoryId, item.externalId);
        seenKeys.add(key);
        const localCategory = categoryMap.get(remoteCategoryId) ?? null;
        const existing = await this.coursesRepository.findOne({
          where: {
            sourceType: 'third_party',
            provider: PROVIDER,
            externalId: item.externalId,
            remoteCategoryId,
          },
        });
        const changed =
          !existing ||
          existing.name !== item.name ||
          String(existing.price) !== item.price ||
          existing.content !== item.content ||
          existing.categoryId !== (localCategory?.id ?? null) ||
          existing.syncStatus !== 'active' ||
          JSON.stringify(existing.rawPayload ?? null) !==
            JSON.stringify(item.rawPayload ?? null);
        const product = this.coursesRepository.create({
          id: existing?.id ?? randomUUID(),
          sourceType: 'third_party',
          apiAccountId: API_ACCOUNT_ID,
          provider: PROVIDER,
          externalId: item.externalId,
          categoryId: localCategory?.id ?? existing?.categoryId ?? null,
          remoteCategoryId,
          remoteCourseId: item.externalId,
          name: item.name,
          price: item.price,
          coverUrl: existing?.coverUrl ?? null,
          content: item.content,
          stock: existing?.stock ?? null,
          salesLimit: existing?.salesLimit ?? null,
          fulfillmentType: 'third_party_api',
          afterSalesNote: existing?.afterSalesNote ?? null,
          orderFormSchema: existing?.orderFormSchema ?? this.getDefaultThirdPartyOrderFormSchema(),
          sortOrder: existing?.sortOrder ?? 0,
          isFavorite: existing?.isFavorite ?? false,
          enabled: true,
          syncStatus: 'active',
          rawPayload: item.rawPayload,
          lastSyncedAt: syncedAt,
          lastSeenAt: syncedAt,
          lastSeenSyncJobId: jobId,
          removedAt: null,
        });
        await this.coursesRepository.save(product);
        if (existing) {
          if (changed) stats.productUpdated += 1;
        } else {
          stats.productCreated += 1;
        }
      }
    }

    const existingProducts = await this.coursesRepository.find({
      where: {
        sourceType: 'third_party',
        provider: PROVIDER,
        ...(scopedRemoteCategoryId
          ? { remoteCategoryId: scopedRemoteCategoryId }
          : {}),
        syncStatus: Not('removed'),
      },
    });

    for (const product of existingProducts) {
      const key = this.getProductKey(
        product.remoteCategoryId ?? '',
        product.externalId ?? product.remoteCourseId ?? '',
      );
      if (!seenKeys.has(key)) {
        await this.retireMissingProduct(product, syncedAt);
        stats.productRemoved += 1;
      }
    }

    return stats;
  }

  private async retireMissingProduct(product: Course, syncedAt: Date) {
    const hasOrders = await this.orderItemsRepository.exist({
      where: { productId: product.id },
    });
    if (!hasOrders) {
      await this.coursesRepository.remove(product);
      return;
    }

    product.enabled = false;
    product.syncStatus = 'removed';
    product.removedAt = syncedAt;
    await this.coursesRepository.save(product);
  }

  private async fetchCategories() {
    const response = await this.thirdPartyClientService.call('getcate');
    return this.extractRecords(response).map((item, index) =>
      this.normalizeCategory(item, index),
    );
  }

  private async fetchCourses(remoteCategoryId?: string) {
    const payload = remoteCategoryId ? { fenlei: remoteCategoryId } : {};
    const response = await this.thirdPartyClientService.call('getclass', payload);
    return this.extractRecords(response).map((item) =>
      this.normalizeCourse(item, remoteCategoryId),
    );
  }

  private getProductKey(remoteCategoryId: string, externalId: string) {
    return `${PROVIDER}:${remoteCategoryId}:${externalId}`;
  }

  private getDefaultThirdPartyOrderFormSchema(): OrderFormField[] {
    return [
      {
        key: 'school',
        label: '学校',
        type: 'text',
        required: true,
        placeholder: '请输入学校名称',
      },
      {
        key: 'account',
        label: '账号',
        type: 'text',
        required: true,
        placeholder: '请输入学习账号',
      },
      {
        key: 'password',
        label: '密码',
        type: 'password',
        required: true,
        placeholder: '请输入学习密码',
      },
    ];
  }

  async listCourses(query: Record<string, unknown>) {
    const page = Math.max(Number(query.page || 1), 1);
    const pageSize = Math.min(Math.max(Number(query.pageSize || 10), 1), 100);
    const keyword = String(query.keyword || '').trim();
    const remoteCategoryId = String(query.remoteCategoryId || '').trim();

    const builder = this.coursesRepository
      .createQueryBuilder('course')
      .where('course.sourceType = :sourceType', { sourceType: 'third_party' })
      .andWhere('course.provider = :provider', { provider: PROVIDER })
      .orderBy('course.updatedAt', 'DESC')
      .skip((page - 1) * pageSize)
      .take(pageSize);

    if (keyword) {
      builder.andWhere(
        '(course.name LIKE :keyword OR course.externalId LIKE :keyword)',
        { keyword: `%${keyword}%` },
      );
    }
    if (remoteCategoryId) {
      builder.andWhere('course.remoteCategoryId = :remoteCategoryId', {
        remoteCategoryId,
      });
    }

    const [list, total] = await builder.getManyAndCount();
    return { list, total, page, pageSize };
  }

  private extractRecords(response: unknown): UnknownRecord[] {
    const value = this.unwrapResponse(response);
    if (Array.isArray(value)) {
      return value.filter(this.isRecord);
    }
    if (this.isRecord(value)) {
      return Object.entries(value).map(([key, item]) =>
        this.isRecord(item) ? { id: key, ...item } : { id: key, name: item },
      );
    }
    return [];
  }

  private unwrapResponse(response: unknown): unknown {
    if (!this.isRecord(response)) {
      return response;
    }
    return response.data ?? response.rows ?? response.list ?? response.result ?? response;
  }

  private normalizeCategory(
    item: UnknownRecord,
    index: number,
  ): NormalizedCategory {
    const externalId = this.readString(
      item,
      ['id', 'cid', 'cateid', 'category_id', 'fenlei', 'value'],
      `category-${index + 1}`,
    );
    return {
      externalId,
      name: this.readString(item, ['name', 'title', 'label', 'fenlei_name'], externalId),
      sortOrder: this.readNumber(item, ['sort', 'sort_order', 'order'], index),
      rawPayload: item,
    };
  }

  private normalizeCourse(
    item: UnknownRecord,
    fallbackRemoteCategoryId?: string,
  ): NormalizedCourse {
    const externalId = this.readString(item, [
      'id',
      'kcid',
      'cid',
      'course_id',
      'class_id',
      'value',
    ]);
    return {
      externalId,
      remoteCategoryId:
        this.readOptionalString(item, [
          'fenlei',
          'category_id',
          'cateid',
          'cid',
          'sortid',
        ]) ??
        fallbackRemoteCategoryId ??
        '',
      name: this.readString(item, ['name', 'kcname', 'title', 'label'], externalId),
      price: this.readPrice(item),
      content: this.readOptionalString(item, ['content', 'desc', 'description', 'remark']),
      rawPayload: item,
    };
  }

  private readString(
    item: UnknownRecord,
    keys: string[],
    fallback?: string,
  ): string {
    return this.readOptionalString(item, keys) ?? fallback ?? randomUUID();
  }

  private readOptionalString(item: UnknownRecord, keys: string[]) {
    for (const key of keys) {
      const value = item[key];
      if (value !== undefined && value !== null && String(value).trim()) {
        return String(value).trim();
      }
    }
    return null;
  }

  private readNumber(item: UnknownRecord, keys: string[], fallback: number) {
    for (const key of keys) {
      const value = Number(item[key]);
      if (Number.isFinite(value)) {
        return value;
      }
    }
    return fallback;
  }

  private readPrice(item: UnknownRecord) {
    const value = this.readOptionalString(item, [
      'price',
      'money',
      'amount',
      'cost',
      'sell_price',
    ]);
    const numberValue = Number(value);
    return Number.isFinite(numberValue) ? numberValue.toFixed(2) : '0.00';
  }

  private isRecord(value: unknown): value is UnknownRecord {
    return !!value && typeof value === 'object' && !Array.isArray(value);
  }
}
