import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
import { DataSource, EntityManager } from 'typeorm';
import { SaveRoomDecorPlacementDto } from '../../../business/room_decor/dto/save_room_decor_placement.dto';
import { ResetRoomDecorPlacementsDto } from '../../../business/room_decor/dto/reset_room_decor_placements.dto';
import { RoomDecorLayoutState } from './room_decor_layout_state.entity';
import { RoomDecorPlacements } from './room_decor_placements.entity';

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

  async listPlacements(userId: bigint): Promise<RoomDecorPlacements[]> {
    return await this.dataSource.getRepository(RoomDecorPlacements).find({
      where: { user_id: userId },
      order: { created_at: 'ASC', id: 'ASC' },
    });
  }

  async getSnapshot(userId: bigint): Promise<{ revision: number; placements: RoomDecorPlacements[] }> {
    return await this.dataSource.transaction('REPEATABLE READ', async (manager) => {
      await this.ensureLayoutState(manager, userId);
      const state = await manager.getRepository(RoomDecorLayoutState).findOneByOrFail({ user_id: userId });
      const placements = await manager.getRepository(RoomDecorPlacements).find({
        where: { user_id: userId },
        order: { created_at: 'ASC', id: 'ASC' },
      });
      return { revision: state.revision, placements };
    });
  }

  async savePlacement(
    userId: bigint,
    placement: SaveRoomDecorPlacementDto,
  ): Promise<{ revision: number; placement: RoomDecorPlacements }> {
    return await this.dataSource.transaction(async (manager) => {
      const state = await this.lockLayoutState(manager, userId);
      const placementsRepository = manager.getRepository(RoomDecorPlacements);
      const decorId = this.normalizeDecorId(placement.decor_id);
      const mutationScope = `decor:${decorId}`;
      let row = await placementsRepository.findOne({
        where: { user_id: userId, decor_id: decorId },
        lock: { mode: 'pessimistic_write' },
      });
      if (this.isIdempotentRetry(state, placement, mutationScope)) {
        if (!row) throw new ConflictException('装修 mutation 状态不完整，请刷新布局');
        return { revision: state.revision, placement: row };
      }
      this.assertNextRevision(state, placement);
      if (!row) {
        row = new RoomDecorPlacements();
        row.user_id = userId;
        row.decor_id = decorId;
        row.created_at = new Date();
      }
      row.placed = placement.placed;
      row.position_x = placement.placed ? Number(placement.position_x ?? row.position_x ?? 0) : null;
      row.position_y = placement.placed ? Number(placement.position_y ?? row.position_y ?? 0) : null;
      row.scale = Number(placement.scale ?? row.scale ?? 1);
      row.rotation_degrees = Number(placement.rotation_degrees ?? row.rotation_degrees ?? 0);
      row.z_index = Number(placement.z_index ?? row.z_index ?? 0);
      row.mutation_revision = placement.mutation_revision;
      row.updated_at = new Date();
      const saved = await placementsRepository.save(row);
      await this.advanceLayoutState(manager, state, placement.mutation_revision, placement.mutation_id, mutationScope);
      return { revision: placement.mutation_revision, placement: saved };
    });
  }

  async resetPlacements(
    userId: bigint,
    mutation: ResetRoomDecorPlacementsDto,
  ): Promise<{ revision: number; placements: RoomDecorPlacements[] }> {
    return await this.dataSource.transaction(async (manager) => {
      const state = await this.lockLayoutState(manager, userId);
      if (!this.isIdempotentRetry(state, mutation, 'reset')) {
        this.assertNextRevision(state, mutation);
        await manager.getRepository(RoomDecorPlacements).createQueryBuilder()
          .update(RoomDecorPlacements)
          .set({
            placed: false,
            position_x: null,
            position_y: null,
            mutation_revision: mutation.mutation_revision,
          })
          .where('user_id = :userId', { userId: userId.toString() })
          .execute();
        await this.advanceLayoutState(manager, state, mutation.mutation_revision, mutation.mutation_id, 'reset');
      }
      const placements = await manager.getRepository(RoomDecorPlacements).find({
        where: { user_id: userId },
        order: { created_at: 'ASC', id: 'ASC' },
      });
      return { revision: state.revision, placements };
    });
  }

  private async ensureLayoutState(manager: EntityManager, userId: bigint): Promise<void> {
    await manager.getRepository(RoomDecorLayoutState).createQueryBuilder()
      .insert()
      .values({ user_id: userId, revision: 0, last_mutation_id: null, last_mutation_scope: null })
      .orIgnore()
      .execute();
  }

  private async lockLayoutState(manager: EntityManager, userId: bigint): Promise<RoomDecorLayoutState> {
    await this.ensureLayoutState(manager, userId);
    return await manager.getRepository(RoomDecorLayoutState).findOneOrFail({
      where: { user_id: userId },
      lock: { mode: 'pessimistic_write' },
    });
  }

  private isIdempotentRetry(
    state: RoomDecorLayoutState,
    mutation: Pick<SaveRoomDecorPlacementDto, 'mutation_revision' | 'mutation_id'>,
    mutationScope: string,
  ): boolean {
    return mutation.mutation_revision === state.revision
      && mutation.mutation_id === state.last_mutation_id
      && mutationScope === state.last_mutation_scope;
  }

  private assertNextRevision(
    state: RoomDecorLayoutState,
    mutation: Pick<SaveRoomDecorPlacementDto, 'layout_revision' | 'mutation_revision'>,
  ): void {
    if (mutation.layout_revision !== state.revision || mutation.mutation_revision !== state.revision + 1) {
      throw new ConflictException({
        message: '装修布局版本已过期，请刷新后重试',
        current_revision: state.revision,
      });
    }
  }

  private async advanceLayoutState(
    manager: EntityManager,
    state: RoomDecorLayoutState,
    revision: number,
    mutationId: string,
    mutationScope: string,
  ): Promise<void> {
    state.revision = revision;
    state.last_mutation_id = mutationId;
    state.last_mutation_scope = mutationScope;
    state.updated_at = new Date();
    await manager.getRepository(RoomDecorLayoutState).save(state);
  }

  private normalizeDecorId(decorId: string): string {
    const normalized = (decorId || '').trim();
    if (!/^[A-Za-z0-9_:-]{1,100}$/.test(normalized)) {
      throw new BadRequestException('摆件ID格式不正确');
    }
    return normalized;
  }
}
