/*
 * Contains all of the logic for interacting with JMAP
 * None of the dependencies should leak, in types or otherwise
 */
import * as base64 from "base-64";
import * as jmapclient from "jmap-client-ts";
import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport";

import { IAccount, IEmail, IMailbox, ISession } from "./types";

type Callback = () => void;

export interface IAuth {
	email: string;
	password: string;
}

export interface ClientState {
	session: ISession | null;
}

export default class Client {
	// Get the currently active account
	account(accountId: string): IAccount | null {
		if (!(this.state.session && this.state.session.accounts)) return null;
		return this.state.session.accounts[accountId];
	}
	// All objects which currently are listening for changes
	callbacks: Array<Callback> = [];
	jclient: jmapclient.Client | null = null;
	state: ClientState = {
		session: null,
	};

	onChange(f: Callback) {
		this.callbacks.push(f);
	}

	// Ensure we have the fully-fleshed email
	ensureEmailGet(accountId: string, emailId: string) {
		if (this.state.session == null) return;
		const existing = this.state.session.emails[emailId];
		if (existing != null) return;
		this.emailGet(accountId, emailId);
	}

	// Ensure we have the list of emails for the provided account and mailbox
	ensureEmailList(accountId: string, mailboxId: string) {
		const mailbox = this.mailbox(accountId, mailboxId);
		if (mailbox != null && mailbox.emailIds != null) return;
		this.emailList(accountId, mailboxId, []);
	}

	// Ensure that login happens. If this is called many times, only login once.
	ensureLogin(auth: IAuth) {
		if (this.jclient != null) return;
		this.doLogin(auth);
	}

	// Make the request to get system metadata
	doLogin(auth: IAuth) {
		const domain = auth.email.split("@")[1];
		const well_known_url = "https://" + domain + "/.well-known/jmap";
		const basic_auth =
			"Basic " + base64.encode(auth.email + ":" + auth.password);

		this.jclient = new jmapclient.Client({
			accessToken: "fake token",
			httpHeaders: { Authorization: basic_auth },
			sessionUrl: well_known_url,
			transport: new FetchTransport(fetch.bind(window)),
		});

		this.jclient
			.fetchSession()
			.then(() => {
				this._onSession();
			})
			.catch((error) => console.error(error));

		return;
	}

	email(emailId: string): IEmail | null {
		if (this.state.session == null) return null;
		return this.state.session.emails[emailId];
	}

	emailGet(accountId: string, emailId: string) {
		if (this.jclient == null) return;

		this.jclient
			.email_get({
				accountId: accountId,
				ids: [emailId],
				properties: ["mailboxIds", "subject"],
			})
			.then((response) => {
				console.log("Email response", response);
				response.list.forEach((e) => {
					if (this.state.session == null) return;
					const existing = this.state.session.emails[e.id];
					if (existing !== undefined && existing.subject !== e.subject) {
						console.error(
							"Expectations violation: email ID is not unique within the server!",
						);
					}
					this.state.session.emails[e.id] = e;
					this._triggerChange("Email " + e.id);
				});
			})
			.catch((x) => {
				console.error("Failed to get email", emailId, x);
			});
	}

	emailList(accountId: string, mailboxId: string, ids: Array<string>) {
		if (this.jclient == null) return;
		this.jclient
			.email_query({
				accountId: accountId,
				filter: { inMailbox: mailboxId },
			})
			.then((response) => {
				const mailbox = this.mailbox(accountId, mailboxId);
				if (mailbox == null) return;
				mailbox.emailIds = response.ids;
				this._triggerChange("Email list " + mailboxId);
			})
			.catch(() => {
				console.error("Failed to get email list from mailbox", mailboxId);
			});
	}

	mailbox(accountId: string, mailboxId: string): IMailbox | null {
		if (this.state.session == null) return null;
		const account = this.state.session.accounts[accountId];
		if (account.mailboxes == null) return null;
		for (let i = 0; i < account.mailboxes.length; i++) {
			if (account.mailboxes[i].id === mailboxId) {
				return account.mailboxes[i];
			}
		}
		return null;
	}
	mailboxList(accountId: string, ids: Array<string>) {
		if (this.jclient == null) return;
		if (this.state.session == null) return;
		// We already populated the list
		if (this.state.session.accounts[accountId].mailboxes != null) return;
		this.jclient
			.mailbox_get({
				accountId: accountId,
				ids: null,
			})
			.then((response) => {
				if (this.state.session == null) return;
				const account = this.state.session.accounts[response.accountId];
				const mailboxes: Array<IMailbox> = [];
				response.list.forEach((m) => {
					mailboxes.push({
						...m,
						emailIds: null,
						emails: null,
					});
				});
				account.mailboxes = mailboxes;
				this._triggerChange("Mailboxes " + accountId);
			});
	}

	_triggerChange(msg: string) {
		console.log("Client change", msg);
		this.callbacks.forEach((c) => {
			c();
		});
	}
	_onSession() {
		console.log("Session received");

		// For the type checker
		if (!this.jclient) return;

		const session = this.jclient.getSession();
		this.state.session = {
			...session,
			accounts: Object.fromEntries(
				Object.entries(session.accounts).map(([key, account]) => [
					key,
					{ ...account, id: key.toString(), mailboxes: null },
				]),
			),
			emails: {},
		};
		if (!this.state.session) return;
		this._triggerChange("Session");
	}
}
