/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import common from '@ohos.app.ability.common';
import rdb from '@ohos.data.relationalStore';
import { ValuesBucket } from '@ohos.data.ValuesBucket';
import LogUtils from '../../common/utils/LogUtils';
import { StringUtil } from '../../common/utils/StringUtil';
import { BusinessError } from '@ohos.base';
import dataShare from '@ohos.data.dataShare';
import CommonEventManager from '@ohos.commonEventManager';
import { BaseSingleRdbInsertHelper } from '../helperBase/BaseSingleRdbInsertHelper';
import { BaseRdbHelperForCloudSync } from '../helperBase/BaseRdbHelperForCloudSync';
import { BaseDbHelper } from '../helperBase/BaseDbHelper';

const RETRY_COUNT = 3;
const DELAY_TIMES = 100;
const TAG = 'DatabaseUtils';
const DELAY_TIME_ARRAY = [50, 100, 200, 400, 800];
const NEED_RETRY_CODE = new Set([14800015, 14800024, 14800025, 14800026, 14800028, 14800029]);
const DND_DATA_RESTORE_FINISH: string = 'custom.event.DND_DATA_RESTORE_FINISH';

/**
 * Used to handle failed operations when querying or inserting into the database
 */
export default class DatabaseUtils {
  /**
   * Retry three times getRdbStoreHelper for get double rdb object when get failed
   *
   * @param baseHelper to getRdbStoreHelper
   * @param context to get rdb object
   * @param dbName the rdb table
   * @returns rdb.RdbStore object or undefined
   */
  public static async retryGetDoubleRdb(baseHelper: BaseDbHelper, context: common.Context,
    dbName: string): Promise<rdb.RdbStore | undefined> {
    let rdbStoreHelper: rdb.RdbStore | undefined = undefined;
    for (let i = 1; i <= RETRY_COUNT; i++) {
      LogUtils.i(TAG, `retryGetDoubleRdb start: ${i}`);
      rdbStoreHelper = await baseHelper.getRdbStoreHelper(context, dbName);
      if (rdbStoreHelper) {
        LogUtils.i(TAG, `retryGetDoubleRdb ${i} success`);
        break;
      }
      await DatabaseUtils.delay(DELAY_TIMES);
    }
    return rdbStoreHelper;
  }

  /**
   * Retry three times getRdbStoreHelper for get single rdb object when get failed
   *
   * @param baseHelper to getRdbStoreHelper
   * @param context to get rdb object
   * @param dbName the rdb table
   * @returns rdb.RdbStore object or undefined
   */
  public static async retryGetSingleRdb(baseHelper: BaseSingleRdbInsertHelper, context: common.Context,
    dbName: string, isSupportSearch: boolean): Promise<rdb.RdbStore | undefined> {
    let rdbStoreHelper: rdb.RdbStore | undefined = undefined;
    for (let i = 1; i <= RETRY_COUNT; i++) {
      LogUtils.i(TAG, `retryGetSingleRdb start: ${i}`);
      rdbStoreHelper = await baseHelper.getRdbStoreHelper(context, dbName, isSupportSearch);
      if (rdbStoreHelper) {
        LogUtils.i(TAG, `retryGetSingleRdb ${i} success`);
        break;
      }
      await DatabaseUtils.delay(DELAY_TIMES);
    }
    return rdbStoreHelper;
  }

  public static async retryGetSingleRdbForCloudSync(baseHelper: BaseRdbHelperForCloudSync, context: common.Context,
    dbName: string, isSupportSearch: boolean): Promise<rdb.RdbStore | undefined> {
    let rdbStoreHelper: rdb.RdbStore | undefined = undefined;
    for (let i = 1; i <= RETRY_COUNT; i++) {
      LogUtils.i(TAG, `retryGetSingleRdb start: ${i}`);
      rdbStoreHelper = await baseHelper.getRdbStoreHelper(context, dbName, isSupportSearch);
      if (rdbStoreHelper) {
        LogUtils.i(TAG, `retryGetSingleRdb ${i} success`);
        break;
      }
      await DatabaseUtils.delay(DELAY_TIMES);
    }
    return rdbStoreHelper;
  }

