/**
 * 專案名稱： @wistroni40/bnft
 * 部門代號： ML8100
 * 檔案說明： 抽象效益計算範本
 * @CREATE Thu Jan 21 2021 下午1:44:54
 * @author Steve Y Lin
 * @contact Steve_Y_Lin@wistron.com #1342
 * -----------------------------------------------------------------------------
 * @NOTE
 */

import axios from 'axios';
import * as schedule from 'node-schedule';
import { from, Observable, of, Subject } from 'rxjs';
import { catchError, concatAll, filter, map } from 'rxjs/operators';
import { Server } from './../api';
import { AxiosHttpService, HttpAdapter, HttpResponse } from './../http';
import { Log4js } from './../logger';
import {
  HttpProducer,
  ProducePayloadEntity as ProducePayload,
  ProducePayloadModel,
} from './../retry';
import { TimeManager } from './../utils';
import {
  ApiConfig,
  BaseModel as ApiBaseModel,
  BenefitActivedSystemModel as ActivedSystemModel,
  BenefitActivedSystemService,
  BenefitLaborCostService,
  BenefitLatestLaborCostResponse,
  BenefitPlantModel as PlantModel,
} from './api';
import { BenefitLatestLaborCostEntity as LaborCost } from './api/models';
import { BenefitQueryConvertor } from './classes';
import {
  BenefitConfigEntity,
  BenefitConfigModel,
  BenefitQueryModel,
  BenefitSavingEntity,
  Bnft,
} from './models';

/**
 * 效益上拋回傳結果
 */
export type BenefitResponse = {
  error: any;
  result: ProducePayloadModel<Bnft.BenefitSaving>;
};

/**
 * 抽象效益計算範本
 */
export abstract class BnftTemplate<C = any> {
  /**
   * 日誌
   */
  private readonly console = new Log4js('bnft');
  /**
   * API服務器
   */
  private apiServer = Server.instance.register(this);
  /**
   * HTTP請求
   */
  protected http: HttpAdapter = new AxiosHttpService(axios);
  /**
   * HTTP拋送者
   */
  protected producer: HttpProducer;
  /**
   * 效益激活系統服務
   */
  protected activedSystemService = new BenefitActivedSystemService(this.http);
  /**
   * 效益DL及IDL人員工時服務
   */
  protected benefitLaborCostService = new BenefitLaborCostService(this.http);
  /**
   * 排程設定
   */
  protected cron: string | null = null;
  /**
   * 需要計算的廠別
   */
  protected abstract enabledPlant?: string[];
  /**
   * 送出數據完畢
   */
  public sendCompleted: Subject<BenefitResponse>;

  /**
   * @param config 效益設定檔
   */
  constructor(public config: BenefitConfigModel<C>) {
    this.config = new BenefitConfigEntity(this.config);
    ApiConfig.path = this.config.benefitApi;
    this.producer = new HttpProducer(this.http, {
      count: this.config.retry,
      interval: this.config.retryInterval,
    });
    this.sendCompleted = this.producer.sendCompleted;
  }

  /**
   * 取得預設的效益參數資料
   *
   * @method private
   * @return 回傳預設的效益參數資料
   */
  private getDefaultProcucePayload(): Observable<
    ProducePayloadModel<Bnft.BenefitSaving>
  > {
    return of(
      new ProducePayload(this.config.publishApi, new BenefitSavingEntity()),
    );
  }

  /**
   * 建構效益參數實體
   *
   * @method protected
   * @param timestamp 資料時間戳
   * @param condition 效益查詢條件
   * @param params    效益參數
   */
  protected buildBenefitEntity(
    timestamp: Date,
    condition: BenefitQueryModel,
    params: Bnft.Param[],
  ): Bnft.BenefitSaving {
    return new BenefitSavingEntity({
      evt_dt: timestamp.getTime(),
      freq: 'D',
      site: condition.site,
      company: condition.company,
      plant: condition.plant,
      plant_code: condition.plantCode,
      system_id: this.config.systemId,
      type_id: this.config.typeId,
      benefit_type: this.config.benefitType,
      params,
    });
  }

  /**
   * 建構查詢激活系統的查詢條件
   *
   * @method protected
   * @return 回傳查詢條件
   */
  protected buildQueryActivatedSystemsFilter(): ApiBaseModel.Filter {
    return {
      include: 'benefitPlant',
      where: { system_id: this.config.systemId },
    };
  }

  /**
   * 查詢效益參數
   *
   * @method protected
   * @param plant     廠別資料作為查詢條件
   * @param timestamp 查詢開始時間
   * @return 回傳效益參數
   */
  protected queryBenefit(
    plant: PlantModel,
    timestamp?: Date,
  ): Observable<Bnft.BenefitSaving> {
    const start = TimeManager.getStartTime(timestamp);
    const end = TimeManager.getEndTime(timestamp);
    const condition = new BenefitQueryConvertor(plant)
      .setStartTime(start)
      .setEndTime(end)
      .build();
    return this.buildPayload(start, condition);
  }

