import { ITorrentLink } from "@autoplex-api/torrent-search";
import { MovieTicket, MovieTorrent } from "@autoplex/database";
import { InternalService, Microservice } from "@autoplex/microservice";
import diskusage from "diskusage";
import { readdir } from "fs/promises";
import Supervisor from "./Supervisor";
import TorrentIpc from "./TorrentIpc";

 interface IPendingMovieTorrent {
	link: string,
	movie: MovieTicket
}

/**
 * A mapping of available disks
 */
interface IDiskMap {
	movies: string[],
	// tvshows: string[]
}

export default class TorrentManager extends InternalService
{
	/**
	 * The service name
	 */
	public readonly NAME = "Torrent Manager";

	/**
	 * The queue of movies to add to the client
	 */
	protected pendingMovies: IPendingMovieTorrent[] = [];

	/**
	 * Indicate if the service is currently adding movies to the torrent client
	 */
	protected isAddingMovies: boolean = false;

	/**
	 * Available movie disk names
	 */
	protected disks!: IDiskMap;

	/**
	 * The torrent IPC client
	 */
	protected torrentIpc!: TorrentIpc;

	/**
	 * Boot the Torrent Manager service
	 */
	public async boot() {
		this.log("Booting the torrent manager");
		await super.boot();
		await this.loadDisks();
	}

	/**
	 * Link to other services
	 */
	public link(app: Microservice) {
		this.torrentIpc = app.service<TorrentIpc>("Torrent");
	}

	/**
	 * Start the service
	 */
	public start() {
		this.torrentIpc.on("connected", this.onConnect.bind(this));
	}

	// Interface methods ---------------------------------------------------------------------------

	/**
	 * Add a movie to the queue
	 */
	public enqueueMovie(movie: MovieTicket, link: ITorrentLink) {
		if (link.type === "file")  {
			this.log("Warning: Non-magnet torrent links are not yet supported!");
			return;
		}
		this.pendingMovies.push({movie, link: link.link});
		this.addMovies();
	}

	// Volume Management ---------------------------------------------------------------------------

	/**
	 * Load available storage disks
	 */
	public async loadDisks() {
		this.disks = {
			movies: await readdir("/mnt/movies"),
			// tvshows: await readdir("/mnt/tvshows")
		}
	}

	/**
	 * Get the disk with the most space available
	 */
	public async findMostEmptyDisk(diskType: "movies") {
		let diskSizes = await Promise.all(this.disks[diskType].map(
			async diskName => (await diskusage.check(`/mnt/${diskType}/${diskName}`)).available
		));
		let minIndex = 0;
		for (let i = 1; i < diskSizes.length; i++) {
			if (diskSizes[i] < diskSizes[minIndex]) {
				minIndex = i;
			}
		}
		return this.disks[diskType][minIndex];
	}

	// Movie Management ----------------------------------------------------------------------------

	/**
	 * Add the movies in the queue to the torrent client
	 */
	protected async addMovies() {
		if (this.isAddingMovies || !this.torrentIpc.isConnected || this.pendingMovies.length == 0) {
			return;
		}
		this.isAddingMovies = true;
		let diskName = await this.findMostEmptyDisk("movies");
		while (this.pendingMovies.length > 0) {
			let {movie, link} = this.pendingMovies.splice(0, 1)[0];
			try {
				await this.addMovie(movie, link, diskName);
			} catch(e) {
				this.log("Failed to add torrent to client... Added to pending");
				this.pendingMovies.push({movie, link});
				break;
			}
		}
		this.isAddingMovies = false;
	}

	/**
	 * Add a movie to the torrent client
	 */
	public async addMovie(movie: MovieTicket, link: string, diskName: string) {
		try {
			let infoHash = await this.torrentIpc.add(link, `/mnt/movies/${diskName}/Downloads`);
			let torrent = new MovieTorrent();
			torrent.infoHash = infoHash;
			torrent.diskName = diskName;
			torrent.movieTicket = movie;
			await torrent.save();
		} catch(e) {
			console.log("Failed download the torrent");
			return false;
		}
		return true;
	}

	// Event Handling ------------------------------------------------------------------------------

	/**
	 * Invoked when the connection to the torrent client is established/re-established
	 */
	protected onConnect() {
		this.addMovies();
	}

	/**
	 * Invoked when a torrent has finished downloading
	 */
	protected async onTorrentFinished(infoHash: string) {
		let torrent = await MovieTorrent.findOne({
			where: { infoHash }, relations: ["movieTicket"]
		});
		if (torrent === undefined || torrent.movieTicket.isFulfilled || torrent.movieTicket.isCanceled) {
			return;
		}
		let details = (await this.torrentIpc.details([infoHash]))[0];
		this.app.service<Supervisor>("Supervisor").onMovieTorrentFinished(torrent, details);
	}
}
