/*
 Copyright (C) 2020 - 2026 3NSoft Inc.
 
 This program is free software: you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
 Foundation, either version 3 of the License, or (at your option) any later
 version.
 
 This program is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 See the GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License along with
 this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { Core, CoreConf, makeNetClient } from "core-3nweb-client-lib";
import { logError, NOTIFICATIONS_SYSTEM_COMPONENT, SYSTEM_DOMAIN } from "../confs";
import { makeServiceLocator } from "core-3nweb-client-lib";
import { promises as dns } from 'dns';
import { GetServiceToHandleNewCall, makeClientSideConnector, ClientSideConnector } from "../rpc";
import { Component } from "../app-n-components";
import { Notifications } from "../shell/user-notifications";
import { Deferred, defer } from "../lib-common/processes/deferred";
import { StartAppWithCmd } from "../shell/cmd-invocation";
import { GetAppFSResourceFor } from "../shell/fs-resource";
import { AppSetter, SiteCAPsAndSetup, makeCAPsSetAppAndCloseFns } from "./caps";
import { makeRpcCAP } from "./caps/rpc";
import { connectivityCAP } from "./caps/connectivity";
import { makeSystemCAP } from "./caps/system";
import { makeShellCAPs } from "./caps/shell";
import { makeMediaDevicesCAP } from "./caps/mediaDevices";
import { makeUICap } from "../ui";
import { Connectivity, makeConnectivity } from "../connectivity";
import { UserMounts } from "../shell/mounts/user-mounts";
import { makeNativeCryptor } from "napi-nacl";

type SysUtils = web3n.system.SysUtils;
type Logout = web3n.system.Logout;
type AppComponent = web3n.caps.AppComponent;
type SiteComponent = web3n.caps.SiteComponent;
type CmdParams = web3n.shell.commands.CmdParams;
type W3N = web3n.system.W3N;
type SitesW3N = web3n.caps.sites.W3N;
type GUIComponentDef = web3n.caps.GUIComponent;
type OpenDashboard = web3n.shell.OpenDashboard;
type ProgressCB = web3n.startup.ProgressCB;

export type MakeUserMount = (userId: string) => UserMounts;

export function makeCoreDriver(
	conf: CoreConf, makeSystemCapFns: () => SysUtils,
	startAppWithCmd: StartAppWithCmd, openDashboard: OpenDashboard,
	logout: Logout, getService: GetServiceToHandleNewCall,
	getAppFSResourceFor: GetAppFSResourceFor, makeUserMounts: MakeUserMount|undefined
): CoreDriver {
	return new CoreDriver(
		conf, makeSystemCapFns, startAppWithCmd, openDashboard, logout, getService, getAppFSResourceFor,
		makeUserMounts
	);
}

export class CoreDriver {

	private readonly core: Core;
	private signedUser: string|undefined = undefined;
	private readonly rpcClientSide: ClientSideConnector;
	private readonly userNotifications: Notifications;
	private readonly connectivity: Connectivity;
	private coreReady: Deferred<void>|undefined = defer();
	private fsMounts: UserMounts|undefined = undefined;

	constructor(
		conf: CoreConf,
		private readonly makeSystemCapFns: () => SysUtils,
		private readonly startAppWithCmd: StartAppWithCmd,
		private readonly openDashboard: OpenDashboard,
		private readonly logout: Logout,
		getService: GetServiceToHandleNewCall,
		private readonly getAppFSResourceFor: GetAppFSResourceFor,
		makeUserMounts: MakeUserMount|undefined
	) {
		this.core = Core.make(
			conf,
			makeNetClient,
			makeServiceLocator({
				resolveTxt: hostname => dns.resolveTxt(hostname)
			}),
			makeNativeCryptor
		);
		this.rpcClientSide = makeClientSideConnector(getService);
		this.userNotifications = new Notifications((appDomain, { cmd, params }) => this.startAppWithCmd(
			SYSTEM_DOMAIN, NOTIFICATIONS_SYSTEM_COMPONENT, appDomain, cmd, ...params
		));
		this.connectivity = makeConnectivity(this.whenReady().then(() => this.core.connectivityEvents));
		this.coreReady!.promise.then(async () => {
			this.fsMounts = makeUserMounts?.(this.signedUser!);
		}).catch(noop);
		Object.seal(this);
	}

	async close(): Promise<void> {
		this.connectivity.close();
		this.userNotifications.close();
		await this.fsMounts?.close().catch(logError);
		await this.core.close().catch(logError);
	}

	start(): { capsForStartup: web3n.startup.W3N; coreInit: Promise<void>; } {
		if (!this.core) { throw new Error(`Core is already closed`); }
		const { capsForStartup, coreInit, coreAppsInit } = this.core.start();
		return {
			capsForStartup,
			coreInit: coreInit.then(
				userId => this.doAfterInit(userId, coreAppsInit),
				err => {
					this.coreReady?.reject(err);
					throw err;
				}
			)
		};
	}

	async startCoreDirectlyFor(userId: string, storageKey: Uint8Array): Promise<void> {
		if (!this.core) { throw new Error(`Core is already closed`); }
		const procStarted = await this.core.startDirectlyFromCache(userId, storageKey);
		if (!procStarted) {
			throw `Is storage key correct?`;
		}
		const { coreInit } = procStarted;
		await coreInit;
		this.doAfterInit(userId);
	}

	async getRootKey(user: string, pass: string, progressCB: ProgressCB): Promise<Uint8Array|undefined> {
		if (!this.core) { throw new Error(`Core is already closed`); }
		return this.core.getRootKey(user, pass, progressCB);
	}

	async whenReady(): Promise<void> {
		if (this.coreReady) {
			await this.coreReady.promise;
		}
	}

	private async doAfterInit(userId: string, coreAppsInit?: Promise<void>): Promise<void> {
		this.signedUser = userId;
		this.userNotifications.setUserId(this.signedUser);
		await coreAppsInit;
		if (this.coreReady) {
			this.coreReady.resolve();
			this.coreReady = undefined;
		}
	}

	isStarted(): boolean {
		return !!this.signedUser;
	}

	getUserId(): string {
		if (this.signedUser) {
			return this.signedUser;
		} else {
			throw new Error(`Core is not initialized`);
		}
	}

	get storages() {
		if (!this.core) { throw new Error(`Core is already closed`); }
		return this.core.getStorages();
	}

	makeCAPsForAppComponent(
		appDomain: string, appVersion: string, component: string,
		componentDef: AppComponent, startCmd: CmdParams|undefined
	): { w3n: W3N; close: () => void; setApp: AppSetter; } {
		if (!this.core) { throw new Error(`Core is already closed`); }
		const capsReq = (componentDef.capsRequested ?
			componentDef.capsRequested : {}
		);
		const baseW3N = this.core.makeCAPsForApp(appDomain, capsReq);
		const { log, keyrings, mail, mailerid, storage } = baseW3N.caps;
		const closeSelf = this.closeSelfCAP();
		const shell = makeShellCAPs(
			appDomain, component, capsReq,
			(componentDef as GUIComponentDef).startCmds, startCmd,
			this.getAppFSResourceFor,
			this.userNotifications,
			this.startAppWithCmd,
			this.openDashboard,
			this.fsMounts
		);
		const rpc = makeRpcCAP(
			this.rpcClientSide, appDomain, componentDef, capsReq
		);
		const mediaDevices = makeMediaDevicesCAP(capsReq.mediaDevices);
		const connectivity = connectivityCAP(capsReq.connectivity, this.connectivity);
		const { close, setApp } = makeCAPsSetAppAndCloseFns(
			shell, rpc, mediaDevices, baseW3N, closeSelf
		);
		const w3n: W3N = {
			log, keyrings, mail, mailerid, storage,
			myVersion: async () => appVersion,
			ui: makeUICap(),
			closeSelf: closeSelf.cap,
			system: makeSystemCAP(
				this.makeSystemCapFns,
				this.logout,
				(capsReq as web3n.system.RequestedCAPs).system
			),
			shell: shell?.cap,
			rpc: rpc?.cap,
			connectivity: connectivity?.cap,
			mediaDevices: mediaDevices?.cap,
		};
		return { w3n, close, setApp };
	}

	makeCAPsForSiteComponent(
		siteDomain: string, component: string, componentDef: SiteComponent
	): SiteCAPsAndSetup {
		const w3n: SitesW3N = {};
		return { w3n };
	}

	private closeSelfCAP(): { cap: W3N['closeSelf']; setApp: AppSetter; } {
		let self: Component = undefined as any;
		const cap: W3N['closeSelf'] = () => {
			if (self) {
				self.close();
			}
		};
		const setApp: AppSetter = app => { self = app; };
		return { cap, setApp };
	}

	async onDeviceSystemSuspend(): Promise<void> {
		await this.core.onDeviceSystemSuspend();
	}

	async onDeviceSystemResume(): Promise<void> {
		await this.core.onDeviceSystemResume();
	}

}
Object.freeze(CoreDriver.prototype);
Object.freeze(CoreDriver);


function noop() {}


Object.freeze(exports);