  /**
   * Retry three times init double mimeType for get typeIdMap
   *
   * @param baseHelper to init mimeType
   * @param context to get rdb object
   * @param typeIdMap to get the relative of double mimeType and single type
   * @returns if retryInitType success
   */
  public static async retryInitType(baseHelper: BaseDbHelper, context: common.Context,
    typeIdMap: Record<number, number>): Promise<boolean> {
    let result: boolean = false;
    for (let i = 1; i <= RETRY_COUNT; i++) {
      LogUtils.i(TAG, `retryInitType start: ${i}`);
      result = await baseHelper.initMimeTypeId(context, typeIdMap);
      if (result) {
        LogUtils.i(TAG, `retryInitType ${i} success`);
        break;
      }
      await DatabaseUtils.delay(DELAY_TIMES);
    }
    return result;
  }

  /**
   * Retry three times insert single type to table contact_type
   *
   * @param baseHelper to insert the type data
   * @param context to get rdb object
   * @returns the number of successful insertions
   */
  public static async retryInsertType(baseHelper: BaseSingleRdbInsertHelper, context: common.Context): Promise<number> {
    let ret: number = -1;
    for (let i = 1; i <= RETRY_COUNT; i++) {
      LogUtils.i(TAG, `retryInsertType start: ${i}`);
      ret = await baseHelper.insertContactType(context);
      if (ret >= 0) {
        LogUtils.i(TAG, `retryInsertType ${i} success`);
        break;
      }
      await DatabaseUtils.delay(DELAY_TIMES);
    }
    return ret;
  }

  /**
   * Retry three times query double phone account id
   *
   * @param baseHelper to query phone account id
   * @param context to get rdb object
   * @returns double phone account id
   */
  public static async retryQueryAccountId(baseHelper: BaseDbHelper, context: common.Context): Promise<number> {
    let ret: number = -1;
    for (let i = 1; i <= RETRY_COUNT; i++) {
      LogUtils.i(TAG, `retryQueryAccountId start: ${i}`);
      ret = await baseHelper.queryAccountId(context);
      if (ret >= 0) {
        LogUtils.i(TAG, `retryQueryAccountId ${i} success`);
        break;
      }
      await DatabaseUtils.delay(DELAY_TIMES);
    }
    return ret;
  }

  /**
   * Retry three times insert single phone account id to table account
   *
   * @param baseHelper to insert phone account id
   * @param context to get rdb object
   * @param accountId which to be inserted
   * @returns the number of successful insertions
   */
  public static async retryInsertPhoneAccount(baseHelper: BaseSingleRdbInsertHelper,
    context: common.Context, accountId: number): Promise<number> {
    let ret: number = -1;
    for (let i = 1; i <= RETRY_COUNT; i++) {
      LogUtils.i(TAG, `retryInsertPhoneAccount start: ${i}`);
      ret = await baseHelper.insertPhoneAccount(context, accountId);
      if (ret >= 0) {
        LogUtils.i(TAG, `retryInsertPhoneAccount ${i} success`);
        break;
      }
      await DatabaseUtils.delay(DELAY_TIMES);
    }
    return ret;
  }

  /**
   * Retry three times batch insert single data
   *
   * @param baseHelper to insert data
   * @param context to get RdbStore
   * @param values which data to insert
   * @param tableName which table name to insert
   * @returns the number of successful insertions
   */
  public static async retryInsertBatchData(baseHelper: BaseSingleRdbInsertHelper,
    context: common.Context, values: ValuesBucket[], tableName: string): Promise<number> {
    let ret: number = -1;
    for (let i = 1; i <= RETRY_COUNT; i++) {
      LogUtils.i(TAG, `retryInsertBatchData start: ${i}`);
      ret = await baseHelper.insertBatchData(context, values, tableName);
      if (ret >= 0) {
        LogUtils.i(TAG, `retryInsertBatchData ${i} success`);
        break;
      }
      await DatabaseUtils.delay(DELAY_TIMES);
    }
    return ret;
  }

