import { NOT_FOUND_FILE, NOT_FOUND_MONTH } from '@api/constants/admin.constant';
import {
  NOT_FOUND_INPUT,
  NOT_FOUND_REQUIRED_PARAMETER,
} from '@api/constants/api.constant';
import { generateDayRange, generateMonthRange } from '@api/helpers/date.helper';
import { formatDateRange } from '@api/helpers/format.helper';
import { isValidateFailed } from '@api/helpers/validation-helper';
import { mysqlErrorWrapper } from '@lib/drizzle/src/drizzle.utill';
import { S3Service } from '@lib/s3/src';
import { SeparatorService } from '@lib/separator/src';
import { BadRequestException, Injectable } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';
import dayjs from 'dayjs';
import { MemberType } from '../fc/fc.entity';
import { FcService } from '../fc/fc.service';
import {
  AdvertisementImageDto,
  CreateAdvertisementDto,
  DeleteAdvertisementDto,
  GetCommonAdvertisementListDto,
  GetUserAdvertisementListDto,
  IncreseAdvertisementDto,
  UpdateAdvertisementDto,
} from './advertisement.dto';
import { AdsType } from './advertisement.entity';
import { AdvertisementRepository } from './advertisement.repository';

@Injectable()
export class AdvertisementService {
  constructor(
    private advertisementRepository: AdvertisementRepository,
    private s3Service: S3Service,
    private separatorService: SeparatorService,
    private fcService: FcService,
  ) {}

  async getAdminList(dto: GetCommonAdvertisementListDto) {
    const where = await this.advertisementRepository.whereClause(dto);
    const orderBy = await this.advertisementRepository.orderByClause(dto);
    const list = await this.advertisementRepository.getAdminList(
      where,
      orderBy,
      dto.page,
    );
    const count = await this.advertisementRepository.count(where);

    return {
      count,
      list,
    };
  }

  async getDetail(adsId: number) {
    return await this.advertisementRepository.getDetail(adsId);
  }

  async createAdvertisement(
    advertisementString: string,
    files: Express.Multer.File[],
  ) {
    let image;

    const advertisement = await this.validate(advertisementString);

    //* 이미지 타입일 경우 업로드 로직
    if (advertisement.adsType === AdsType.이미지) {
      if (Object.keys(files).length === 0) {
        throw new BadRequestException(NOT_FOUND_FILE);
      }

      image = await this.uploadImage(files);
    }

    return await this.advertisementRepository
      .create(advertisement, image)
      .catch((error) => {
        mysqlErrorWrapper(error);
      });
  }

  async updateAdvertisement(
    adsId: number,
    advertisementString: string,
    files: Express.Multer.File[],
  ) {
    const { advertisement, image } =
      await this.updateValidate(advertisementString);
    const updateImage = await this.getUpdateImage(image, files);

    return await this.advertisementRepository
      .update(adsId, advertisement, updateImage)
      .catch((error) => {
        mysqlErrorWrapper(error);
      });
  }

  async deleteAdvertisement(dto: DeleteAdvertisementDto) {
    return await this.advertisementRepository.delete(dto.ids);
  }

  //* 광고 통계 조회
  async getAdvertisementStatis(
    adsId: number,
    dateType: 'month' | 'day',
    year: string,
    month?: string,
  ) {
    if (dateType === 'day' && !month) {
      throw new BadRequestException(NOT_FOUND_MONTH);
    }

    const date = year + dateType === 'day' ? `-${month}` : '';

    const { startDate, endDate } = formatDateRange(dateType, date);

    // 기간 내 광고통계 조회
    const result = await this.advertisementRepository.getStatistics(
      adsId,
      dateType,
      startDate,
      endDate,
    );

    const allDates =
      dateType === 'month'
        ? generateMonthRange(startDate, endDate)
        : generateDayRange(startDate, endDate);

    const list = await this.buildAdvertisementStatisList(
      dateType,
      result,
      allDates,
    );

    return {
      statisticsList: list,
    };
  }

