import { HttpException, HttpService, Injectable, HttpStatus } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Observable } from 'rxjs';
import { AxiosResponse } from 'axios';
import { Structure, StructureDocument } from './schemas/structure.schema';
import { Logger } from '@nestjs/common';
import { structureDto } from './dto/structure.dto';
import { UsersService } from '../users/users.service';

@Injectable()
export class StructuresService {
  constructor(
    private readonly httpService: HttpService,
    private readonly userService: UsersService,
    @InjectModel(Structure.name) private structureModel: Model<StructureDocument>
  ) {}

  public async create(idUser: string, structureDto: structureDto): Promise<Structure> {
    const user = await this.userService.findOne(idUser);
    if (!user) {
      throw new HttpException('Invalid profile', HttpStatus.NOT_FOUND);
    }
    const createdStructure = new this.structureModel(structureDto);
    createdStructure.id = (await this.getNumberStructures()) + 1;
    createdStructure.save();
    user.structuresLink.push(createdStructure.id);
    user.save();

    return createdStructure;
  }

  public async search(searchString: string, filters?: Array<any>): Promise<Structure[]> {
    if (searchString && filters) {
      return this.structureModel
        .find({ $and: [...this.parseFilter(filters), { $text: { $search: searchString } }] })
        .exec();
    } else if (filters) {
      return this.structureModel.find({ $or: this.parseFilter(filters) }).exec();
    } else {
      return this.structureModel.find({ $or: [{ $text: { $search: searchString } }] }).exec();
    }
  }

  /**
   * Parse filter value from string to boolean
   * @param filters
   */
  private parseFilter(filters: Array<any>): Array<any> {
    return filters.map((filter) => {
      const key = Object.keys(filter)[0];
      if (filter[key] === 'True') {
        return { [key]: true };
      } else {
        return filter;
      }
    });
  }

  public async findAll(): Promise<Structure[]> {
    const structures = await this.structureModel.find().exec();
    // Update structures coord and address before sending them
    await Promise.all(
      structures.map((structure: Structure) => {
        // If structre has no address, add it
        if (!structure.address) {
          return this.getStructurePosition(structure).then((postition) => {
            this.structureModel
              .findOneAndUpdate({ id: structure.id }, { address: postition.address, coord: postition.coord })
              .exec();
          });
        }
        if (structure.coord.length <= 0) {
          return new Promise((resolve) => {
            this.getStructurePosition(structure).then((postition) => {
              this.structureModel
                .findOneAndUpdate({ id: postition.id }, { coord: postition.coord })
                .exec()
                .then(() => {
                  resolve('');
                });
            });
          });
        }
      })
    );
    return this.structureModel.find().exec();
  }

  public async update(idStructure: number, structure: structureDto): Promise<Structure> {
    const result = await this.structureModel.updateOne({ id: idStructure }, structure); //NOSONAR
    if (!result) {
      throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST);
    }
    return this.structureModel.findOne({ id: idStructure }).exec();
  }

  public findOne(idParam: number): Promise<Structure> {
    return this.structureModel.findOne({ id: idParam }).exec();
  }
  /**
   * Get structures positions and add marker corresponding to those positons on the map
   */
  private getStructurePosition(structure: Structure): Promise<Structure> {
    return new Promise((resolve) => {
      this.getCoord(structure.address.numero, structure.address.street, structure.address.commune).subscribe(
        (res) => {
          const address = res.data.features[0];
          structure.coord = address.geometry.coordinates;
          resolve(structure);
        },
        (err) => {
          Logger.error(`Request error: ${err.config.url}`, 'StructureService');
          Logger.error(err);
        }
      );
    });
  }

  public async isClaimed(idStructure): Promise<boolean> {
    const users = await this.userService.findAll();
    users.forEach((user) => {
      if (user.structuresLink.includes(idStructure)) {
        return true;
      }
    });
    return false;
  }

  /**
   * Count every value occurence of a given key
   * @param key structure key
   * @return [{id: 'key', count: 'value'}]
   */
  public async countByStructureKey(key: string): Promise<any> {
    const uniqueElements = await this.structureModel.distinct(key).exec();
    return await Promise.all(
      uniqueElements.map(async (value) => {
        return {
          id: value,
          count: await this.structureModel.countDocuments({ [key]: { $elemMatch: { $eq: value } } }).exec(),
        };
      })
    );
  }

  public getCoord(numero: string, address: string, zipcode: string): Observable<AxiosResponse<any>> {
    const req =
      'https://download.data.grandlyon.com/geocoding/photon/api' + '?q=' + numero + ' ' + address + ' ' + zipcode;
    Logger.log(`Request : ${req}`, 'StructureService - getCoord');
    return this.httpService.get(encodeURI(req));
  }

  public async getNumberStructures(): Promise<number> {
    return await this.structureModel.countDocuments(); //NOSONAR
  }
}