import {
  Body,
  Controller,
  Delete,
  Get,
  HttpException,
  HttpService,
  HttpStatus,
  Param,
  Post,
  Put,
  Query,
  UseGuards,
} from '@nestjs/common';
import { ApiParam } from '@nestjs/swagger';
import { Types } from 'mongoose';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CategoriesAccompagnementService } from '../categories/services/categories-accompagnement.service';
import { CategoriesFormationsService } from '../categories/services/categories-formations.service';
import { CategoriesOthersService } from '../categories/services/categories-others.service';
import { CreateTempUserDto } from '../temp-user/dto/create-temp-user.dto';
import { TempUserService } from '../temp-user/temp-user.service';
import { Roles } from '../users/decorators/roles.decorator';
import { IsStructureOwnerGuard } from '../users/guards/isStructureOwner.guard';
import { User } from '../users/schemas/user.schema';
import { UsersService } from '../users/users.service';
import { CreateStructureDto } from './dto/create-structure.dto';
import { QueryStructure } from './dto/query-structure.dto';
import { structureDto } from './dto/structure.dto';
import { Structure, StructureDocument } from './schemas/structure.schema';
import { StructuresService } from './services/structures.service';
import { RolesGuard } from '../users/guards/roles.guard';

@Controller('structures')
export class StructuresController {
  constructor(
    private readonly httpService: HttpService,
    private readonly structureService: StructuresService,
    private readonly userService: UsersService,
    private readonly tempUserService: TempUserService,
    private readonly categoriesFormationsService: CategoriesFormationsService,
    private readonly categoriesOthersService: CategoriesOthersService,
    private readonly categoriesAccompagnementService: CategoriesAccompagnementService
  ) {}

  /**
   * Return points of given town exist.
   * @param zipcode
   * @returns Array of points
   */
  @Get('coordinates/:zipcode')
  @ApiParam({ name: 'zipcode', type: String, required: true })
  public async getCoordinates(@Param('zipcode') city: string): Promise<any> {
    return await this.httpService
      .get(encodeURI('https://download.data.grandlyon.com/geocoding/photon-bal/api?q=' + city))
      .toPromise()
      .then(async (res) => res.data.features)
      .then((data) => data.filter((cityPoint) => cityPoint.properties.city.toLowerCase().includes(city.toLowerCase())))
      .then((data) => data.map((filteredCityPoint) => filteredCityPoint.geometry.coordinates));
  }

  @Post()
  public async create(@Body() createStructureDto: CreateStructureDto): Promise<Structure> {
    return this.structureService.create(createStructureDto.idUser, createStructureDto.structure);
  }

  @Post('search')
  public async search(@Query() query: QueryStructure, @Body() body): Promise<Structure[]> {
    return await this.structureService.searchForStructures(query.query, body ? body.filters : null);
  }

  @Post('resetSearchIndex')
  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  public async resetES(): Promise<StructureDocument[]> {
    return this.structureService.initiateStructureIndex();
  }

  @Put('updateAfterOwnerVerify/:id')
  public async updateAfterOwnerVerify(@Param('id') id: string): Promise<Structure> {
    return this.structureService.updateAccountVerified(id);
  }

  @Put(':id')
  @UseGuards(JwtAuthGuard, IsStructureOwnerGuard)
  @Roles('admin')
  public async update(@Param('id') id: string, @Body() body: structureDto): Promise<Structure> {
    return this.structureService.update(id, body);
  }

  @Get()
  public async findAll(): Promise<Structure[]> {
    return this.structureService.findAll();
  }

  @Get('formated')
  public async findAllFormated(): Promise<Structure[]> {
    const formationCategories = await this.categoriesFormationsService.findAll();
    const accompagnementCategories = await this.categoriesAccompagnementService.findAll();
    const otherCategories = await this.categoriesOthersService.findAll();
    return this.structureService.findAllFormated(formationCategories, accompagnementCategories, otherCategories);
  }

  @Post(':id/isClaimed')
  public async isClaimed(@Param('id') id: string, @Body() user?: User): Promise<boolean> {
    return this.structureService.isClaimed(id, user);
  }

  @Post(':id/claim')
  public async claim(@Param('id') idStructure: string, @Body() user: User): Promise<Types.ObjectId[]> {
    return this.userService.updateStructureLinkedClaim(user.email, idStructure);
  }

  @Post('count')
  public async countCategories(
    @Body()
    selectedFilter: { id: string; text: string }[]
  ): Promise<Array<{ id: string; count: number }>> {
    const data = await Promise.all([
      this.structureService.countByStructureKey('proceduresAccompaniment', selectedFilter),

      this.structureService.countByStructureKey('accessRight', selectedFilter),
      this.structureService.countByStructureKey('baseSkills', selectedFilter),
      this.structureService.countByStructureKey('parentingHelp', selectedFilter),
      this.structureService.countByStructureKey('digitalCultureSecurity', selectedFilter),
      this.structureService.countByStructureKey('socialAndProfessional', selectedFilter),

      this.structureService.countByStructureKey('publicsAccompaniment', selectedFilter),
      this.structureService.countByStructureKey('labelsQualifications', selectedFilter),
      this.structureService.countByStructureKey('publics', selectedFilter),
      this.structureService.countByStructureKey('accessModality', selectedFilter),
      this.structureService.countByStructureKey('equipmentsAndServices', selectedFilter),
    ]);
    // Return a concat of all arrays
    return data.reduce((a, b) => [...a, ...b]);
  }

