import { IMovie, IMovieDetails, IPaginatedResponse, MovieStatus } from "@autoplex-api/search";
import { InternalService } from "@autoplex/microservice";
import { env, secret } from "@autoplex/utils";
import { request } from "https";
import TheMovieDb, {
	ExternalSource,
	IMovieDetails as IRawMovieDetails,
	IMovieSearchResult
} from "../tmdb";

/**
 * How frequently to clear the movie details cache
 */
const CACHE_CLEAR_INTERVAL = 1000*60; // 60 seconds

export default class MovieSearch extends InternalService
{
	/**
	 * The name of the service
	 */
	public readonly NAME = "Movie Search";

	/**
	 * A reference to The Movie DB API
	 */
	protected tmdb!: TheMovieDb;

	/**
	 * Hold a cache of recently fetched movies to speed up request times
	 */
	protected movieCache: { [tmdbId: number]: { timestamp: number, movie: IMovieDetails } } = {};

	/**
	 * Hold the clear cache interval reference
	 */
	private __clearCacheInterval: NodeJS.Timeout|null = null;

	/**
	 * Boot the service
	 */
	public async boot() {
		let apiKey = await secret(env("TMDB_KEY_FILE"));
		this.tmdb = new TheMovieDb(apiKey);
	}

	/**
	 * Store an IMDb ID in cache
	 */
	protected cacheMovie(tmdbId: number, movie: IMovieDetails) {
		this.movieCache[tmdbId] = { movie, timestamp: Date.now() + CACHE_CLEAR_INTERVAL };
		if (this.__clearCacheInterval === null) {
			this.__clearCacheInterval = setInterval(() => this.cleanMovieCache(), CACHE_CLEAR_INTERVAL);
		}
		return this.movieCache[tmdbId];
	}

	/**
	 * Clean the IMDb cache
	 */
	protected cleanMovieCache() {
		let now = Date.now();
		let remaining = 0;
		for (let key in this.movieCache) {
			if (now > this.movieCache[key].timestamp) {
				delete this.movieCache[key];
			} else {
				remaining++;
			}
		}
		if (remaining == 0) {
			clearInterval(<NodeJS.Timeout>this.__clearCacheInterval);
			this.__clearCacheInterval = null;
		}
	}

	/**
	 * Format a general TMDb movie object
	 */
	protected formatMovie(movie: IMovieSearchResult) {
		return <IMovie> {
			backdropPath    : movie.backdrop_path     || null,
			originalLanguage: movie.original_language || null,
			originalTitle   : movie.original_title    || null,
			overview        : movie.overview          || null,
			posterPath      : movie.poster_path       || null,
			releaseDate     : movie.release_date      || null,
			title           : movie.title,
			tmdbId          : movie.id
		}
	}

	/**
	 * Format a TMDb movie details object
	 */
	protected formatMovieDetails(tmdbId: number, movie: IRawMovieDetails) {
		return <IMovieDetails> {
			backdropPath    : movie.backdrop_path     || null,
			imdbId          : movie.imdb_id           || null,
			originalLanguage: movie.original_language || null,
			originalTitle   : movie.original_title    || null,
			overview        : movie.overview          || null,
			posterPath      : movie.poster_path       || null,
			releaseDate     : movie.release_date      || null,
			runtime         : movie.runtime           || null,
			spokenLanguages : movie.spoken_languages,
			status          : movie.status,
			tagline         : movie.tagline || null,
			title           : movie.title,
			tmdbId
		}
	}

	// Interface -----------------------------------------------------------------------------------

	/**
	 * Verify the IMDb ID exists
	 */
	public verifyImdbId(imdbId: string) {
		return new Promise<boolean>((resolve, reject) => {
			let req = request({ method: "HEAD", host: "www.imdb.com", path: `/title/${imdbId}/` }, (response) => {
				response.resume();
				if (response.statusCode == undefined) {
					reject();
					return;
				}
				resolve(response.statusCode === 200);
				response.destroy();
			});
			req.end();
		});
	}

	/**
	 * Get the details of a movie
	 */
	public async details(tmdbId: number) {
		if (tmdbId in this.movieCache) {
			return this.movieCache[tmdbId].movie;
		}
		let movie = await this.tmdb.movie(tmdbId);
		let result = this.formatMovieDetails(tmdbId, movie);
		return this.cacheMovie(tmdbId, result).movie;
	}

	/**
	 * Find a movie by its IMDb ID
	 */
	public async findFromImdb(imdbId: string) {
		let movie = (await this.tmdb.findMovie(imdbId, ExternalSource.Imdb)).movie_results[0] ?? null;
		if (movie === null) {
			return null;
		}
		return this.formatMovie(movie);
	}

	/**
	 * Search for a movie
	 */
	public async search(query: string, year?: number, page?: number) {
		let results = await this.tmdb.searchMovie(query, year, page);
		return <IPaginatedResponse<IMovie>>{
			page: results.page,
			results: results.results.map(this.formatMovie),
			totalPages: results.total_pages,
			totalResults: results.total_results
		};
	}
}
