import { Product } from '@/entities/product.entity';
import { PublistHistory } from '@/entities/publist-history.entity';
import CoreService from '@/system/core/core-service';
import { SystemLang } from '@/system/lang/system.lang';
import AppResponse from '@/system/response/ktq-response';
import { urlToBase64 } from '@/ultils/fn';
import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer';
import { isURL } from 'class-validator';
import { fromEvent, map, Observable } from 'rxjs';
import { Repository } from 'typeorm';
import { EventsService } from '../events/events.service';
import { MediasService } from '../medias/medias.service';
import { CreateProductDto } from './dtos/create-product.dto';
import { HistoryDto } from './dtos/publist-finish.dto';
import { Request } from 'express';
import { SyncDto } from './dtos/syncs.dto';
import { Paginated } from 'nestjs-paginate';

@Injectable()
export class ProductsService extends CoreService<Product> {
  public static EVENTS = {
    SEND_PUBLIST: 'send-publist',
    SEND_DELETE: 'send-delete',
    SEND_EDIT: 'send-edit',
    PUBLIST_FINISH: 'publist-finish',
    DElETE_FINISH: 'delete-finish',
    EDIT_FINISH: 'edit-finish',
  };

  private TIMEOUT_WATING = 60000;

  constructor(
    @InjectRepository(Product)
    readonly repo: Repository<Product>,
    @InjectRepository(PublistHistory)
    readonly historiesRepo: Repository<PublistHistory>,
    private readonly mediasService: MediasService,
    private readonly eventService: EventsService,
  ) {
    super(
      repo,
      {
        sortableColumns: ['id', 'description', 'title', 'sku', 'created_at'],
        defaultSortBy: [['id', 'DESC']],
        filterableColumns: {
          id: true,
          description: true,
          title: true,
          sku: true,
          created_at: true,
        },
      },
      Product,
    );
  }

  protected async beforeIndex(data: Paginated<Product>): Promise<void> {
    for (const item of data.data) {
      const histories = await this.historiesRepo.find({
        where: { product: { id: item.id } },
        order: { created_at: 'DESC' },
      });

      item.histories = histories;
    }
  }

  protected async afterDelete(
    id: number,
    data: Partial<Product>,
    req: Request,
  ): Promise<void> {
    // Xóa images nếu có
    if (data.images?.length) {
      for (const image of data.images) {
        await this.mediasService.removeMediasFolder(
          `${data.title.toLowerCase().replaceAll(' ', '-')}/${image}` as string,
          'products',
        );
      }
    }
  }

  async mapImages(data: Product, images: string[]) {
    if (images && images.length) {
      const newImages = [];

      for (const image of images) {
        if (image.startsWith('data:')) {
          const result = await this.mediasService.saveBase64Image(
            image,
            `product-${data.id}`,
            `medias/products/${data.title.toLowerCase().replaceAll(' ', '-')}`,
          );
          newImages.push(result.filename);
        } else if (
          isURL(image, { require_host: true, require_protocol: true })
        ) {
          const result = await this.mediasService.downloadImageAndSave(
            image,
            `product-${data.id}`,
            `medias/products/${data.title.toLowerCase().replaceAll(' ', '-')}`,
          );
          newImages.push(result.filename);
        } else {
          newImages.push(image);
        }
      }

      if (newImages.length) {
        return newImages;
      }
    }

    return [];
  }

  async create(data: CreateProductDto): Promise<any> {
    const product = this.repo.create(data);

    const result = await this.repo.save({ ...product, images: [] });

    const newImages = await this.mapImages(result, data.images);

    result.images = newImages;

    await this.repo.update(result.id, result);

    return AppResponse.toResponse(result);
  }