  @Post('address')
  public async searchAddress(@Body() data: { searchQuery: string }) {
    return await this.structureService.searchAddress(data);
  }

  @Get(':id')
  public async find(@Param('id') id: string) {
    const result = await this.structureService.findOne(id);
    if (!result || result.deletedAt) {
      throw new HttpException('Structure does not exist', HttpStatus.NOT_FOUND);
    } else {
      return result;
    }
  }

  @Post(':id/withOwners')
  public async findWithOwners(@Param('id') id: string, @Body() data: { emailUser: string }) {
    return this.structureService.findWithOwners(id, data.emailUser);
  }

  @Delete(':id')
  @UseGuards(JwtAuthGuard, IsStructureOwnerGuard)
  @Roles('admin')
  @ApiParam({ name: 'id', type: String, required: true })
  public async delete(@Param('id') id: string) {
    return this.structureService.deleteOne(id);
  }

  @Post(':id/addOwner')
  @UseGuards(JwtAuthGuard, IsStructureOwnerGuard)
  @Roles('admin')
  @ApiParam({ name: 'id', type: String, required: true })
  public async addOwner(@Param('id') id: string, @Body() user: CreateTempUserDto): Promise<any> {
    // Get structure name
    const structure = await this.structureService.findOne(id);
    if (!structure) {
      throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND);
    }
    user.pendingStructuresLink = [Types.ObjectId(id)];
    // If user already exist, use created account
    if (await this.userService.verifyUserExist(user.email)) {
      return this.userService.updateStructureLinked(user.email, id);
    }
    // If temp user exist, update it
    if (await this.tempUserService.findOne(user.email)) {
      return this.tempUserService.updateStructureLinked(user);
    }
    // If not, create
    return this.tempUserService.create(user, structure.structureName);
  }

  @Post(':id/join')
  @ApiParam({ name: 'id', type: String, required: true })
  public async join(@Param('id') id: string, @Body() user: User): Promise<void> {
    // Get structure name
    const structure = await this.structureService.findOne(id);
    if (!structure) {
      throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND);
    }
    // Get user and add pending structure
    const userFromDb = await this.userService.findOne(user.email);
    if (!userFromDb) {
      throw new HttpException('Invalid User', HttpStatus.NOT_FOUND);
    }
    // If user has not already request it, send owner's validation email
    if (!userFromDb.pendingStructuresLink.includes(Types.ObjectId(id))) {
      userFromDb.pendingStructuresLink.push(Types.ObjectId(id));
      userFromDb.save();
      // Send structure owner's an email
      this.structureService.sendStructureJoinRequest(userFromDb, structure);
    }
  }

  @Post(':id/join/:userId/:status')
  @UseGuards(JwtAuthGuard, IsStructureOwnerGuard)
  @ApiParam({ name: 'id', type: String, required: true })
  @ApiParam({ name: 'userId', type: String, required: true })
  @ApiParam({ name: 'status', type: String, required: true })
  public async joinValidation(
    @Param('id') id: string,
    @Param('status') status: string,
    @Param('userId') userId: string
  ): Promise<any> {
    // Get structure name
    const structure = await this.structureService.findOne(id);
    if (!structure) {
      throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND);
    }

    // Get user and add pending structure
    const userFromDb = await this.userService.findById(userId);
    if (!userFromDb) {
      throw new HttpException('Invalid User', HttpStatus.NOT_FOUND);
    }

    if (!userFromDb.pendingStructuresLink.includes(Types.ObjectId(id))) {
      throw new HttpException('User not linked to structure', HttpStatus.NOT_FOUND);
    }

    if (status === 'true') {
      // Accept
      await this.userService.updateStructureLinked(userFromDb.email, id);
      await this.userService.removeFromPendingStructureLinked(userFromDb.email, id);
    } else {
      // Refuse
      this.userService.removeFromPendingStructureLinked(userFromDb.email, id);
    }
  }

  @Delete(':id/owner/:userId')
  @UseGuards(JwtAuthGuard, IsStructureOwnerGuard)
  @Roles('admin')
  @ApiParam({ name: 'id', type: String, required: true })
  @ApiParam({ name: 'userId', type: String, required: true })
  public async removeOwner(@Param('id') id: string, @Param('userId') userId: string): Promise<void> {
    // Get structure
    const structure = await this.structureService.findOne(id);
    if (!structure) {
      throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND);
    }
    // Get user
    const userFromDb = await this.userService.findById(userId);
    if (!userFromDb || !userFromDb.structuresLink.includes(Types.ObjectId(id))) {
      throw new HttpException('Invalid User', HttpStatus.NOT_FOUND);
    }
    this.userService.removeFromStructureLinked(userFromDb.email, id);
  }

  @Post('reportStructureError')
  public async reportStructureError(@Body() data: { structureId: string; content: string }): Promise<void> {
    return await this.structureService.reportStructureError(data.structureId, data.content);
  }
}