import { db, Prisma, PrismaClient } from '@nice/common';
import {
  Operations,
  DelegateArgs,
  DelegateReturnTypes,
  DataArgs,
  WhereArgs,
  DelegateFuncs,
  UpdateOrderArgs,
  TransactionType,
  OrderByArgs,
  SelectArgs,
} from './base.type';
import {
  NotFoundException,
  InternalServerErrorException,
} from '@nestjs/common';
import { ERROR_MAP, operationT, PrismaErrorCode } from './errorMap.prisma';

/**
 * BaseService provides a generic CRUD interface for a prisma model.
 * It enables common data operations such as find, create, update, and delete.
 *
 * @template D - Type for the model delegate, defining available operations.
 * @template A - Arguments for the model delegate's operations.
 * @template R - Return types for the model delegate's operations.
 */
export class BaseService<
  D extends DelegateFuncs,
  A extends DelegateArgs<D> = DelegateArgs<D>,
  R extends DelegateReturnTypes<D> = DelegateReturnTypes<D>,
> {
  protected ORDER_INTERVAL = 100;
  /**
   * Initializes the BaseService with the specified model.
   * @param model - The Prisma model delegate for database operations.
   */
  constructor(
    protected prisma: PrismaClient,
    protected objectType: string,
    protected enableOrder: boolean = false,
  ) {}

  /**
   * Retrieves the name of the model dynamically.
   * @returns {string} - The name of the model.
   */
  private getModelName(): string {
    const modelName = this.getModel().constructor.name;
    return modelName;
  }
  private getModel(tx?: TransactionType): D {
    return tx?.[this.objectType] || (this.prisma[this.objectType] as D);
  }
  /**
   * Error handling helper function
   */
  private handleError(error: any, operation: operationT): never {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      const handler = ERROR_MAP[error.code as PrismaErrorCode];
      if (handler) {
        throw handler(
          operation,
          error?.meta || {
            target: 'record',
            model: this.getModelName(),
          },
        );
      }
      throw new InternalServerErrorException(
        `Database error: ${error.message}`,
      );
    }
    throw new InternalServerErrorException(
      `Unexpected error: ${error.message || 'Unknown error occurred.'}`,
    );
  }
  /**
   * Finds a unique record by given criteria.
   * @param args - Arguments to find a unique record.
   * @returns {Promise<R['findUnique']>} - A promise resolving to the found record.
   * @example
   * const user = await service.findUnique({ where: { id: 'user_id' } });
   */
  async findUnique(args: A['findUnique']): Promise<R['findUnique']> {
    try {
      return this.getModel().findUnique(args as any) as Promise<
        R['findUnique']
      >;
    } catch (error) {
      this.handleError(error, 'read');
    }
  }
  /**
   * Finds the first record matching the given criteria.
   * @param args - Arguments to find the first matching record.
   * @returns {Promise<R['findFirst']>} - A promise resolving to the first matching record.
   * @example
   * const firstUser = await service.findFirst({ where: { name: 'John' } });
   */
  async findFirst(args: A['findFirst']): Promise<R['findFirst']> {
    try {
      return this.getModel().findFirst(args as any) as Promise<R['findFirst']>;
    } catch (error) {
      this.handleError(error, 'read');
    }
  }

  /**
   * Finds a record by its ID.
   * @param id - The ID of the record to find.
   * @param args - Optional additional arguments for the find operation.
   * @returns {Promise<R['findFirst']>} - A promise resolving to the found record.
   * @throws {NotFoundException} - If no record is found with the given ID.
   * @example
   * const user = await service.findById('user_id');
   */
  async findById(id: string, args?: A['findFirst']): Promise<R['findFirst']> {
    try {
      const record = (await this.getModel().findFirst({
        where: { id },
        ...(args || {}),
      })) as R['findFirst'];
      if (!record) {
        throw new NotFoundException(`Record with ID ${id} not found.`);
      }
      return record;
    } catch (error) {
      this.handleError(error, 'read');
    }
  }

  /**
   * Finds multiple records matching the given criteria.
   * @param args - Arguments to find multiple records.
   * @returns {Promise<R['findMany']>} - A promise resolving to the list of found records.
   * @example
   * const users = await service.findMany({ where: { isActive: true } });
   */
  async findMany(args: A['findMany']): Promise<R['findMany']> {
    try {
      return this.getModel().findMany(args as any) as Promise<R['findMany']>;
    } catch (error) {
      this.handleError(error, 'read');
    }
  }

  /**
   * Creates a new record with the given data.
   * @param args - Arguments to create a record.
   * @returns {Promise<R['create']>} - A promise resolving to the created record.
   * @example
   * const newUser = await service.create({ data: { name: 'John Doe' } });
   */
  async create(args: A['create'], params?: any): Promise<R['create']> {
    try {
      if (this.enableOrder && !(args as any).data.order) {
        // 查找当前最大的 order 值
        const maxOrderItem = (await this.getModel(params?.tx).findFirst({
          orderBy: { order: 'desc' },
        })) as any;
        // 设置新记录的 order 值
        const newOrder = maxOrderItem
          ? maxOrderItem.order + this.ORDER_INTERVAL
          : 1;
        // 将 order 添加到创建参数中
        (args as any).data.order = newOrder;
      }
      return this.getModel(params?.tx).create(args as any) as Promise<
        R['create']
      >;
    } catch (error) {
      this.handleError(error, 'create');
    }
  }

  /**
   * Creates multiple new records with the given data.
   * @param args - Arguments to create multiple records.
   * @returns {Promise<R['createMany']>} - A promise resolving to the created records.
   * @example
   * const newUsers = await service.createMany({ data: [{ name: 'John' }, { name: 'Jane' }] });
   */
  async createMany(
    args: A['createMany'],
    params?: any,
  ): Promise<R['createMany']> {
    try {
      return this.getModel(params?.tx).createMany(args as any) as Promise<
        R['createMany']
      >;
    } catch (error) {
      this.handleError(error, 'create');
    }
  }

  /**
   * Updates a record with the given data.
   * @param args - Arguments to update a record.
   * @returns {Promise<R['update']>} - A promise resolving to the updated record.
   * @example
   * const updatedUser = await service.update({ where: { id: 'user_id' }, data: { name: 'John' } });
   */
  async update(args: A['update'], params?: any): Promise<R['update']> {
    try {
      return this.getModel(params?.tx).update(args as any) as Promise<
        R['update']
      >;
    } catch (error) {
      this.handleError(error, 'update');
    }
  }

  /**
   * Updates a record by ID with the given data.
   * @param id - The ID of the record to update.
   * @param data - The data to update the record with.
   * @returns {Promise<R['update']>} - A promise resolving to the updated record.
   * @example
   * const updatedUser = await service.updateById('user_id', { name: 'John Doe' });
   */
  async updateById(
    id: string,
    data: DataArgs<A['update']>,
  ): Promise<R['update']> {
    try {
      return (await this.getModel().update({
        where: { id },
        data: data as any,
      })) as R['update'];
    } catch (error) {
      this.handleError(error, 'update');
    }
  }

  /**
   * Deletes a record by ID.
   * @param id - The ID of the record to delete.
   * @returns {Promise<R['delete']>} - A promise resolving to the deleted record.
   * @example
   * const deletedUser = await service.deleteById('user_id');
   */
  async deleteById(id: string): Promise<R['delete']> {
    try {
      return (await this.getModel().delete({
        where: { id },
      })) as R['delete'];
    } catch (error) {
      this.handleError(error, 'delete');
    }
  }

  /**
   * Deletes a record based on the given criteria.
   * @param args - Arguments to delete a record.
   * @returns {Promise<R['delete']>} - A promise resolving to the deleted record.
   * @example
   * const deletedUser = await service.delete({ where: { name: 'John' } });
   */
  async delete(args: A['delete'], params?: any): Promise<R['delete']> {
    try {
      return this.getModel(params?.tx).delete(args as any) as Promise<
        R['delete']
      >;
    } catch (error) {
      this.handleError(error, 'delete');
    }
  }

  /**
   * Creates or updates a record based on the given criteria.
   * @param args - Arguments to upsert a record.
   * @returns {Promise<R['upsert']>} - A promise resolving to the created or updated record.
   * @example
   * const user = await service.upsert({ where: { id: 'user_id' }, create: { name: 'John' }, update: { name: 'Johnny' } });
   */
  async upsert(args: A['upsert']): Promise<R['upsert']> {
    try {
      return this.getModel().upsert(args as any) as Promise<R['upsert']>;
    } catch (error) {
      this.handleError(error, 'create');
    }
  }

  /**
   * Counts the number of records matching the given criteria.
   * @param args - Arguments to count records.
   * @returns {Promise<R['count']>} - A promise resolving to the count.
   * @example
   * const userCount = await service.count({ where: { isActive: true } });
   */
  async count(args: A['count']): Promise<R['count']> {
    try {
      return this.getModel().count(args as any) as Promise<R['count']>;
    } catch (error) {
      this.handleError(error, 'read');
    }
  }

  /**
   * Aggregates records based on the given criteria.
   * @param args - Arguments to aggregate records.
   * @returns {Promise<R['aggregate']>} - A promise resolving to the aggregation result.
   * @example
   * const userAggregates = await service.aggregate({ _count: true });
   */
  async aggregate(args: A['aggregate']): Promise<R['aggregate']> {
    try {
      return this.getModel().aggregate(args as any) as Promise<R['aggregate']>;
    } catch (error) {
      this.handleError(error, 'read');
    }
  }

  /**
   * Deletes multiple records based on the given criteria.
   * @param args - Arguments to delete multiple records.
   * @returns {Promise<R['deleteMany']>} - A promise resolving to the result of the deletion.
   * @example
   * const deleteResult = await service.deleteMany({ where: { isActive: false } });
   */
  async deleteMany(
    args: A['deleteMany'],
    params?: any,
  ): Promise<R['deleteMany']> {
    try {
      return this.getModel(params?.tx).deleteMany(args as any) as Promise<
        R['deleteMany']
      >;
    } catch (error) {
      this.handleError(error, 'delete');
    }
  }

  /**
   * Updates multiple records based on the given criteria.
   * @param args - Arguments to update multiple records.
   * @returns {Promise<R['updateMany']>} - A promise resolving to the result of the update.
   * @example
   * const updateResult = await service.updateMany({ where: { isActive: true }, data: { isActive: false } });
   */
  async updateMany(args: A['updateMany']): Promise<R['updateMany']> {
    try {
      return this.getModel().updateMany(args as any) as Promise<
        R['updateMany']
      >;
    } catch (error) {
      this.handleError(error, 'update');
    }
  }

  /**
   * Finds a record by unique criteria or creates it if not found.
   * @param args - Arguments to find or create a record.
   * @returns {Promise<R['findUnique'] | R['create']>} - A promise resolving to the found or created record.
   * @example
   * const user = await service.findOrCreate({ where: { email: 'john@example.com' }, create: { email: 'john@example.com', name: 'John' } });
   */
  async findOrCreate(args: {
    where: WhereArgs<A['findUnique']>;
    create: DataArgs<A['create']>;
  }): Promise<R['findUnique'] | R['create']> {
    try {
      const existing = (await this.getModel().findUnique({
        where: args.where,
      } as any)) as R['findUnique'];
      if (existing) {
        return existing;
      }
      return this.getModel().create({ data: args.create } as any) as Promise<
        R['create']
      >;
    } catch (error) {
      this.handleError(error, 'create');
    }
  }

  /**
   * Checks if a record exists based on the given criteria.
   * @param where - The criteria to check for existence.
   * @returns {Promise<boolean>} - A promise resolving to true if the record exists, false otherwise.
   * @example
   * const exists = await service.exists({ email: 'john@example.com' });
   */
  async exists(where: WhereArgs<A['findUnique']>): Promise<boolean> {
    try {
      const count = (await this.getModel().count({ where } as any)) as number;
      return count > 0;
    } catch (error) {
      this.handleError(error, 'read');
    }
  }

  /**
   * Soft deletes records by setting `isDeleted` to true for the given IDs.
   * @param ids - An array of IDs of the records to soft delete.
   * @param data - Additional data to update on soft delete.  (Optional)
   * @returns {Promise<R['update'][]>} - A promise resolving to an array of updated records.
   * @example
   * const softDeletedUsers = await service.softDeleteByIds(['user_id1', 'user_id2'], { reason: 'Bulk deletion' });
   */
  async softDeleteByIds(
    ids: string[],
    data: Partial<DataArgs<A['update']>> = {}, // Default to empty object
  ): Promise<R['update'][]> {
    try {
      if (!ids || ids.length === 0) {
        return []; // Return empty array if no IDs are provided
      }
      return this.getModel().updateMany({
        where: { id: { in: ids } },
        data: { ...data, deletedAt: new Date() } as any,
      }) as Promise<R['update'][]>;
    } catch (error) {
      this.handleError(error, 'delete');
    }
  }
  /**
   * Restores soft-deleted records by setting `isDeleted` to false for the given IDs.
   * @param ids - An array of IDs of the records to restore.
   * @param data - Additional data to update on restore. (Optional)
   * @returns {Promise<R['update'][]>} - A promise resolving to an array of updated records.
   * @example
   * const restoredUsers = await service.restoreByIds(['user_id1', 'user_id2'], { restoredBy: 'admin' });
   */
  async restoreByIds(
    ids: string[],
    data: Partial<DataArgs<A['update']>> = {}, // Default to empty object
  ): Promise<R['update'][]> {
    try {
      if (!ids || ids.length === 0) {
        return []; // Return empty array if no IDs are provided
      }
      return this.getModel().updateMany({
        where: { id: { in: ids }, isDeleted: true }, // Only restore soft-deleted records
        data: { ...data, deletedAt: null } as any,
      }) as Promise<R['update'][]>;
    } catch (error) {
      this.handleError(error, 'update');
    }
  }

  /**
   * Finds multiple records with pagination.
   * @param args - Arguments including page, pageSize, and optional filters.
   * @returns {Promise<R['findMany']>} - A promise resolving to the paginated list of records.
   * @example
   * const users = await service.findManyWithPagination({ page: 1, pageSize: 10, where: { isActive: true } });
   */
  async findManyWithPagination(args: {
    page?: number;
    pageSize?: number;
    where?: WhereArgs<A['findMany']>;
    orderBy?: OrderByArgs<A['findMany']>;
    select?: SelectArgs<A['findMany']>;
  }): Promise<{ items: R['findMany']; totalPages: number }> {
    const { page = 1, pageSize = 10, where, select, orderBy } = args;

    try {
      // 获取总记录数
      const total = (await this.getModel().count({ where })) as number;
      // 获取分页数据
      const items = (await this.getModel().findMany({
        where,
        select,
        orderBy,
        skip: (page - 1) * pageSize,
        take: pageSize,
      } as any)) as R['findMany'];
      // 计算总页数
      const totalPages = Math.ceil(total / pageSize);
      return {
        items,
        totalPages,
      };
    } catch (error) {
      this.handleError(error, 'read');
    }
  }
  /**
   * 基于游标的分页查询方法
   * @description 该方法实现了基于游标的分页查询,相比传统的offset/limit分页有更好的性能
   * @param args 查询参数对象,包含cursor、take、where、orderBy、select等字段
   * @returns 返回查询结果对象,包含items数组、总数和下一页游标
   */
  async findManyWithCursor(
    args: A['findMany'],
  ): Promise<{ items: R['findMany']; nextCursor: string | null }> {
    // 解构查询参数,设置默认每页取10条记录
    const { cursor, take = 6, where, orderBy, select } = args as any;

    try {
      const items = (await this.getModel().findMany({
        where: where,
        orderBy: [{ ...orderBy }, { updatedAt: 'desc' }, { id: 'desc' }],
        select,
        take: take + 1,
        cursor: cursor
          ? { updatedAt: cursor.split('_')[0], id: cursor.split('_')[1] }
          : undefined,
      } as any)) as any[];

      /**
       * 处理下一页游标
       * @description
       * 1. 如果查到的记录数超过take,说明还有下一页
       * 2. 将最后一条记录弹出,用其updatedAt和id构造下一页游标
       * 3. 游标格式为: updatedAt_id
       */
      let nextCursor: string | null = '';
      if (items.length > take) {
        const nextItem = items.pop();
        nextCursor = `${nextItem!.updatedAt?.toISOString()}_${nextItem!.id}`;
      }
      if (nextCursor === '') {
        nextCursor = null;
      }

      /**
       * 返回查询结果
       * @returns {Object}
       * - items: 当前页记录
       * - totalCount: 总记录数
       * - nextCursor: 下一页游标
       */
      return {
        items: items as R['findMany'],
        nextCursor: nextCursor,
      };
    } catch (error) {
      this.handleError(error, 'read');
    }
  }

  async updateOrder(args: UpdateOrderArgs) {
    const { id, overId } = args;
    const [currentObject, targetObject] = (await Promise.all([
      this.findFirst({ where: { id } } as any),
      this.findFirst({ where: { id: overId } } as any),
    ])) as any;
    if (!currentObject || !targetObject) {
      throw new Error('Invalid object or target object');
    }
    const nextObject = (await this.findFirst({
      where: {
        order: { gt: targetObject.order },
        deletedAt: null,
      },
      orderBy: { order: 'asc' },
    } as any)) as any;

    const newOrder = nextObject
      ? (targetObject.order + nextObject.order) / 2
      : targetObject.order + this.ORDER_INTERVAL;
    return this.update({ where: { id }, data: { order: newOrder } } as any);
  }
  /**
   * Wraps the result of a database operation with a transformation function.
   * @template T - The type of the result to be transformed.
   * @param operationPromise - The promise representing the database operation.
   * @param transformFn - A function that transforms the result.
   * @returns {Promise<T>} - A promise resolving to the transformed result.
   * @example
   * const user = await service.wrapResult(
   *   service.findUnique({ where: { id: 'user_id' } }),
   *   (result) => ({ ...result, fullName: `${result.firstName} ${result.lastName}` })
   * );
   */
  async wrapResult<T>(
    operationPromise: Promise<T>,
    transformFn: (result: T) => Promise<T>,
  ): Promise<T> {
    try {
      const result = await operationPromise;
      return await transformFn(result);
    } catch (error) {
      throw error; // Re-throw the error to maintain existing error handling
    }
  }
}