  async update(
    id: number,
    { images, ...data }: Partial<Product> | any,
    request: Request,
  ): Promise<any> {
    const prev = await this.repo.findOne({ where: { id } });

    if (!prev)
      throw new NotFoundException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'not_found'),
        }),
      );

    const newImages = await this.mapImages(prev, images);

    data.images = [...prev.images, ...newImages];

    const result = await this.repo.update(id, data);

    if (!result) throw new BadRequestException(AppResponse.toResponse(false));

    if (data?.title) {
      await this.mediasService.renameMediasFolder(
        `${prev.title.toLowerCase().replaceAll(' ', '-')}` as string,
        'products',
        `${data.title.toLowerCase().replaceAll(' ', '-')}` as string,
      );
    }

    const product = await this.repo.findOne({ where: { id } });

    // const plainData = plainToClass(Product, product);

    // let base64Image: string[] = [];

    // try {
    //   base64Image = await this.toImagesBase64(plainData);
    // } catch (error) {
    //   console.error('Error converting images to Base64:', error);
    //   throw new BadRequestException(
    //     AppResponse.toResponse(null, {
    //       message: SystemLang.getText('messages', 'server_error'),
    //     }),
    //   );
    // }

    // this.eventService.sendEvent(ProductsService.EVENTS.SEND_EDIT, {
    //   prev: prev,
    //   data: { ...plainData, images: base64Image },
    // });

    // const { publist_id } = await this.waitForEditResult(prev, product);

    // if (publist_id && !product.publist_id) {
    //   await this.repo.update(id, { publist_id });
    // }

    return AppResponse.toResponse(product);
  }

  async rePublist(id: Product['id']) {
    const product = await this.repo.findOne({ where: { id } });

    if (!product) {
      throw new NotFoundException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'not_found'),
        }),
      );
    }

    const plainData = plainToClass(Product, product);
    let base64Image: string[] = [];

    try {
      base64Image = await this.toImagesBase64(plainData);
    } catch (error) {
      console.error('Error converting images to Base64:', error);
      throw new BadRequestException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'server_error'),
        }),
      );
    }

    this.eventService.sendEvent(ProductsService.EVENTS.SEND_EDIT, {
      prev: product,
      data: { ...plainData, images: base64Image },
    });

    const { publist_id } = await this.waitForEditResult(product, product);

    if (publist_id && !product.publist_id) {
      await this.repo.update(id, { publist_id });
    }

    return AppResponse.toResponse(plainData);
  }

  async toImagesBase64(product: Product) {
    const images = [];

    for (const image of product.images) {
      const base64 = await urlToBase64(image);

      images.push(base64);
    }

    return images;
  }

  async publist(id: Product['id']) {
    const product = await this.repo.findOne({ where: { id } });

    if (!product) {
      throw new NotFoundException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'not_found'),
        }),
      );
    }

    const plainData = plainToClass(Product, product);
    let base64Image: string[] = [];

    try {
      base64Image = await this.toImagesBase64(plainData);
    } catch (error) {
      console.error('Error converting images to Base64:', error);
      throw new BadRequestException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'server_error'),
        }),
      );
    }

    // Emit sự kiện để bắt đầu publish
    this.eventService.sendEvent(ProductsService.EVENTS.SEND_PUBLIST, {
      ...plainData,
      images: base64Image,
    });

    // Đợi phản hồi từ client
    const publistResult = await this.waitForPublistResult(plainData);

    if (!publistResult.published) {
      throw new BadRequestException(
        AppResponse.toResponse(null, {
          message: SystemLang.getCustomText({
            vi: publistResult.error,
            en: publistResult.error,
          }),
        }),
      );
    }

    await this.repo.update(plainData.id, { status: true });

    return AppResponse.toResponse(plainData);
  }

  async unlist(id: Product['id']) {
    const product = await this.repo.findOne({ where: { id } });

    if (!product) {
      throw new NotFoundException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'not_found'),
        }),
      );
    }

    const plainData = plainToClass(Product, product);

    // Emit sự kiện để bắt đầu publish
    this.eventService.sendEvent(ProductsService.EVENTS.SEND_DELETE, {
      ...plainData,
    });

    // Đợi phản hồi từ client
    const publistResult = await this.waitForDeleteResult(plainData);

    await this.repo.update(plainData.id, { status: false });

    return AppResponse.toResponse(plainData);
  }

  /**
   * Chờ event PUBLIST_FINISH và bắt lỗi nếu quá thời gian hoặc lỗi bất ngờ
   */
  private async waitForPublistResult(data: Product): Promise<PublistHistory> {
    try {
      return await this.eventService.waitForEvent<PublistHistory>(
        `${ProductsService.EVENTS.PUBLIST_FINISH}_${data.id}`,
        this.TIMEOUT_WATING,
      );
    } catch {
      throw new BadRequestException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'try_again'),
        }),
      );
    }
  }

  private async waitForDeleteResult(
    data: Partial<Product>,
  ): Promise<PublistHistory> {
    try {
      return await this.eventService.waitForEvent<PublistHistory>(
        `${ProductsService.EVENTS.DElETE_FINISH}_${data.id}`,
        this.TIMEOUT_WATING,
      );
    } catch {
      throw new BadRequestException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'try_again'),
        }),
      );
    }
  }

  private async waitForEditResult(prev: Product, data: Product): Promise<any> {
    try {
      return await this.eventService.waitForEvent<PublistHistory>(
        `${ProductsService.EVENTS.EDIT_FINISH}_${prev.id}`,
        this.TIMEOUT_WATING,
      );
    } catch {
      throw new BadRequestException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'try_again'),
        }),
      );
    }
  }

  async publistFinish(id: Product['id'], data: HistoryDto) {
    const product = await this.repo.findOne({ where: { id } });

    if (!product)
      throw new NotFoundException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'not_found'),
        }),
      );

    const plainData = plainToClass(Product, product);

    const result = await this.historiesRepo.save({ ...data, product });

    this.eventService.sendEvent(
      `${ProductsService.EVENTS.PUBLIST_FINISH}_${plainData.id}`,
      plainToClass(PublistHistory, result),
    );

    return AppResponse.toResponse(plainData);
  }

  async deleteFinish(id: Product['id'], data: HistoryDto) {
    const product = await this.repo.findOne({ where: { id } });

    if (!product)
      throw new NotFoundException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'not_found'),
        }),
      );

    const plainData = plainToClass(Product, product);

    const result = await this.historiesRepo.save({ ...data, product });

    this.eventService.sendEvent(
      `${ProductsService.EVENTS.DElETE_FINISH}_${plainData.id}`,
      plainToClass(PublistHistory, result),
    );

    return AppResponse.toResponse(plainData);
  }

  async updateFinish(id: Product['id'], { publist_id, ...data }: HistoryDto) {
    const product = await this.repo.findOne({ where: { id } });

    if (!product)
      throw new NotFoundException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'not_found'),
        }),
      );

    const plainData = plainToClass(Product, product);

    const result = await this.historiesRepo.save({
      ...data,
      product: { id: product.id },
    });

    this.eventService.sendEvent(
      `${ProductsService.EVENTS.EDIT_FINISH}_${plainData.id}`,
      { history: plainToClass(PublistHistory, result), publist_id },
    );

    return AppResponse.toResponse(plainData);
  }

  sendPublistEvents(): Observable<{ data: any }> {
    return fromEvent(
      this.eventService.event,
      ProductsService.EVENTS.SEND_PUBLIST,
    ).pipe(
      map((data) => ({
        data, // gửi data về client
      })),
    );
  }

  sendDeleteEvents(): Observable<{ data: any }> {
    return fromEvent(
      this.eventService.event,
      ProductsService.EVENTS.SEND_DELETE,
    ).pipe(
      map((data) => ({
        data, // gửi data về client
      })),
    );
  }

  sendEditEvents(): Observable<{ data: any }> {
    return fromEvent(
      this.eventService.event,
      ProductsService.EVENTS.SEND_EDIT,
    ).pipe(
      map((data) => ({
        data, // gửi data về client
      })),
    );
  }

  async sync({ items }: SyncDto) {
    // Nếu items rỗng thì disable toàn bộ sản phẩm
    if (!items.length) {
      const products = await this.repo.find({ select: ['id'] });

      if (products.length) {
        await this.repo
          .createQueryBuilder()
          .update(Product)
          .set({ status: false })
          .whereInIds(products.map((p) => p.id))
          .execute();

        await this.historiesRepo.save(
          products.map((product) => ({
            message: `Product was disabled (empty sync) at: ${new Date().toISOString()}`,
            product,
            published: false,
          })),
        );
      }

      return {
        disabled: products.length,
        enabled: 0,
        inserted: 0,
      };
    }

    const incomingTitles = new Set(items.map((d) => d.title));

    // Lấy danh sách sản phẩm hiện tại trong DB
    const products = await this.repo.find({
      select: ['id', 'title', 'status', 'price'],
    });

    const toDisable: Product[] = [];
    const toEnable: { product: Product; price: number }[] = [];
    const toInsert: { title: string; price: number }[] = [];

    const existingMap = new Map(products.map((p) => [p.title, p]));

    // Tìm sản phẩm cần disable
    for (const product of products) {
      if (!incomingTitles.has(product.title) && product.status) {
        toDisable.push(product);
      }
    }

    // Tìm sản phẩm cần enable hoặc insert mới
    for (const item of items) {
      const existing = existingMap.get(item.title);
      if (!existing) {
        toInsert.push(item);
      } else if (!existing.status || existing.price !== item.price) {
        toEnable.push({ product: existing, price: item.price });
      }
    }

    // Batch update disable
    if (toDisable.length) {
      await this.repo
        .createQueryBuilder()
        .update(Product)
        .set({ status: false })
        .whereInIds(toDisable.map((p) => p.id))
        .execute();

      await this.historiesRepo.save(
        toDisable.map((product) => ({
          message: `Product was disabled at: ${new Date().toISOString()}`,
          product,
          published: false,
        })),
      );
    }

    // Update enable + update giá + log
    for (const { product, price } of toEnable) {
      await this.repo.update(product.id, { status: true, price });
    }

    return {
      disabled: toDisable.length,
      enabled: toEnable.length,
      inserted: toInsert.length,
    };
  }
}
