//  Copyright (c) 2024 Huawei Technologies Co., Ltd.
//  openUBMC is licensed under Mulan PSL v2.
//  You can use this software according to the terms and conditions of the Mulan PSL v2.
//  You may obtain a copy of Mulan PSL v2 at:
//        #  http://license.coscl.org.cn/MulanPSL2
//  THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
//  EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
//  MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
//  See the Mulan PSL v2 for more details.
import { getEncryptKey } from '@/utils/common-api';
import { IbmcHttp } from '@/utils/http-service';
import axios, { AxiosError, AxiosResponse } from 'axios';

const expireDays = 7;

export const update = async (): Promise<string> => {
  const newKey = (await getEncryptKey()).data?.PubKey ?? '';
  const date = new Date();
  date.setDate(date.getDate() + expireDays);
  localStorage.setItem('key', btoa(JSON.stringify({
    pubKey: newKey,
    nextUpdate: date,
  })));
  return newKey;
};

// 获取缓存的公钥
export const getCachedPubKey = async (): Promise<string> => {
  const key = localStorage.getItem('key') as string;
  try {
    const {pubKey, nextUpdate} = JSON.parse(atob(key));
    return Date.parse(nextUpdate) > Date.now() ? pubKey : await update();
  } catch (e) {
    return await update();
  }
};

type PathLike = string | string[];
const noop = (): void => {};

class SensitiveRequest extends IbmcHttp {
  private currentKey: string | null = null;
  private parentResolve: (value: (AxiosResponse | PromiseLike<AxiosResponse>)) => void = noop;
  private parentReject: (error: AxiosResponse) => void = noop;

  constructor(
      private encryptedPaths: PathLike[],
      private paramGetter: (pubKey: string) => Promise<any>
  ) {
    super();
  }

  async init(): Promise<string> {
    this.currentKey = await getCachedPubKey();
    return this.currentKey;
  }

  override async rejectOperate(
      resolve: (value: (AxiosResponse | PromiseLike<AxiosResponse>)) => void,
      reject: (error?: AxiosResponse) => void,
      _: AxiosError
  ): Promise<void> {
    this.parentResolve = resolve;
    this.parentReject = reject;
  }

  override async errorHandle(reqError: AxiosError<any>, method?: string, config?: any): Promise<void> {
    if (reqError.response?.data?.error?.[0].code === 'InvalidPubKey') {
      await this.forceUpdateKey();
      const reqConfig = reqError.config;
      if (!reqConfig) {
        return;
      }
      reqConfig.data = JSON.stringify(await this.paramGetter(this.currentKey ?? ''));
      try {
        const res = await axios.request(reqConfig);
        setTimeout(() => {
          this.parentResolve(res);
        });
      } catch (e: any) {
        super.errorHandle(e, method, config);
        setTimeout(() => {
          this.parentReject(e.response);
        });
      }
    } else {
      super.errorHandle(reqError, method, config);
      setTimeout(() => {
        this.parentReject(reqError.response as AxiosResponse);
      });
    }
  }

  override get = async (url: string, config?: any): Promise<AxiosResponse> =>
      await super.get(url, await this.getParams(), this.getConfig(config));

  override post = async (url: string, config?: any): Promise<AxiosResponse> =>
      await super.post(url, await this.getParams(), this.getConfig(config));

  override patch = async (url: string, config?: any): Promise<AxiosResponse> =>
      await super.patch(url, await this.getParams(), this.getConfig(config));

  override delete = async (url: string, config?: any): Promise<AxiosResponse> =>
      await super.delete(url, await this.getParams(), this.getConfig(config));

  private async getParams(): Promise<any> {
    try {
      return await this.paramGetter(this.currentKey ?? await this.init());
    } catch (e) {
      await this.forceUpdateKey();
      return await this.paramGetter(this.currentKey as string);
    }
  }

  private getConfig = (config: any): any => {
    const newConfig = config ?? { headers: {} };
    newConfig.headers['Encrypted-Properties'] = JSON.stringify(this.encryptedPaths);
    return newConfig;
  };

  private async forceUpdateKey(): Promise<void> {
    try {
      this.currentKey = await update();
    } catch (e) {
      setTimeout(() => this.parentReject(e as any));
    }
  }
}

export default function sensitive(encryptedPaths: PathLike[], paramGetter: (pubKey: string) => any): SensitiveRequest {
  return new SensitiveRequest(encryptedPaths, paramGetter);
};