import express					from "express";

import Imap						from "imap";

import { simpleParser }			from "mailparser";

import { promisify }			from "util";

import { once }					from "events";

import data						from "./data.json" with { type: "json" };


class ProtonMailIMAPReader {

	constructor ( config ) {

		this.imap				= new Imap( config );

		this.imap.once( "error", this.onImapError.bind( this ) );

		this.imap.once( "end", this.onImapEnd.bind( this ) );

		this.openBoxAsync		= promisify( this.imap.openBox.bind( this.imap ) );

		this.getBoxesAsync		= promisify( this.imap.getBoxes.bind( this.imap ) );

	}


	onImapError ( err ) {

		console.error( "IMAP Error:", err );

	}


	onImapEnd ( ) {

		console.log( "Connection ended" );

	}


	connect ( ) {

		this.imap.connect( );

		return once( this.imap, "ready" );

	}


	openBox ( boxName ) {

		return this.openBoxAsync( boxName, false );

	}


	async fetchEmails ( folder = "INBOX" ) {

		const box				= await this.openBox( folder );

		if ( !box || box.messages.total === 0 ) {

			return new Array( );

		}

		let range;

		if ( box.messages.total > 9 ) {

			range				= ( box.messages.total - 9 ) + ":" + box.messages.total;

		} else {

			range				= "1:" + box.messages.total;

		}

		const fetch				= this.imap.seq.fetch( range, { bodies: "", struct: true } );

		const messageBuffers	= { };

		fetch.on( "message", this.handleMessage.bind( this, messageBuffers ) );

		await once( fetch, "end" );

		const emails			= await this.parseMessages( messageBuffers );

		emails.sort( this.compareEmailsBySeqno.bind( this ) );

		return emails;

	}


	async parseMessages ( messageBuffers ) {

		const emails			= new Array( );

		const seqnos			= Object.keys( messageBuffers );

		for ( let i = 0 ; i < seqnos.length ; i ++ ) {

			const seqno		= seqnos[ i ];

			const parsed	= await simpleParser( messageBuffers[ seqno ] );

			const email		= this.prepareEmailObject( parseInt( seqno ), parsed );

			emails.push( email );

		}

		return emails;

	}


	compareEmailsBySeqno ( a, b ) {

		if ( a.seqno < b.seqno ) {

			return -1;

		}

		if ( a.seqno > b.seqno ) {

			return 1;

		}

		return 0;

	}


	handleMessage ( buffers, msg, seqno ) {

		const bufferChunks		= new Array( );

		msg.on( "body", this.onMessageBody.bind( this, bufferChunks ) );

		msg.once( "end", this.onMessageEnd.bind( this, buffers, seqno, bufferChunks ) );

	}


	onMessageBody ( bufferChunks, stream ) {

		stream.on( "data", this.onStreamData.bind( this, bufferChunks ) );

	}


	onStreamData ( bufferChunks, chunk ) {

		bufferChunks.push( chunk );

	}


	onMessageEnd ( buffers, seqno, bufferChunks ) {

		buffers[ seqno ]			= Buffer.concat( bufferChunks );

	}


	async fetchEmailBySeqno ( seqno, folder = "INBOX" ) {

		await this.openBox( folder );

		const fetch				= this.imap.seq.fetch( String( seqno ), { bodies: "", struct: true } );

		const messageBuffers	= { };

		fetch.on( "message", this.handleMessage.bind( this, messageBuffers ) );

		await once( fetch, "end" );

		if ( !messageBuffers[ seqno ] ) {

			return null;

		}

		const parsed			= await simpleParser( messageBuffers[ seqno ] );

		return this.prepareEmailObject( parseInt( seqno ), parsed );

	}


	prepareEmailObject ( seqno, parsed ) {

		const email				= {

			seqno,

			subject: 	parsed.subject ?? "(no subject)",

			from: 		parsed.from?.text ?? "(unknown sender)",

			date: 		parsed.date,

			text: 		parsed.text ?? ""

		};

		return email;

	}


	async markAsRead ( seqno, folder = "INBOX" ) {

		await this.openBox( folder );

		return this.addFlagsAsync( seqno, "\\Seen" );

	}


	async removeMessage ( seqno, folder = "INBOX" ) {

		await this.openBox( folder );

		const success	= await this.addFlagsAsync( seqno, "\\Deleted" );

		if ( !success ) {

			return false;

		}

		return this.expungeAsync( seqno );

	}


	addFlagsAsync ( seqno, flag ) {

		return new Promise( ( resolve ) => {

			this.imap.seq.addFlags( seqno, flag, this.onAddFlagsDone.bind( this, resolve ) );

		} );

	}


	onAddFlagsDone ( resolve, err ) {

		if ( err ) {

			console.error( "Error adding flags:", err );

		}

		resolve( !err );

	}


