import {
  ObjectLiteral,
  Repository,
  EntityTarget,
  UpdateResult,
  DeleteResult,
  UpdateQueryBuilder,
  Brackets,
  FindOptionsWhere,
} from 'typeorm';
import { Context } from '../log';

/**
 * VS Code上で型解析エラーが発生するため、typeorm内の型定義と同一の型定義をここに記述する
 */
type QueryDeepPartialEntity<T> = _QueryDeepPartialEntity<
  ObjectLiteral extends T ? unknown : T
>;
type _QueryDeepPartialEntity<T> = {
  [P in keyof T]?:
    | (T[P] extends Array<infer U>
        ? Array<_QueryDeepPartialEntity<U>>
        : T[P] extends ReadonlyArray<infer U>
        ? ReadonlyArray<_QueryDeepPartialEntity<U>>
        : _QueryDeepPartialEntity<T[P]>)
    | (() => string);
};

interface InsertEntityOptions {
  id: number;
}

const insertEntity = async <T extends InsertEntityOptions & ObjectLiteral>(
  entity: EntityTarget<T>,
  repository: Repository<T>,
  value: QueryDeepPartialEntity<T>,
  isCommentOut: boolean,
  context: Context,
): Promise<T> => {
  let query = repository.createQueryBuilder().insert().into(entity);
  if (isCommentOut) {
    query = query.comment(
      `${context.getTrackingId()}_${new Date().toUTCString()}`,
    );
  }
  const result = await query.values(value).execute();
  // result.identifiers[0].idがnumber型でない場合はエラー
  if (typeof result.identifiers[0].id !== 'number') {
    throw new Error('Failed to insert entity');
  }
  const where: FindOptionsWhere<T> = { id: result.identifiers[0].id } as T;

  // 結果をもとにセレクトする
  const inserted = await repository.findOne({
    where,
    comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
  });
  if (!inserted) {
    throw new Error('Failed to insert entity');
  }
  return inserted;
};

const insertEntities = async <T extends InsertEntityOptions & ObjectLiteral>(
  entity: EntityTarget<T>,
  repository: Repository<T>,
  values: QueryDeepPartialEntity<T>[],
  isCommentOut: boolean,
  context: Context,
): Promise<T[]> => {
  let query = repository.createQueryBuilder().insert().into(entity);
  if (isCommentOut) {
    query = query.comment(
      `${context.getTrackingId()}_${new Date().toUTCString()}`,
    );
  }
  const result = await query.values(values).execute();

  // 挿入するレコードが0で、結果も0であれば、からの配列を返す
  if (values.length === 0 && result.identifiers.length === 0) {
    return [];
  }

  // 挿入するレコード数と挿入されたレコード数が一致しない場合はエラー
  if (result.identifiers.length !== values.length) {
    throw new Error('Failed to insert entities');
  }
  const where: FindOptionsWhere<T>[] = result.identifiers.map((i) => {
    // idがnumber型でない場合はエラー
    if (typeof i.id !== 'number') {
      throw new Error('Failed to insert entities');
    }
    return { id: i.id } as T;
  });

  // 結果をもとにセレクトする
  const inserted = await repository.find({
    where,
    comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
  });
  if (!inserted) {
    throw new Error('Failed to insert entity');
  }
  return inserted;
};

const updateEntity = async <T extends ObjectLiteral>(
  repository: Repository<T>,
  criteria:
    | string
    | ((qb: UpdateQueryBuilder<T>) => string)
    | Brackets
    | ObjectLiteral
    | ObjectLiteral[],
  values: QueryDeepPartialEntity<T>,
  isCommentOut: boolean,
  context: Context,
): Promise<UpdateResult> => {
  let query = repository.createQueryBuilder().update();
  if (isCommentOut) {
    query = query.comment(
      `${context.getTrackingId()}_${new Date().toUTCString()}`,
    );
  }
  return await query.set(values).where(criteria).execute();
};

const deleteEntity = async <T extends ObjectLiteral>(
  repository: Repository<T>,
  criteria: string | Brackets | ObjectLiteral | ObjectLiteral[],
  isCommentOut: boolean,
  context: Context,
): Promise<DeleteResult> => {
  let query = repository.createQueryBuilder().delete();
  if (isCommentOut) {
    query = query.comment(
      `${context.getTrackingId()}_${new Date().toUTCString()}`,
    );
  }
  return await query.where(criteria).execute();
};

export { insertEntity, insertEntities, updateEntity, deleteEntity };
