import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { CreatePlaceDateTradDto } from './dto/create-place-date.dto';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Place } from './entities/place.entity';
import { DataSource, Repository } from 'typeorm';
import { AvailableDate } from './entities/available-date.entity';
import { CreatePlaceDto } from './dto/create-place.dto';
import { CreateDateDto } from './dto/create-date.dto';
import { PlaceTraduction } from './entities/place-traduction.entity';
import { LANGUAGES } from 'src/shared/enum/languages.enum';
import { Town } from 'src/town/entities/town.entity';
import { GetPlaceDto } from './dto/get-place.dto';
import { ServerConstants } from 'src/constants/server.contants';
import { Available } from 'src/pointOfInterest/enum/available.enum';
import { UpdatePlaceReqDto } from './dto/update-place.req.dto';
import { Category } from 'src/category/entities/category.entity';
import { Visited } from 'src/visited/entities/visited.entity';
import { isPlaceOpen } from './utils/isPlaceOpen';

@Injectable()
export class PlaceService {
  constructor(
    @InjectRepository(Place) private placeRepository: Repository<Place>,
    @InjectRepository(AvailableDate) private availableDateRepository: Repository<AvailableDate>,
    @InjectRepository(PlaceTraduction) private placeTraductionRepository: Repository<PlaceTraduction>,
    @InjectRepository(Town) private townRepository: Repository<Town>,
    @InjectDataSource() private dataSource: DataSource,
  ) {}

  async create(createPlaceDto: CreatePlaceDateTradDto) {
    const town: Town = await this.townRepository.findOneBy({ townId: createPlaceDto.idTown });
    const categories: Category[] = [];
    for (const categoryId of createPlaceDto.categoriesId) {
      const categoryEN: Category = await this.dataSource
        .getRepository(Category)
        .findOneByOrFail({ idCategory: categoryId });
      categoryEN.language = LANGUAGES.EN;
      const categoryES: Category = { ...categoryEN, language: LANGUAGES.ES };
      categories.push(categoryEN);
      categories.push(categoryES);
    }
    const createPlace: CreatePlaceDto = {
      available: createPlaceDto.available,
      closeAt: createPlaceDto.closeAt,
      latitude: createPlaceDto.latitude,
      longitude: createPlaceDto.longitude,
      town: town,
      name: createPlaceDto.name,
      openAt: createPlaceDto.openAt,
      imageName: createPlaceDto.image.filename,
      categories: categories,
      address: createPlaceDto.address,
    };

    if (!town) {
      throw new BadRequestException('Town not found');
    }

    const insertedPlace = await this.placeRepository.save({ ...createPlace });
    const createDate: CreateDateDto = {
      endDate: createPlaceDto.endDate,
      startDate: createPlaceDto.startDate,
      idPlace: insertedPlace,
    };
    const createTradEn: PlaceTraduction = {
      language: LANGUAGES.EN,
      idPlace: insertedPlace.idPlace,
      description: createPlaceDto.descriptionEN,
    };
    const createTradEs: PlaceTraduction = {
      language: LANGUAGES.ES,
      idPlace: insertedPlace.idPlace,
      description: createPlaceDto.descriptionES,
    };

    // TODO: add transaction to rollback if one of the inserts fails
    await this.placeTraductionRepository.insert(createTradEs);
    await this.placeTraductionRepository.insert(createTradEn);
    if (createPlaceDto.available === Available.CUSTOM) {
      await this.availableDateRepository.insert({
        ...createDate,
        place: insertedPlace,
        idPlace: insertedPlace.idPlace,
      });
    }
  }

  async findAllByTown(idTown: number, lang: LANGUAGES): Promise<GetPlaceDto[]> {
    const res: any[] = await this.dataSource
      .getRepository(Place)
      .createQueryBuilder('place')
      .leftJoinAndSelect('place.availableDates', 'availableDate')
      .leftJoinAndSelect('place.categories', 'category')
      .leftJoinAndSelect('place.placeTraduction', 'placeTrad')
      .leftJoinAndSelect('place.town', 'town')
      .where('place.idTown = :idTown', { idTown: idTown })
      .andWhere('placeTrad.language = :language', { language: lang })
      .andWhere('category.language = :language', { language: lang })
      .getMany();
    const places: GetPlaceDto[] = res.map((place): GetPlaceDto => {
      return {
        idTown: place.town.townId,
        idPlace: place.idPlace,
        available: place.available,
        latitude: place.latitude,
        longitude: place.longitude,
        imageName: `${ServerConstants.HOST}/places/${place.imageName}`,
        name: place.name,
        openAt: place.openAt,
        closeAt: place.closeAt,
        startDate: place.availableDates[0]?.startDate || null,
        endDate: place.availableDates[0]?.endDate || null,
        categories: place.categories,
        description: place.placeTraduction[0].description,
        address: place.address,
      };
    });
    return places;
  }

  async findOne(id: number) {
    return await this.placeRepository.findOneBy({ idPlace: id });
  }

