import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { DataSource, EntityTarget, FindOptionsWhere, ObjectLiteral } from 'typeorm';

export type OrderField = 'displayOrder' | 'stepOrder';

@Injectable()
export class DisplayOrderService {
  constructor(private readonly dataSource: DataSource) {}

  async getMaxOrder<T extends ObjectLiteral>(
    entity: EntityTarget<T>,
    field: OrderField,
    where?: FindOptionsWhere<T>,
  ): Promise<number> {
    const repo = this.dataSource.getRepository(entity);
    const qb = repo.createQueryBuilder('e').select(`MAX(e.${field})`, 'max');
    if (where) {
      Object.entries(where).forEach(([key, value]) => {
        if (value !== undefined) {
          qb.andWhere(`e.${key} = :${key}`, { [key]: value });
        }
      });
    }
    const row = await qb.getRawOne<{ max: string | null }>();
    return row?.max ? Number(row.max) : 0;
  }

  async resolveCreateOrder<T extends ObjectLiteral>(
    entity: EntityTarget<T>,
    field: OrderField,
    options: {
      explicitOrder?: number;
      insertAfterId?: string;
      scopeWhere?: FindOptionsWhere<T>;
    },
  ): Promise<number> {
    const { explicitOrder, insertAfterId, scopeWhere } = options;

    if (insertAfterId) {
      if (explicitOrder !== undefined) {
        throw new BadRequestException('Cannot set both explicit order and insertAfterId');
      }
      const repo = this.dataSource.getRepository(entity);
      const after = await repo.findOne({
        where: { id: insertAfterId, ...scopeWhere } as unknown as FindOptionsWhere<T>,
      });
      if (!after) {
        throw new NotFoundException(`Record ${insertAfterId} not found in scope`);
      }
      const afterOrder = Number((after as Record<string, unknown>)[field]);
      await this.shiftOrdersFrom(entity, field, afterOrder + 1, 1, scopeWhere);
      return afterOrder + 1;
    }

    if (explicitOrder !== undefined) {
      return explicitOrder;
    }

    const max = await this.getMaxOrder(entity, field, scopeWhere);
    return max + 1;
  }

  async reorderByIds<T extends ObjectLiteral>(
    entity: EntityTarget<T>,
    field: OrderField,
    ids: string[],
    scopeWhere?: FindOptionsWhere<T>,
  ): Promise<void> {
    const repo = this.dataSource.getRepository(entity);
    const existing = await repo.find({
      where: scopeWhere,
      order: { [field]: 'ASC' } as never,
    });

    const scopedIds = new Set(existing.map((row) => String(row.id)));
    if (ids.length !== scopedIds.size) {
      throw new BadRequestException('Reorder list must include every item in scope exactly once');
    }
    for (const id of ids) {
      if (!scopedIds.has(id)) {
        throw new BadRequestException(`ID ${id} is not in the reorder scope`);
      }
    }

    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      for (let i = 0; i < ids.length; i++) {
        await queryRunner.manager.update(entity, ids[i], { [field]: -(i + 1) } as never);
      }
      for (let i = 0; i < ids.length; i++) {
        await queryRunner.manager.update(entity, ids[i], { [field]: i + 1 } as never);
      }
      await queryRunner.commitTransaction();
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }

  async moveOne<T extends ObjectLiteral>(
    entity: EntityTarget<T>,
    field: OrderField,
    id: string,
    direction: 'up' | 'down',
    scopeWhere?: FindOptionsWhere<T>,
  ): Promise<void> {
    const repo = this.dataSource.getRepository(entity);
    const items = await repo.find({
      where: scopeWhere,
      order: { [field]: 'ASC' } as never,
    });

    const index = items.findIndex((row) => String(row.id) === id);
    if (index === -1) {
      throw new NotFoundException(`Record ${id} not found in scope`);
    }

    const targetIndex = direction === 'up' ? index - 1 : index + 1;
    if (targetIndex < 0 || targetIndex >= items.length) {
      throw new BadRequestException(`Cannot move ${direction}`);
    }

    const current = items[index] as Record<string, unknown>;
    const neighbor = items[targetIndex] as Record<string, unknown>;
    const currentOrder = Number(current[field]);
    const neighborOrder = Number(neighbor[field]);

    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      await queryRunner.manager.update(entity, String(current.id), { [field]: -1 } as never);
      await queryRunner.manager.update(entity, String(neighbor.id), { [field]: -2 } as never);
      await queryRunner.manager.update(entity, String(current.id), { [field]: neighborOrder } as never);
      await queryRunner.manager.update(entity, String(neighbor.id), { [field]: currentOrder } as never);
      await queryRunner.commitTransaction();
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }

  private async shiftOrdersFrom<T extends ObjectLiteral>(
    entity: EntityTarget<T>,
    field: OrderField,
    fromOrder: number,
    delta: number,
    scopeWhere?: FindOptionsWhere<T>,
  ): Promise<void> {
    const repo = this.dataSource.getRepository(entity);
    const orderColumn = repo.metadata.findColumnWithPropertyName(field)?.databaseName ?? field;
    const qb = repo
      .createQueryBuilder()
      .update()
      .set({ [field]: () => `"${orderColumn}" + ${delta}` } as never)
      .where(`"${orderColumn}" >= :fromOrder`, { fromOrder });

    if (scopeWhere) {
      Object.entries(scopeWhere).forEach(([key, value]) => {
        if (value !== undefined) {
          const col = repo.metadata.findColumnWithPropertyName(key)?.databaseName ?? key;
          qb.andWhere(`"${col}" = :scope_${key}`, { [`scope_${key}`]: value });
        }
      });
    }

    await qb.execute();
  }
}