  /**
   * Check the columnName whether exist in the tableName
   *
   * @param rdbStore to get RdbStore
   * @param tableName which table name to check
   * @param columnName which column name to check
   * @returns the boolean of existence
   */
  public static async checkColumn(rdbStore: rdb.RdbStore, tableName: string, columnName: string): Promise<boolean> {
    let index : number = -1;
    LogUtils.i(TAG, 'checkColumn start');
    if (rdbStore === undefined) {
      return Promise.resolve(false);
    }
    if (StringUtil.isEmpty(tableName) || StringUtil.isEmpty(columnName)) {
      return Promise.resolve(false);
    }
    let predicatesInfo = new rdb.RdbPredicates(tableName);
    let resultSet: rdb.ResultSet | null | undefined = null;
    try {
      predicatesInfo.limitAs(1);
      resultSet = await rdbStore.query(predicatesInfo);
      if (resultSet && resultSet.goToFirstRow()) {
        index = resultSet.getColumnIndex(columnName);
        LogUtils.w(TAG, `checkColumn index : ${index} columnName : ${columnName}`);
        return Promise.resolve(index >= 0);
      }
      LogUtils.i(TAG, 'checkColumn end');
    } catch (error) {
      LogUtils.w(TAG, `checkColumn error : ${error?.message}, stack: ${error?.stack}`);
    } finally {
      if (resultSet && !resultSet.isClosed) {
        resultSet.close();
      }
    }
    return Promise.resolve(false);
  }

  /**
   * Check the columnName whether exist in the tableName
   *
   * @param rdbStore to get RdbStore
   * @param tableName which table name to check
   * @param columnName which column name to check
   * @returns the boolean of existence
   */
  public static async checkIsHasValue(rdbStore: rdb.RdbStore, tableName: string): Promise<boolean> {
    let index : number = -1;
    if (rdbStore === undefined) {
      return Promise.resolve(false);
    }
    if (StringUtil.isEmpty(tableName)) {
      return Promise.resolve(false);
    }
    let predicatesInfo = new rdb.RdbPredicates(tableName);
    let resultSet: rdb.ResultSet | null | undefined = null;
    try {
      predicatesInfo.limitAs(1);
      resultSet = await rdbStore.query(predicatesInfo);
      if (resultSet) {
        let size = resultSet.rowCount;
        LogUtils.w(TAG, `checkIsHasValue ${size}`);
        // 查询出来了值,就是有数据
        return Promise.resolve(size > 0);
      }
      LogUtils.i(TAG, 'checkColumn end');
    } catch (error) {
      LogUtils.w(TAG, `checkColumn error : ${error?.message}, stack: ${error?.stack}`);
    } finally {
      if (resultSet && !resultSet.isClosed) {
        resultSet.close();
      }
    }
    return Promise.resolve(false);
  }

  public static async delay(time: number): Promise<number> {
    return new Promise(resolve => setTimeout(resolve, time));
  }

  public static async notifyChange(dataAbilityHelper: dataShare.DataShareHelper, uri: string) {
    LogUtils.i(TAG, 'notifyChange start. uri:' + uri);
    await dataAbilityHelper.notifyChange(uri).then(() => {
      LogUtils.i(TAG, 'notifyChange success')
    }).catch((error: BusinessError) => {
      LogUtils.e(TAG, 'notifyChange error:' + error.message);
    });
  }

  public static notifyIntelligentScene(): void {
    LogUtils.i(TAG, 'notifyIntelligentScene start');
    let options: CommonEventManager.CommonEventPublishData = {
      bundleName: 'com.ohos.intelligentscene',
      data: 'com.ohos.contactsdataability',
    };
    try {
      CommonEventManager.publish(DND_DATA_RESTORE_FINISH, options, (error) => {
        if (error) {
          LogUtils.i(TAG, `notifyIntelligentScene error: ${error.code}, message is ${error.message}`);
        } else {
          LogUtils.e(TAG, `notifyIntelligentScene end`);
        }
      });
    } catch (error) {
      LogUtils.e(TAG, `notifyIntelligentScene error: ${error.code}, message is ${error.message}`);
    }
  }

  public static VALUE_BUCKET_SLOT: string = 'VALUE_BUCKET_SLOT'

  public static async executeSqlFuncWithRetry(sqlFunc: Function): Promise<void | number> {
    let retryIdx = 0;
    let ret: void | number = -1;
    while (retryIdx <= DELAY_TIME_ARRAY.length - 1) {
      try {
        ret = await sqlFunc();
      } catch (e) {
        LogUtils.e(TAG, `executeSql err,retryIdx:${retryIdx},code:${e.code},msg:${e.message}`);
        if (NEED_RETRY_CODE.has(e.code)) {
          await DatabaseUtils.delay(DELAY_TIME_ARRAY[retryIdx]);
          retryIdx++;
          continue;
        }
      }
      return ret;
    }
    LogUtils.e(TAG, `after retried still failed`);
    return ret;
  }
}