import { Inject, Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Character } from '../characters/entities/character.entity';
import { LocationConnection } from '../world/entities/location-connection.entity';
import { LocationDefinition } from '../world/entities/location-definition.entity';
import { CLOCK } from './clock';
import type { Clock } from './clock';
import { Travel } from './entities/travel.entity';
import {
  characterNotFound,
  invalidTravelState,
  invalidTravelTarget,
  travelAlreadyActive,
} from './travel.errors';
import { TravelStatus } from './travel-status.enum';

export interface LocationSummary {
  id: string;
  key: string;
  name: string;
}

export interface IdleTravelResponse {
  status: 'IDLE';
}

export interface TravellingResponse {
  status: TravelStatus.TRAVELLING;
  originLocation: LocationSummary;
  targetLocation: LocationSummary;
  startedAt: Date;
  arrivesAt: Date;
}

export interface CompletedTravelResponse {
  status: TravelStatus.COMPLETED;
  targetLocation: LocationSummary;
}

export type CurrentTravelResponse =
  IdleTravelResponse | TravellingResponse | CompletedTravelResponse;

@Injectable()
export class TravelService {
  constructor(
    private readonly dataSource: DataSource,
    @Inject(CLOCK) private readonly clock: Clock,
  ) {}

  startTravel(
    characterId: string,
    targetLocationId: string,
  ): Promise<TravellingResponse> {
    return this.dataSource.transaction(async (manager) => {
      const characters = manager.getRepository(Character);
      const travels = manager.getRepository(Travel);
      const locations = manager.getRepository(LocationDefinition);
      const connections = manager.getRepository(LocationConnection);

      const character = await this.lockCharacter(characters, characterId);
      const activeTravel = await this.lockActiveTravel(travels, characterId);
      if (activeTravel) {
        throw travelAlreadyActive();
      }

      const targetLocation = await locations.findOneBy({
        id: targetLocationId,
      });
      if (!targetLocation) {
        throw invalidTravelTarget();
      }

      const connection = await connections.findOneBy({
        fromLocationId: character.currentLocationId,
        toLocationId: targetLocationId,
        enabled: true,
      });
      if (!connection) {
        throw invalidTravelTarget();
      }

      const originLocation = await locations.findOneBy({
        id: character.currentLocationId,
      });
      if (!originLocation) {
        throw invalidTravelState();
      }

      const startedAt = new Date(this.clock.now().getTime());
      const arrivesAt = new Date(
        startedAt.getTime() + connection.travelDurationSeconds * 1000,
      );
      const travel = travels.create({
        characterId,
        originLocationId: character.currentLocationId,
        targetLocationId,
        startedAt,
        arrivesAt,
        status: TravelStatus.TRAVELLING,
      });
      await travels.save(travel);

      return this.toTravellingResponse(travel, originLocation, targetLocation);
    });
  }

  async getCurrentTravel(characterId: string): Promise<CurrentTravelResponse> {
    const travels = this.dataSource.getRepository(Travel);
    const travel = await travels.findOneBy({
      characterId,
      status: TravelStatus.TRAVELLING,
    });
    if (!travel) {
      return { status: 'IDLE' };
    }

    const locations = this.dataSource.getRepository(LocationDefinition);
    const { originLocation, targetLocation } = await this.loadTravelLocations(
      locations,
      travel,
    );
    return this.toTravellingResponse(travel, originLocation, targetLocation);
  }

  completeTravelIfDue(characterId: string): Promise<CurrentTravelResponse> {
    return this.dataSource.transaction(async (manager) => {
      const characters = manager.getRepository(Character);
      const travels = manager.getRepository(Travel);
      const locations = manager.getRepository(LocationDefinition);

      const character = await this.lockCharacter(characters, characterId);
      const travel = await this.lockActiveTravel(travels, characterId);
      if (!travel) {
        return { status: 'IDLE' };
      }

      const { originLocation, targetLocation } = await this.loadTravelLocations(
        locations,
        travel,
      );
      const now = this.clock.now();
      if (travel.arrivesAt.getTime() > now.getTime()) {
        return this.toTravellingResponse(
          travel,
          originLocation,
          targetLocation,
        );
      }

      travel.status = TravelStatus.COMPLETED;
      character.currentLocationId = travel.targetLocationId;
      await travels.save(travel);
      await characters.save(character);

      return {
        status: TravelStatus.COMPLETED,
        targetLocation: this.toLocationSummary(targetLocation),
      };
    });
  }

  private async lockCharacter(
    characters: Repository<Character>,
    characterId: string,
  ): Promise<Character> {
    const character = await characters.findOne({
      where: { id: characterId },
      lock: { mode: 'pessimistic_write' },
    });
    if (!character) {
      throw characterNotFound();
    }
    return character;
  }

  private lockActiveTravel(
    travels: Repository<Travel>,
    characterId: string,
  ): Promise<Travel | null> {
    return travels.findOne({
      where: { characterId, status: TravelStatus.TRAVELLING },
      lock: { mode: 'pessimistic_write' },
    });
  }

  private async loadTravelLocations(
    locations: Repository<LocationDefinition>,
    travel: Travel,
  ): Promise<{
    originLocation: LocationDefinition;
    targetLocation: LocationDefinition;
  }> {
    const [originLocation, targetLocation] = await Promise.all([
      locations.findOneBy({ id: travel.originLocationId }),
      locations.findOneBy({ id: travel.targetLocationId }),
    ]);
    if (!originLocation || !targetLocation) {
      throw invalidTravelState();
    }
    return { originLocation, targetLocation };
  }

  private toTravellingResponse(
    travel: Travel,
    originLocation: LocationDefinition,
    targetLocation: LocationDefinition,
  ): TravellingResponse {
    return {
      status: TravelStatus.TRAVELLING,
      originLocation: this.toLocationSummary(originLocation),
      targetLocation: this.toLocationSummary(targetLocation),
      startedAt: travel.startedAt,
      arrivesAt: travel.arrivesAt,
    };
  }

  private toLocationSummary(location: LocationDefinition): LocationSummary {
    return {
      id: location.id,
      key: location.key,
      name: location.name,
    };
  }
}