  async findOneAndTradAndAvailable(idPlace: number, lang: LANGUAGES) {
    const place: any = await this.dataSource
      .getRepository(Place)
      .createQueryBuilder('place')
      .leftJoinAndSelect('place.availableDates', 'availableDate')
      .leftJoinAndSelect('place.categories', 'category')
      .leftJoinAndSelect('place.placeTraduction', 'placeTrad')
      .leftJoinAndSelect('place.town', 'town')
      .where('place.idPlace = :idPlace', { idPlace })
      .andWhere('placeTrad.language = :language', { language: lang })
      .andWhere('category.language = :language', { language: lang })
      .getOne();
    const res: GetPlaceDto = {
      idTown: place.town.townId,
      idPlace: place.idPlace,
      available: place.available,
      latitude: place.latitude,
      longitude: place.longitude,
      imageName: `${ServerConstants.HOST}/places/${place.imageName}`,
      name: place.name,
      openAt: place.openAt,
      closeAt: place.closeAt,
      startDate: place.availableDates[0]?.startDate,
      endDate: place.availableDates[0]?.endDate,
      categories: place.categories,
      description: place.placeTraduction[0].description,
      address: place.address,
    };
    return res;
  }

  async update(idPlace: number, updatePlaceReqDto: UpdatePlaceReqDto) {
    const place: Place = await this.placeRepository.findOne({
      where: { idPlace },
      relations: ['categories'],
    });

    if (!place) {
      throw new NotFoundException('Place not found');
    }

    // Update place properties
    place.available = updatePlaceReqDto.available;
    place.closeAt = updatePlaceReqDto.closeAt;
    place.latitude = updatePlaceReqDto.latitude;
    place.longitude = updatePlaceReqDto.longitude;
    place.name = updatePlaceReqDto.name;
    place.openAt = updatePlaceReqDto.openAt;
    place.imageName = updatePlaceReqDto.image.filename;
    place.address = updatePlaceReqDto.address;

    // Update categories
    const categories: Category[] = [];
    for (const categoryId of updatePlaceReqDto.categoriesId) {
      const categoryEN: Category = await this.dataSource
        .getRepository(Category)
        .findOneByOrFail({ idCategory: categoryId });
      categoryEN.language = LANGUAGES.EN;
      const categoryES: Category = { ...categoryEN, language: LANGUAGES.ES };
      categories.push(categoryEN);
      categories.push(categoryES);
    }
    place.categories = categories;

    // Save the updated place
    await this.placeRepository.save(place);

    // Update PlaceTraduction for EN
    await this.placeTraductionRepository
      .createQueryBuilder()
      .update(PlaceTraduction)
      .set({ description: updatePlaceReqDto.descriptionEN })
      .where('idPlace = :idPlace AND language = :language', { idPlace, language: LANGUAGES.EN })
      .execute();

    // Update PlaceTraduction for ES
    await this.placeTraductionRepository
      .createQueryBuilder()
      .update(PlaceTraduction)
      .set({ description: updatePlaceReqDto.descriptionES })
      .where('idPlace = :idPlace AND language = :language', { idPlace, language: LANGUAGES.ES })
      .execute();

    // Update available date if necessary
    if (updatePlaceReqDto.available === Available.CUSTOM) {
      await this.availableDateRepository.upsert(
        {
          idPlace: place.idPlace,
          place: place,
          startDate: updatePlaceReqDto.startDate,
          endDate: updatePlaceReqDto.endDate,
        },
        ['idPlace'],
      );
    }
  }

  async findPlacesNotVisitedByUser(email: string, lang: LANGUAGES, idTown: number): Promise<GetPlaceDto[]> {
    const places: GetPlaceDto[] = await this.findAllByTown(idTown, lang);
    const visited: any[] = await this.dataSource
      .getRepository(Visited)
      .createQueryBuilder('visited')
      .leftJoinAndSelect('visited.place', 'place')
      .where('visited.user = :email', { email })
      .getMany();
    const visitedIds: number[] = visited.map((visit) => visit.place.idPlace);
    // podemos hacerlo en la query
    return places.filter((place) => !visitedIds.includes(place.idPlace));
  }

  async findPlacesNotVisitedByUserAndOpen(email: string, lang: LANGUAGES, idTown: number): Promise<GetPlaceDto[]> {
    const places: GetPlaceDto[] = await this.findPlacesNotVisitedByUser(email, lang, idTown);

    const placesWithAvailability = await Promise.all(
      places.map(async (place) => {
        if (place.available === Available.CUSTOM) {
          const availableDate = await this.availableDateRepository.findOneBy({ place });
          const isOpen = isPlaceOpen(place.available, availableDate?.startDate, availableDate?.endDate);
          return isOpen ? place : null;
        } else {
          return isPlaceOpen(place.available, null, null) ? place : null;
        }
      }),
    );

    return placesWithAvailability.filter((place) => place !== null);
  }
}
