import { RecommendPlace } from '../dto/recommend-route.dto';
import { DataFrame, Series } from './math';
import { customSort, sortByClose } from './sort';

export class RecommendationsSystem {
  private visited: RecommendPlace[] = [];
  private candidates: RecommendPlace[] = [];
  private start: number = 0;
  private end: number = 0;

  constructor(visited: RecommendPlace[], candidates: RecommendPlace[], start: number, end: number) {
    this.visited = visited;
    this.candidates = candidates;
    this.start = start;
    this.end = end;
  }

  private getCategories(places: RecommendPlace[]): number[] {
    const categories = new Set<number>();
    for (const place of places) {
      for (const category of place.categories) {
        categories.add(category);
      }
    }
    return Array.from(categories);
  }

  private oneHotEncode(categories: number[], placesToEncode: RecommendPlace[]): DataFrame {
    const data: DataFrame = {};
    for (const category of categories) {
      data[category] = [];
      for (const place of placesToEncode) {
        data[category].push(place.categories.includes(category) ? 1 : 0);
      }
    }
    return data;
  }

  private rankVisited(visited: RecommendPlace[]): Series {
    const visitedCategories = this.getCategories(visited);
    const visitedEncoded = this.oneHotEncode(visitedCategories, visited);
    const grades = visited.map((place) => place.rating);

    const dataframe: DataFrame = visitedEncoded;

    const result: Series = {};
    for (const category in dataframe) {
      result[category] = dataframe[category].reduce((sum, value, index) => sum + value * grades[index], 0);
    }

    const sumResult = Object.values(result).reduce((a, b) => a + b, 0);
    for (const category in result) {
      result[category] /= sumResult;
    }

    return result;
  }

  private rankCandidates(candidates: RecommendPlace[], visited: Series): [number, number][] {
    const visitedCategories = Object.keys(visited).map(Number);
    const candidatesEncoded = this.oneHotEncode(visitedCategories, candidates);

    const dataframe: DataFrame = candidatesEncoded;

    const result: number[] = candidates.map((_, i) => {
      return visitedCategories.reduce((sum, category) => {
        return sum + dataframe[category][i] * visited[category];
      }, 0);
    });

    const ranked: [number, number][] = candidates.map((place, i) => [place.idPlace, result[i]]);
    ranked.sort(customSort);

    return ranked;
  }

  takeTopNValid(recommendations: [number, number][]): RecommendPlace[] {
    const validRecommendations: RecommendPlace[] = [];

    // Filtrar los lugares que existen en candidates
    for (const [id] of recommendations) {
      const place = this.candidates.find((candidate) => candidate.idPlace === id);
      if (place) {
        validRecommendations.push(place);
      }
    }

    validRecommendations.sort(sortByClose);

    const chosen: RecommendPlace[] = [];
    let currentTime = this.start; // Tiempo inicial de la ruta

    for (const place of validRecommendations) {
      // Verificar si el lugar está abierto y si se puede acomodar dentro del horario
      const maxStart = Math.max(currentTime, place.openAt);
      const minEnd = Math.min(this.end, place.closeAt);

      if (minEnd - 2 >= maxStart) {
        // Verificar si aún hay tiempo suficiente en el rango total de la ruta
        if ((currentTime + 2) % 24 <= this.end) {
          // Actualizar el lugar con el nuevo horario dentro de la ruta
          place.openAt = Math.max(currentTime, place.openAt);
          place.closeAt = (place.openAt + 2) % 24;

          chosen.push(place);

          // Actualizar la hora actual para el siguiente lugar
          currentTime = (currentTime + 2) % 24;
        } else {
          // Si ya no hay tiempo suficiente para otro lugar, terminamos el bucle
          break;
        }
      }
    }

    return chosen;
  }

  recommend(): RecommendPlace[] {
    const visitedRanking = this.rankVisited(this.visited);
    const candidatesRanked = this.rankCandidates(this.candidates, visitedRanking);
    const chosen = this.takeTopNValid(candidatesRanked);
    return chosen;
  }
}
