import { HttpService } from "@nestjs/axios";
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { Observable, map, take } from "rxjs";
import { PageDto } from "../domain/dtos";
import { EsQueryDto } from "../domain/dtos/elastic/es-query.dto";
import { RequestDto } from "../domain/dtos/request.dto";
import { SearchQueryDto } from "../domain/dtos/search-q.dto";
import { EsTime } from "../domain/enums/es-time.enum";
import { Order } from "../domain/enums/page-order.enum";
import { PageMeta } from "../domain/interfaces";
import { EsPit } from "../domain/interfaces/elastic/es-pit.interface";

/**
 * Previous search data storage
 */
class PrevSearch {
    /**
     * Constructs an uninitialized object
     */
    constructor() {
        this.pit = undefined;
        this.tiebreaker = undefined;
        this.prevPage = -1;
    }

    /**
     * PIT object of the previous search
     */
    private pit: EsPit;
    set _pit(pit: EsPit) {
        this.pit = pit;
    }
    get _pit(): EsPit {
        return this.pit;
    }

    /**
     * Tiebreaker and sort parameters
     */
    private tiebreaker: unknown[];
    set _tiebreaker(tiebreaker: unknown[]) {
        this.tiebreaker = tiebreaker;
    }
    get _tiebreaker(): unknown[] {
        return this.tiebreaker;
    }

    /**
     * Number of the previous page
     */
    private prevPage: number;
    set _prevPage(page: number) {
        this.prevPage = page;
    }
    get _prevPage(): number {
        return this.prevPage;
    }

    /**
     * Checks if there was the search before current one
     * @returns true/false, showing whether or not there was another search before
     */
    public isSet(): boolean {
        if (this.pit && this.tiebreaker && this.prevPage !== -1) return true;
        return false;
    }
}

/**
 * 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) {
        this.prevSearch = new PrevSearch;
    }

    /**
     * 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>> {
        let request: RequestDto = context.switchToHttp().getRequest<RequestDto>();
        const query: SearchQueryDto = request.query;
        let reverse: boolean = false;

        request.es_query = new EsQueryDto();

        request.es_query.query = {
            query_string: {
                query: query.query,
                default_field: 'content',
            }
        };
        request.es_query.sort = [
            { _score: { order: !query?.order ? Order.DESC : query.order } },
            { _shard_doc: 'desc' }
        ];

        if (this.prevSearch.isSet()) {
            request.es_query.pit = this.prevSearch._pit;
            request.es_query.search_after = this.prevSearch._tiebreaker;

            let limit = !query?.limit ? 10 : query.limit;
            request.es_query.size = limit * Math.abs(query.page - this.prevSearch._prevPage);
            
            if (query.page < this.prevSearch._prevPage) {
                request.es_query.sort = [{ _score: { order: 'asc' } }];
                request.es_query.size += limit - 1;
                reverse = true;
            } else if (query.page == this.prevSearch._prevPage) {
                // Caching should be HERE
                request.es_query.sort = [{ _score: { order: 'asc' } }];
                reverse = true;
            }
        } else {
            this.prevSearch._pit = request.es_query.pit = await this.getPIT(1);

            let limit = !query?.limit ? 10 : query.limit;
            request.es_query.size = limit * query.page;
        }

        return next.handle().pipe(
            map((res) => {
                // Setting the page meta-data
                let meta: PageMeta = {
                    total: res.hits.total.value,
                    pagenum: !query?.page ? 1 : +query.page,
                    order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC,
                    pagesize: !query?.limit ? 10 : query.limit,
                    hasNext: undefined,
                    hasPrev: undefined,
                }; 
                meta.hasNext = meta.pagenum * meta.pagesize < meta.total ? true : false;
                meta.hasPrev = meta.pagenum != 1 ? true : false;

                // Saving the search info
                this.prevSearch._pit.id = res.pit_id;
                this.prevSearch._tiebreaker = res.hits.hits[res.hits.hits.length - 1]?.sort;
                this.prevSearch._prevPage = query.page;

                // Check if the performed search is a backwards search
                let data = res.hits.hits.slice(-meta.pagesize);
                if (reverse) {
                    this.prevSearch._tiebreaker = data[0]?.sort;
                    data.reverse();
                    reverse = false;
                }

                // Omitting the redundant info and leaving only the document
                data = data.map((el) => el._source);

                // Return the page
                return new PageDto(data, meta);
            })
        );
    }

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

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

    /**
     * Info about previously completed search
     */
    private prevSearch: PrevSearch;

    /**
     * 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);
            }
        })
    }
}