import { HttpService } from "@nestjs/axios";
import { BadRequestException, CACHE_MANAGER, CallHandler, ExecutionContext, Inject, Injectable, InternalServerErrorException, NestInterceptor } from "@nestjs/common";
import { Observable, map, take, switchMap, of } from "rxjs";
import { PageDto } from "../domain/dtos";
import { EsTime } from "../domain/enums/es-time.enum";
import { Order, toOrder } from "../domain/enums/page-order.enum";
import { EsPit } from "../domain/interfaces/elastic/es-pit.interface";
import { Cache } from 'cache-manager'
import { PageMetaDto } from "../domain/dtos/page-meta.dto";

/**
 * Pagination-implementing interceptor
 */
@Injectable()
export class PageInterceptor implements NestInterceptor {
    /**
     * Injects needed dependencies and instantiates the storage object
     * @param httpService
     * @param searchService
     */
    constructor(
        private readonly httpService: HttpService,
        @Inject(CACHE_MANAGER) private cacheManager: Cache
    ) {}

    /**
     * Elastichsearch server port-number
     */
    private readonly ES_PORT = process.env.ES_PORT;

    /**
     * Elastichsearch IP address
     */
     private readonly ES_IP = process.env.ES_CONTAINER_NAME;

    /**
     * Override of intercept() method, specified in NestInterceptor interface
     * @param context
     * @param next
     * @returns Page with content and metadata
     */
    async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<PageDto>> {
        const query = context.switchToHttp().getRequest().query;

        const offset = query.offset;
        const limit = query.limit;
        const order = query.order;
        const query_string = query.query;

        const prev_page = await this.cacheManager.get('prev_page');
        if (prev_page) {
            if (offset == prev_page[1] && 
                limit == prev_page[2] && 
                order == prev_page[3] &&
                query_string === prev_page[4]) return of(prev_page[0]);
        }

        return next.handle().pipe(
            switchMap(async (res) => {
                // Setting the page meta-data
                let meta: PageMetaDto = {
                    total: res.hits.total.value,
                    order: toOrder(order),
                };

                // Check if the performed search is a backwards search
                let data = res?.hits?.hits;
                // Omitting the redundant info and leaving only the document
                data = data.map((el) => el._source);
                // Change the order if set
                if (order == Order.ASC) data.reverse();

                // Cache and return the page
                const page: PageDto = new PageDto(data, meta);
                await this.cacheManager.set('prev_page', [page, offset, limit, order, query_string]);
                return page;
            })
        );
    }

    /**
     * Acquires a PIT ID from Elasticsearch, needed for a request
     * @param alive, amount of time in minutes (defaults to 1). If time unit is not specified - defaults to minutes.
     * @returns PIT object <EsPit> containing PIT ID and keep_alive value
     */
    public async getPIT(alive: number, unit: EsTime = EsTime.min): Promise<EsPit> {
       return new Promise((resolve, reject) => {
           try {
               this.httpService.post<EsPit>(`http://${this.ES_IP}:${this.ES_PORT}/papers/_pit?keep_alive=${alive+unit}`)
                   .pipe(take(1), map(axiosRes => axiosRes.data))
                   .subscribe((res: EsPit) => {
                       res.keep_alive = alive + unit;
                       resolve(res);
                   });
           } catch (error) {
               reject(error);
           }
       });
    }

    /**
     * Deletes the PIT specified by provided ID
     * @param pitID, ID of the PIT, that would be deleted
     * @returns true/false, depending on the result of deletion of the PIT
     */
    async deletePIT(pitID: string): Promise<boolean> {
       return new Promise((resolve, reject) => {
           try {
               this.httpService.delete(`http://${this.ES_IP}:${this.ES_PORT}/_pit`, {
                   data: { id: pitID },
                   headers: { 'Content-Type': 'application/json' },
               })
               .pipe(take(1), map(axiosRes => axiosRes.data))
               .subscribe((res) => {
                   resolve(res.succeeded);
               });
           } catch (error) {
               reject(error);
           }
       })
    }
}