  //* 요청개수에 따라 고정 광고 우선적으로 채워넣고 남은 광고를 랜덤 광고로 채워넣는 방식
  async getUserList(dto: GetUserAdvertisementListDto) {
    let member;

    if (dto.memId) {
      member = await this.fcService.getAgencyIdByMember(dto.memId);
    } else if (dto.altCode) {
      member = await this.fcService.getAgencyByAltCode(dto.altCode);
    } else {
      throw new BadRequestException(NOT_FOUND_REQUIRED_PARAMETER);
    }

    if (member.memType === MemberType.기업FC) {
      let list = await this.getUserAdvertiesList(dto, member.agcId);

      if (list.length === 0) {
        list = await this.getUserAdvertiesList(dto, null);
      }

      return {
        list,
      };
    } else {
      const list = await this.getUserAdvertiesList(dto, null);

      return {
        list,
      };
    }
  }

  //* 광고 동영상/이미지 클릭시 카운트 증가
  async updateIncrese(dto: IncreseAdvertisementDto) {
    return await this.advertisementRepository
      .updateIncrese(dto)
      .catch((error) => {
        mysqlErrorWrapper(error);
      });
  }

  //*------------------------------------- vaildate / s3 upload -------------------------------------*/

  async updateValidate(advertisementString: string) {
    const dto = plainToInstance(
      UpdateAdvertisementDto,
      JSON.parse(advertisementString),
    );

    const validationErrors = await validate(dto);
    if (isValidateFailed(validationErrors)) {
      const { customError, description } = makeCustomErrorWithDescription(
        validationErrors[0],
      );
      throw new BadRequestException(customError, { description });
    }

    return this.separatorService.advertisement(dto);
  }

  //* 광고 이미지 데이터
  async uploadImage(files: Express.Multer.File[]) {
    const uploadImage: AdvertisementImageDto = {
      adsImg: '',
    };

    return this.s3ImageUpload(files, uploadImage);
  }

  //* s3 이미지 업로드
  async s3ImageUpload(
    files: Express.Multer.File[],
    uploadImage: AdvertisementImageDto,
  ) {
    const images = Object.entries(files).map(([key, value]) => value[0]);

    await Promise.all(
      images.map(async (image) => {
        const { fieldname, buffer, originalname } = image;
        const fileName = this.s3Service.encFileName(originalname);
        const imageKey = await this.s3Service.imageUpload(
          fileName,
          buffer,
          'advertisement/images/',
        );

        if (fieldname in uploadImage && imageKey) {
          uploadImage[fieldname] = imageKey;
        }
      }),
    ).catch((error) => {
      throw new BadRequestException(error);
    });

    return uploadImage;
  }

  private async getUpdateImage(
    image: AdvertisementImageDto,
    files: Express.Multer.File[],
  ): Promise<AdvertisementImageDto> {
    if (!files || Object.keys(files).length === 0) {
      return { adsImg: '' };
    }

    const existFileName = image.adsImg?.replace('advertisement/images/', '');
    const [requestFileName] = Object.entries(files).map(
      ([key, value]) => value[0].originalname,
    );

    const isExistingImage = existFileName === requestFileName;

    return isExistingImage ? image : await this.s3ImageUpload(files, image);
  }

  async validate(advertisementString: string) {
    const advertisement = plainToInstance(
      CreateAdvertisementDto,
      JSON.parse(advertisementString),
    );

    const validationErrors = await validate(advertisement);
    if (isValidateFailed(validationErrors)) {
      const { customError, description } = makeCustomErrorWithDescription(
        validationErrors[0],
      );
      throw new BadRequestException(customError, { description });
    }
    return advertisement;
  }