  /**
   * 設定查詢用的HTTP
   *
   * @method public
   * @param http HTTP請求
   * @return 回傳物件本身
   */
  public setHttp(http: HttpAdapter): BnftTemplate {
    this.http = http;
    this.activedSystemService = new BenefitActivedSystemService(this.http);
    return this;
  }

  /**
   * 設定排程計算效益
   *
   * @method public
   * @param cron 排程
   * @return 回傳物件本身
   */
  public setSchedule(cron: string): Observable<BenefitResponse> {
    this.cron = cron;
    if (this.cron) {
      schedule.scheduleJob(this.cron, this.execute.bind(this));
    }
    return this.sendCompleted;
  }

  /**
   * 過濾無須或異常的廠別
   *
   * @method public
   * @param plant 廠別資料
   * @return 回傳該廠別是否要保留
   */
  public filterPlant(plant?: PlantModel): boolean {
    const isNotUndefined = plant !== undefined;
    let isAllowedPlant = true;
    if (plant && this.enabledPlant && this.enabledPlant.length > 0) {
      isAllowedPlant = this.enabledPlant.includes(plant.plantcode);
    }
    return isNotUndefined && isAllowedPlant;
  }

  /**
   * 建構效益參數資料
   *
   * @method public
   * @param timestamp 資料時間戳
   * @param condition 效益查詢條件
   * @return 回傳效益參數資料
   */
  public buildPayload(
    timestamp: Date,
    condition: BenefitQueryModel,
  ): Observable<Bnft.BenefitSaving> {
    return from(this.getBenefitParams(condition)).pipe(
      map(params => this.buildBenefitEntity(timestamp, condition, params)),
    );
  }

  /**
   * 取得效益參數
   *
   * @method public
   * @param condition 效益查詢條件
   * @return 回傳效益參數
   */
  public abstract getBenefitParams(
    condition: BenefitQueryModel,
  ): Promise<Bnft.Param[]>;

  /**
   * 處理效益參數
   *
   * @method public
   * @param response  查詢激活的效益系統
   * @param timestamp 查詢開始時間
   * @return 回傳處理後效益參數
   */
  public processBenefitParams(
    response: Observable<HttpResponse<ActivedSystemModel[]>>,
    timestamp?: Date,
  ): Observable<ProducePayloadModel<Bnft.BenefitSaving>> {
    return response.pipe(
      // 取出HTTP回應的資料
      map(res => res.data),
      // 從系統資料中取出廠別資料
      map(systems => systems.map(system => system.benefitPlant)),
      // 將廠別資料重新建成Subject
      map(plants => from(plants)),
      // 將廠別打散成單筆數據
      concatAll(),
      // 保留需要計算及無異常的廠別
      filter(plant => this.filterPlant(plant)),
      // 查詢效益參數
      map(plant => this.queryBenefit(plant as PlantModel, timestamp)),
      // 將效益參數打散成單筆數據
      concatAll(),
      // 打包成拋送的資料格式
      map(benefit => new ProducePayload(this.config.publishApi, benefit)),
      // 發生錯誤給定預設值
      catchError(() => this.getDefaultProcucePayload()),
    );
  }

  /**
   * 執行效益參數撈取
   *
   * @method public
   * @param timestamp 查詢開始時間
   * @return 回傳效益參數上拋結果
   */
  public execute(timestamp?: Date): Observable<BenefitResponse> {
    const dev = this.config.dev;
    const query = this.buildQueryActivatedSystemsFilter();
    const system$ = this.activedSystemService.find<ActivedSystemModel>(query);
    const param$ = this.processBenefitParams(system$, timestamp);
    param$.subscribe(payload => this.send(payload, !dev));
    return this.sendCompleted;
  }

  /**
   * 將效益參數上拋
   *
   * @method public
   * @param payload  效益參數
   * @param sendable 效益參數是否上拋
   */
  public async send(
    payload: ProducePayloadModel<Bnft.BenefitSaving>,
    sendable = true,
  ): Promise<void> {
    if (sendable) {
      this.producer.publish(payload);
    } else {
      this.console.debug(JSON.stringify(payload));
      this.sendCompleted.next({ error: null, result: payload });
    }
  }

  /**
   * 取得最新的IDL或ID人員工時
   *
   * @method public
   * @param site      Site
   * @param plantCode 廠別代碼
   * @param laborType 人員類別(IDL or DL)
   * @return 回傳最新的IDL或ID人員工時
   */
  public findLatestLaborCost(
    site: string,
    plantCode: string,
    type: 'idl' | 'dl',
  ): Observable<BenefitLatestLaborCostResponse> {
    return this.benefitLaborCostService
      .getLatestCost(site, plantCode, type)
      .pipe(
        // 將工時資料取出
        map(response => response.data),
        // 發生錯誤給定預設值
        catchError(() => of(new LaborCost({ site, plant: plantCode, type }))),
      );
  }
}
