import { withTransaction } from '@/db/with-transaction';
import ValidationError from '@/shared/errors/validation';
import { parseCsvRows } from '@/services/shared/csv-import';
import type { CurrentUser, DbApiOptions } from '@/db/api/types';

/**
 * Structural shape of a generic-CRUD `*DBApi`, generic over the entity's own
 * input/output types. TypeScript infers all of these from the repository passed
 * to {@link createCrudService}, so the produced service stays fully typed.
 */
export interface CrudDbApi<
  CreateData,
  UpdateData,
  ListFilter,
  BulkRow,
  Entity,
> {
  create(data: CreateData, options?: DbApiOptions): Promise<Entity>;
  bulkImport(rows: BulkRow[], options?: DbApiOptions): Promise<Entity[]>;
  update(
    id: string,
    data: UpdateData,
    options?: DbApiOptions,
  ): Promise<Entity | null>;
  deleteByIds(ids: string[], options?: DbApiOptions): Promise<unknown>;
  remove(id: string, options?: DbApiOptions): Promise<unknown>;
  findBy(where: { id: string }, options?: DbApiOptions): Promise<unknown>;
  findAll(
    filter: ListFilter,
    globalAccess: boolean,
    options?: DbApiOptions,
  ): Promise<{ rows: Entity[]; count: number }>;
  findAllAutocomplete(
    query: string | undefined,
    limit: number | undefined,
    offset: number | undefined,
    globalAccess: boolean,
    organizationId?: string,
  ): Promise<unknown>;
}

/**
 * Builds the standard generic-CRUD service (BLL) for an entity from its
 * repository, replacing the per-entity copy-paste. The controller/router
 * factories wrap it for the API layer. Special entities
 * (users/roles/permissions/campuses) keep hand-written services.
 */
export function createCrudService<
  CreateData,
  UpdateData,
  ListFilter,
  BulkRow,
  Entity,
>(
  dbApi: CrudDbApi<CreateData, UpdateData, ListFilter, BulkRow, Entity>,
  { notFoundCode }: { notFoundCode: string },
) {
  return {
    async create(data: CreateData, currentUser?: CurrentUser): Promise<void> {
      await withTransaction((transaction) =>
        dbApi.create(data, { currentUser, transaction }),
      );
    },

    async bulkImport(
      fileBuffer: Buffer,
      currentUser?: CurrentUser,
    ): Promise<void> {
      const rows = await parseCsvRows<BulkRow>(fileBuffer);
      await withTransaction((transaction) =>
        dbApi.bulkImport(rows, {
          transaction,
          ignoreDuplicates: true,
          validate: true,
          currentUser,
        }),
      );
    },

    async update(
      data: UpdateData,
      id: string,
      currentUser?: CurrentUser,
    ): Promise<Entity> {
      return withTransaction(async (transaction) => {
        const updated = await dbApi.update(id, data, {
          currentUser,
          transaction,
        });
        if (!updated) {
          throw new ValidationError(notFoundCode);
        }
        return updated;
      });
    },

    async remove(id: string, currentUser?: CurrentUser): Promise<void> {
      await withTransaction((transaction) =>
        dbApi.remove(id, { currentUser, transaction }),
      );
    },

    async deleteByIds(ids: string[], currentUser?: CurrentUser): Promise<void> {
      await withTransaction((transaction) =>
        dbApi.deleteByIds(ids, { currentUser, transaction }),
      );
    },

    list(filter: ListFilter, globalAccess: boolean, currentUser?: CurrentUser) {
      return dbApi.findAll(filter, globalAccess, { currentUser });
    },

    count(filter: ListFilter, globalAccess: boolean, currentUser?: CurrentUser) {
      return dbApi.findAll(filter, globalAccess, {
        countOnly: true,
        currentUser,
      });
    },

    autocomplete(
      query: string | undefined,
      limit: number | undefined,
      offset: number | undefined,
      globalAccess: boolean,
      organizationId?: string,
    ) {
      return dbApi.findAllAutocomplete(
        query,
        limit,
        offset,
        globalAccess,
        organizationId,
      );
    },

    findById(id: string, currentUser?: CurrentUser) {
      return dbApi.findBy({ id }, { currentUser });
    },
  };
}
