import { HttpService } from '@nestjs/axios';
import {
  BadRequestException,
  HttpException,
  HttpStatus,
  Inject,
  Injectable,
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import { IncomingMessage } from 'http';
import { firstValueFrom, map, Observable } from 'rxjs';
import { ENGINE_MODULE_OPTIONS } from 'src/engine/engine.constants';
import { IEngineOptions, IEngineService } from 'src/engine/engine.interfaces';
import { Domain } from 'src/engine/models/domain.model';
import { Algorithm } from 'src/engine/models/experiment/algorithm.model';
import {
  Experiment,
  PartialExperiment,
} from 'src/engine/models/experiment/experiment.model';
import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input';
import { ExperimentEditInput } from 'src/engine/models/experiment/input/experiment-edit.input';
import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model';
import { Group } from 'src/engine/models/group.model';
import { Variable } from 'src/engine/models/variable.model';
import {
  dataToAlgorithms,
  dataToDataset,
  dataToExperiment,
  dataToGroup,
  dataToVariable,
  experimentInputToData,
} from './converters';
import { ExperimentData } from './interfaces/experiment/experiment.interface';
import { ExperimentsData } from './interfaces/experiment/experiments.interface';
import { Hierarchy } from './interfaces/hierarchy.interface';
import { Pathology } from './interfaces/pathology.interface';

@Injectable()
export default class ExaremeService implements IEngineService {
  headers = {};
  constructor(
    @Inject(ENGINE_MODULE_OPTIONS) private readonly options: IEngineOptions,
    private readonly httpService: HttpService,
    @Inject(REQUEST) private readonly req: Request, //TODO: remove inject, set request from manually take care of graphql request
  ) {
    const gqlRequest = req['req']; // graphql headers exception
    this.headers =
      gqlRequest && gqlRequest instanceof IncomingMessage
        ? gqlRequest.headers
        : req.headers;
  }

  async logout() {
    const path = `${this.options.baseurl}logout`;

    await firstValueFrom(this.httpService.get(path, { headers: this.headers }));
  }

  async createExperiment(
    data: ExperimentCreateInput,
    isTransient = false,
  ): Promise<Experiment> {
    const form = experimentInputToData(data);

    const path =
      this.options.baseurl + `experiments${isTransient ? '/transient' : ''}`;

    const resultAPI = await firstValueFrom(
      this.httpService.post<ExperimentData>(path, form, {
        headers: this.headers,
      }),
    );

    return dataToExperiment(resultAPI.data);
  }

  async listExperiments(page: number, name: string): Promise<ListExperiments> {
    const path = this.options.baseurl + 'experiments';

    const resultAPI = await firstValueFrom(
      this.httpService.get<ExperimentsData>(path, {
        params: { page, name },
        headers: this.headers,
      }),
    );

    return {
      ...resultAPI.data,
      experiments: resultAPI.data.experiments?.map(dataToExperiment) ?? [],
    };
  }

  async getAlgorithms(): Promise<Algorithm[]> {
    const path = this.options.baseurl + 'algorithms';

    const resultAPI = await firstValueFrom(
      this.httpService.get<string>(path, {
        headers: this.headers,
      }),
    );

    return dataToAlgorithms(resultAPI.data);
  }

  async getExperiment(id: string): Promise<Experiment> {
    const path = this.options.baseurl + `experiments/${id}`;

    const resultAPI = await firstValueFrom(
      this.httpService.get<ExperimentData>(path, {
        headers: this.headers,
      }),
    );

    return dataToExperiment(resultAPI.data);
  }

  async editExperient(
    id: string,
    expriment: ExperimentEditInput,
  ): Promise<Experiment> {
    const path = this.options.baseurl + `experiments/${id}`;

    const resultAPI = await firstValueFrom(
      this.httpService.patch<ExperimentData>(path, expriment, {
        headers: this.headers,
      }),
    );

    return dataToExperiment(resultAPI.data);
  }

  async removeExperiment(id: string): Promise<PartialExperiment> {
    const path = this.options.baseurl + `experiments/${id}`;

    try {
      await firstValueFrom(
        this.httpService.delete(path, {
          headers: this.headers,
        }),
      );
      return {
        id: id,
      };
    } catch (error) {
      throw new BadRequestException(`${id} does not exists`);
    }
  }

  async getDomains(ids: string[]): Promise<Domain[]> {
    const path = this.options.baseurl + 'pathologies';

    try {
      const data = await firstValueFrom(
        this.httpService.get<Pathology[]>(path, {
          headers: this.headers,
        }),
      );

      return (
        data?.data
          .filter((data) => !ids || ids.length == 0 || ids.includes(data.code))
          .map((data): Domain => {
            const groups = this.flattenGroups(data.metadataHierarchy);

            return {
              id: data.code,
              label: data.label,
              groups: groups,
              rootGroup: dataToGroup(data.metadataHierarchy),
              datasets: data.datasets ? data.datasets.map(dataToDataset) : [],
              variables: data.metadataHierarchy
                ? this.flattenVariables(data.metadataHierarchy, groups)
                : [],
            };
          }) ?? []
      );
    } catch (error) {
      throw new HttpException(
        `Error in exareme engine : '${error.response.data['message']}'`,
        error.response.data['statusCode'] ?? HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }

  getActiveUser(): Observable<string> {
    const path = this.options.baseurl + 'activeUser';

    return this.httpService
      .get<string>(path, {
        headers: this.req.headers,
      })
      .pipe(map((response) => response.data));
  }

  editActiveUser(): Observable<string> {
    const path = this.options.baseurl + 'activeUser/agreeNDA';

    return this.httpService
      .post<string>(path, this.req.body, {
        headers: this.req.headers,
      })
      .pipe(map((response) => response.data));
  }

  getExperimentREST(id: string): Observable<string> {
    const path = this.options.baseurl + `experiments/${id}`;

    return this.httpService
      .get<string>(path, {
        headers: this.req.headers,
      })
      .pipe(map((response) => response.data));
  }

  deleteExperiment(id: string): Observable<string> {
    const path = this.options.baseurl + `experiments/${id}`;

    return this.httpService
      .delete(path, {
        headers: this.req.headers,
      })
      .pipe(map((response) => response.data));
  }

  editExperimentREST(id: string): Observable<string> {
    const path = this.options.baseurl + `experiments/${id}`;

    return this.httpService
      .patch(path, this.req.body, {
        headers: this.req.headers,
      })
      .pipe(map((response) => response.data));
  }

  startExperimentTransient(): Observable<string> {
    const path = this.options.baseurl + 'experiments/transient';

    return this.httpService
      .post(path, this.req.body, {
        headers: this.req.headers,
      })
      .pipe(map((response) => response.data));
  }

  startExperiment(): Observable<string> {
    const path = this.options.baseurl + 'experiments';

    return this.httpService
      .post(path, this.req.body, {
        headers: this.req.headers,
      })
      .pipe(map((response) => response.data));
  }

  getExperiments(): Observable<string> {
    const path = this.options.baseurl + 'experiments';

    return this.httpService
      .get<string>(path, { params: this.req.query, headers: this.headers })
      .pipe(map((response) => response.data));
  }

  getAlgorithmsREST(): Observable<string> {
    const path = this.options.baseurl + 'algorithms';

    return this.httpService
      .get<string>(path, { params: this.req.query, headers: this.headers })
      .pipe(map((response) => response.data));
  }

  getPassthrough(suffix: string): string | Observable<string> {
    const path = this.options.baseurl + suffix;

    return this.httpService
      .get<string>(path, { params: this.req.query, headers: this.headers })
      .pipe(map((response) => response.data));
  }

  // UTILITIES
  private flattenGroups = (data: Hierarchy): Group[] => {
    let groups: Group[] = [dataToGroup(data)];

    if (data.groups) {
      groups = groups.concat(data.groups.flatMap(this.flattenGroups));
    }

    return groups;
  };

  private flattenVariables = (data: Hierarchy, groups: Group[]): Variable[] => {
    const group = groups.find((group) => group.id == data.code);
    let variables = data.variables ? data.variables.map(dataToVariable) : [];

    variables.forEach((variable) => (variable.groups = group ? [group] : []));

    if (data.groups) {
      variables = variables.concat(
        data.groups.flatMap((hierarchy) =>
          this.flattenVariables(hierarchy, groups),
        ),
      );
    }

    return variables;
  };
}