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 { PublistFinishDto } from './dtos/publist-finish.dto';
import { Request } from 'express';

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

  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,
        },
        relations: {
          histories: true,
        },
      },
      Product,
    );
  }

  protected async afterDelete(
    id: number,
    data: Partial<Product>,
    req: Request,
  ): Promise<void> {
    // Xóa avatar 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 create(data: CreateProductDto): Promise<any> {
    const product = this.repo.create(data);

    if (data.images && data.images.length) {
      const newImages = [];

      for (const image of data.images) {
        if (image.startsWith('data:')) {
          const result = await this.mediasService.saveBase64Image(
            image,
            'product',
            `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',
            `medias/products/${data.title.toLowerCase().replaceAll(' ', '-')}`,
          );
          newImages.push(result.filename);
        } else {
          newImages.push(image);
        }
      }

      if (newImages.length) {
        product.images = newImages;
      }
    }

    await this.repo.save(product);

    return AppResponse.toResponse(product);
  }

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

    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}`,
        30000,
      );
    } catch {
      throw new BadRequestException(
        AppResponse.toResponse(null, {
          message: SystemLang.getText('messages', 'try_again'),
        }),
      );
    }
  }

  async publistFinish(id: Product['id'], data: PublistFinishDto) {
    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);
  }

  sendEvents(): Observable<{ data: any }> {
    // Tạo Observable dựa trên sự kiện 'send publist' của eventEmitter
    return fromEvent(
      this.eventService.event,
      ProductsService.EVENTS.SEND_PUBLIST,
    ).pipe(
      map((data) => ({
        data, // gửi data về client
      })),
    );
  }
}