	expungeAsync ( seqno ) {

		return new Promise( ( resolve ) => {

			this.imap.seq.expunge( seqno, this.onExpungeDone.bind( this, resolve ) );

		} );

	}


	onExpungeDone ( resolve, err ) {

		if ( err ) {

			console.error( "Error expunging message:", err );

		}

		resolve( !err );

	}


	async listFolders ( ) {

		const boxes				= await this.getBoxesAsync( );

		const flat				= new Array( );

		this.flattenBoxes( boxes, "", flat );

		return flat;

	}


	flattenBoxes ( boxes, prefix, flat ) {

		for ( const name in boxes ) {

			const path			= prefix ? prefix + "/" + name : name;

			flat.push( path );

			if ( boxes[ name ].children ) {

				this.flattenBoxes( boxes[ name ].children, path, flat );

			}

		}

	}


	end ( ) {

		this.imap.end( );

	}

}


class ProtonMailServer {

	constructor ( ) {

		this.app				= express( );

		this.app.get( "/emails", this.getEmails.bind( this ) );

		this.app.use( "/emails/folder/", this.getFolderEmails.bind( this ) );

		this.app.get( "/message/:index", this.getEmailByIndex.bind( this ) );

		this.app.get( "/flag/read/:index", this.markEmailAsRead.bind( this ) );

		this.app.get( "/removeMessage/:message_id", this.removeMessageById.bind( this ) );

		this.app.get( "/folders", this.getFolders.bind( this ) );

	}


	formatJsonResponse ( res, data ) {

		const jsonString		= JSON.stringify( data, null, "\t" );

		res.setHeader( "Content-Type", "application/json" );

		res.send( jsonString );

	}


	async getEmails ( req, res ) {

		const reader			= new ProtonMailIMAPReader( data );

		await reader.connect( );

		const emails			= await reader.fetchEmails( );

		reader.end( );

		const filtered			= emails.map( this.filterEmailSummary );

		this.formatJsonResponse( res, {

			type: "emails", data: filtered, message: "Emails have been fetched."

		} );

	}


	filterEmailSummary ( email ) {

		const summary	= {

			seqno: email.seqno, subject: email.subject, from: email.from, date: email.date

		};

		return summary;

	}


async getFolderEmails ( req, res ) {

	let folderPath			= decodeURIComponent( req.path.replace( "/emails/folder/", "" ) );

	if ( folderPath.startsWith( "/" ) ) {

		folderPath			= folderPath.slice( 1 );

	}

	const reader			= new ProtonMailIMAPReader( data );

	await reader.connect( );

	let emails;

	try {

		emails				= await reader.fetchEmails( folderPath );

	} catch ( err ) {

		reader.end( );

		res.status( 500 ).json( { error: "Failed to fetch emails for folder: " + folderPath } );

		return;

	}

	reader.end( );

	const filtered			= emails.map( this.filterEmailSummary );

	this.formatJsonResponse( res, {

		type: "emails", folder: folderPath, data: filtered, message: "Emails have been fetched."

	} );

}



	async getEmailByIndex ( req, res ) {

		const seqno				= req.params.index;

		const reader			= new ProtonMailIMAPReader( data );

		await reader.connect( );

		const email				= await reader.fetchEmailBySeqno( seqno );

		reader.end( );

		if ( email === null ) {

			res.status( 404 ).json( { error: "Email not found" } );

		} else {

			this.formatJsonResponse( res, {

				type: "email", data: email, message: "Email fetched successfully."

			} );

		}

	}


	async markEmailAsRead ( req, res ) {

		const seqno				= req.params.index;

		const reader			= new ProtonMailIMAPReader( data );

		await reader.connect( );

		await reader.markAsRead( seqno );

		reader.end( );

		this.formatJsonResponse( res, { message: "Email marked as read." } );

	}


	async removeMessageById ( req, res ) {

		const seqno				= req.params.message_id;

		const reader			= new ProtonMailIMAPReader( data );

		await reader.connect( );

		const success			= await reader.removeMessage( seqno );

		reader.end( );

		if ( success ) {

			this.formatJsonResponse( res, { message: "Email removed successfully." } );

		} else {

			this.formatJsonResponse( res, { error: "Failed to remove email." } );

		}

	}


	async getFolders ( req, res ) {

		const reader			= new ProtonMailIMAPReader( data );

		await reader.connect( );

		const flatFolders		= await reader.listFolders( );

		reader.end( );

		this.formatJsonResponse( res, { folders: flatFolders } );

	}


	start ( port = 3000 ) {

		this.app.listen( port, this.onServerStarted.bind( this, port ) );

	}


	onServerStarted ( port ) {

		console.log( "ProtonMail IMAP server running on port " + port );

	}

}


const server			= new ProtonMailServer( );

server.start( 3000 );