  private async buildAdvertisementStatisList(
    dateType: 'month' | 'day',
    statistics: Awaited<ReturnType<AdvertisementRepository['getStatistics']>>,
    dates: string[],
  ) {
    const map = new Map(statistics.map((stat) => [stat.date, stat]));

    return dates.map((date) => {
      const stat = map.get(date);

      const prevDate =
        dateType === 'month'
          ? dayjs(date).subtract(1, 'month').format('YYYY-MM')
          : dayjs(date).subtract(1, 'day').format('YYYY-MM-DD');

      const prevStat = map.get(prevDate);

      if (!stat) {
        return {
          date,
          totalImpressions: 0,
          impressionsRate: prevStat?.totalImpressions
            ? this.calculateRate(0, +prevStat.totalImpressions)
            : 0,
          totalClicks: 0,
          clicksRate: prevStat?.totalClicks
            ? this.calculateRate(0, +prevStat.totalClicks)
            : 0,
          totalPlays: 0,
          playsRate: prevStat?.totalPlays
            ? this.calculateRate(0, +prevStat.totalPlays)
            : 0,
        };
      }

      const { totalClicks, totalImpressions, totalPlays } = stat;

      return {
        date,
        totalImpressions: Number(totalImpressions),
        impressionsRate: prevStat
          ? this.calculateRate(+totalImpressions, +prevStat.totalImpressions)
          : 0,
        totalClicks: Number(totalClicks),
        clicksRate: prevStat
          ? this.calculateRate(+totalClicks, +prevStat.totalClicks)
          : 0,
        totalPlays: Number(totalPlays),
        playsRate: prevStat
          ? this.calculateRate(+totalPlays, +prevStat.totalPlays)
          : 0,
      };
    });
  }

  private calculateRate(now: number, prev: number) {
    return prev === 0 ? 0 : Number((((now - prev) / prev) * 100).toFixed(2));
  }

  private shuffle(array: Array<any>) {
    //* 배열 복사
    const result = [...array];

    for (let i = result.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [result[i], result[j]] = [result[j], result[i]];
    }

    return result;
  }

  private async getUserAdvertiesList(
    dto: GetUserAdvertisementListDto,
    agcId: number | null,
  ) {
    const fixedWhere = await this.advertisementRepository.userWhereClause(
      dto,
      0,
      agcId,
    );

    const randomWhere = await this.advertisementRepository.userWhereClause(
      dto,
      1,
      agcId,
    );

    //* 고정광고 GET
    const fixedAds = await this.advertisementRepository.getUserList(
      dto,
      fixedWhere,
      dto.limit,
    );

    if (dto.limit === 0) {
      let randomAds: typeof fixedAds = [];

      //* 모든 랜덤광고 조회
      const allRandomTypeList = await this.advertisementRepository.getUserList(
        dto,
        randomWhere,
        0,
      );

      //* 셔플 로직 실행
      const shuffled = this.shuffle(allRandomTypeList);
      randomAds = shuffled;

      const list = [...fixedAds, ...randomAds];

      // const ids = list.map((ad) => ad.adsId);

      // if (ids.length > 0) {
      //   this.advertisementRepository.impressionIncreseCounts(ids);
      // }

      return list;
    } else {
      //* 필요 광고 계산
      const needCount = dto.limit - fixedAds.length;

      let randomAds: typeof fixedAds = [];
      if (needCount > 0) {
        //* 모든 랜덤광고 조회
        const allRandomTypeList =
          await this.advertisementRepository.getUserList(dto, randomWhere, 0);

        //* 셔플 로직 실행
        const shuffled = this.shuffle(allRandomTypeList);
        randomAds = shuffled.slice(0, needCount);
      }

      const list = [...fixedAds, ...randomAds];

      // const ids = list.map((ad) => ad.adsId);

      // if (ids.length > 0) {
      //   this.advertisementRepository.impressionIncreseCounts(ids);
      // }

      return list;
    }
  }
}

function makeCustomErrorWithDescription(validationError: ValidationError) {
  const description =
    validationError.constraints instanceof Object
      ? Object.values(validationError.constraints).join(', ')
      : '';
  const customError = { ...NOT_FOUND_INPUT };
  customError.message = customError.message.replace(
    /#코드/g,
    validationError.property,
  );
  return { customError, description };
}
