import type { Validation } from '@vuelidate/core';
import type { OptionBuilder } from 'vue-facing-decorator/dist/optionBuilder';
import type { AjaxError } from './api-http';
import type { Language } from './enums/language';
import type { IValidation, ValidationState } from './static-wrappers/interfaces/validation-interface';
import { useVuelidate } from '@vuelidate/core';
import { Vue } from 'vue-facing-decorator';
import { globalState } from '../app/global-state';
import PowerduckState from '../app/powerduck-state';
import NotificationProvider from './../components/ui/notification';
import { TryCallApiResult } from './enums/api';
import StorageProvider from './local-storage-shim';
import ScrollUtils from './scroll-utils';
import { isNullOrEmpty } from './utils/is-null-or-empty';
import { PortalUtils } from './utils/utils';
import { ValidationHelper } from './validation';

export abstract class PowerduckViewModelBase extends Vue {
	blockRoot: boolean = true;
	authorized: boolean = true;
	v$: Validation;

	constructor(optionBuilder: OptionBuilder, vueInstance: any) {
		super(optionBuilder, vueInstance);
		this.v$ = useVuelidate({
			$scope: vueInstance,
		}) as any as Validation;
	}

	/**
	 * Current interface language
	 */
	get appLanguage(): Language {
		return PowerduckState.getCurrentLanguage();
	}

	/**
	 * Try GET data from Inviton API enpoint
	 */
	public async tryGetDataByArgs<TData, TArgs = Record<string, unknown>>(args: TryCallApiArgs<TData, TArgs>): Promise<TData> {
		return this.tryCallApiByArgs(args, false);
	}

	/**
	 * Try POST data to Inviton API endpoint
	 */
	public async tryPostDataByArgs<TData, TArgs>(args: TryCallApiArgs<TData, TArgs>): Promise<TryPostApiResponse<TData>> {
		const retVal = await this.tryCallApiByArgs(args, true);
		if (retVal != null && (retVal as any).ajaxErr != null) {
			return {
				data: null as any,
				error: (retVal as any).ajaxErr,
				result: TryCallApiResult.Error,
			};
		} else {
			return {
				data: retVal,
				error: null as any,
				result: TryCallApiResult.Success,
			};
		}
	}

	/**
	 * Try PATCH data to Inviton API endpoint
	 * just decorator
	 */
	public async tryPatchDataByArgs<TData, TArgs>(args: TryCallApiArgs<TData, TArgs>): Promise<TryPatchApiResponse<TData>> {
		return await this.tryPostDataByArgs(args);
	}

	public async tryDeleteDataByArgs<TData, TArgs>(args: TryCallApiArgs<TData, TArgs>): Promise<TData> {
		return this.tryCallApiByArgs(args, false);
	}

	private async tryCallApiByArgs<TData, TArgs>(args: TryCallApiArgs<TData, TArgs>, includeError: boolean): Promise<TData> {
		let retVal: TData;
		let handle: any = null;

		if (args.blockRoot != false) {
			handle = setTimeout(() => {
				this.blockRoot = true;
			}, 850);
		}

		const apiMethod = args.apiMethod as any;
		const promise = apiMethod(args.requestArgs, args.timeout);

		try {
			retVal = await promise;
		} catch (e: any) {
			let err: AjaxError = e;

			if (args.blockRoot != false) {
				this.blockRoot = false;
			}

			if (e == 'not authorized, token expired') {
				err = {
					authorized: false,
					responseText: PowerduckState.getResourceValue('loginExpired'),
				} as any;
			}

			if (err.authorized == false || e == 'not authorized, token expired') {
				globalState.loginModalRootInstance.show();
			}

			if (!err.authorized && args.toggleAuthorization) {
				this.authorized = err.authorized;
			}

			if (args.showError != false) {
				const parsedMsg = PowerduckState.parseErrorMessage(err.responseText);
				if (!isNullOrEmpty(parsedMsg)) {
					this.showErrorMessage(parsedMsg);
				} else if (err.responseText) {
					this.showErrorMessage(err.responseText);
				} else if ((err as any).message) {
					this.showErrorMessage((err as any).message);
				}
			}

			if (includeError) {
				retVal = {
					ajaxErr: err,
				} as any;
			} else {
				retVal = null as any;
			}
		}

		if (args.blockRoot != false) {
			try {
				clearTimeout(handle);
			} catch (error) { }

			if (this.blockRoot == true) {
				this.blockRoot = false;
			}
		}

		return retVal;
	}

	/**
	 * Get children components of given type
	 *
	 * @param typeName Name of the type
	 */
	getChildrenByType<T>(typeName: string): Array<T> {
		return PortalUtils.getChildrenByType(this, typeName);
	}

	/**
	 * Displays unobtrusive error message
	 * @param errMsg Error message
	 */
	showErrorMessage(errMsg: string): void {
		NotificationProvider.showErrorMessage(errMsg);
	}

	/**
	 * Displays unobtrusive success message
	 * @param successMsg Success message
	 */
	showSuccessMessage(successMsg: string): void {
		NotificationProvider.showSuccessMessage(successMsg);
	}

	/**
	 * Parse variable into number
	 * @param val
	 * @param defaultValue
	 */
	getNumericValue(val: any, defaultValue?: number) {
		if (val != null) {
			try {
				val = Number(val);
				if (isNaN(val)) {
					val = defaultValue;
				}
			} catch (e) {
				val = defaultValue;
			}
		} else {
			val = defaultValue;
		}

		return val;
	}

	/**
	 * Validates current viewModel state based on given valdiation ruleset
	 */
	async validate(showErrorMessage?: boolean, silent?: boolean): Promise<boolean> {
		if (StorageProvider.getString('disableValidation') == '1') {
			return true;
		}

		if (this.v$ == null) {
			throw new Error('Validation rules not specified, has to be specified in @Component declaration!');
		}

		const isInvalid = !(await this.v$.$validate());
		if (isInvalid) {
			if (showErrorMessage != false) {
				this.showValidationErrorMessage();
			}

			if (silent != true) {
				this.validationIncludeDirty = true;
				this.$nextTick(() => {
					this.scrollToFirstPossibleError(this.unwrapRootElement());
				});
			}
		}

		return !isInvalid;
	}

	unwrapRootElement(): HTMLElement {
		if ((this.$el as any).$getChildSlots != null) {
			return (((this.$el as any).$getChildSlots() || [])[0]?.el || this.$el) as any;
		}

		return this.$el as any;
	}

	showValidationErrorMessage() {
		this.showErrorMessage(PowerduckState.getResourceValue('errorsOnForm'));
	}

	scrollToFirstPossibleError(context: Element | null) {
		ScrollUtils.scrollToFirstPossibleError(context);
	}

	/**
	 * Scrolls to element
	 * @param elem
	 */
	scrollToElem(
		elem: typeof Vue | Element | typeof Vue[] | Element[],
		mobileOffset?: boolean | number,
		mobileOffsetSmoothing?: boolean,
		animated?: boolean,
		instant?: boolean,
	): void {
		ScrollUtils.scrollToElem(
			elem,
			mobileOffset,
			mobileOffsetSmoothing,
			animated,
			instant,
		);
	}

	/**
	 * Scrolls to position
	 */
	scrollToPos(
		position: number,
		context?: HTMLElement | null,
		animated?: boolean,
		instant?: boolean,
	): void {
		ScrollUtils.scrollToPos(
			position,
			context,
			animated,
			instant,
		);
	}

	/**
	 * Determines if DIRTY should be included in validation
	 */
	validationIncludeDirty: boolean = false;

	/**
	 * Obtains validation state of given property
	 * @param valProp Validation property
	 */
	validationStateOf(valProp: IValidation | IValidation[], customMessage?: string): ValidationState {
		let retVal: ValidationState = null as any;
		if (!PortalUtils.isArray(valProp)) {
			retVal = ValidationHelper.getValidationDisplayState(valProp as any, this.validationIncludeDirty);
		} else {
			let validationResult: ValidationState;
			for (let i = 0, len = (valProp as IValidation[]).length; i < len; i++) {
				validationResult = ValidationHelper.getValidationDisplayState(valProp[i], this.validationIncludeDirty);
				if (!validationResult.valid) {
					retVal = validationResult;
					break;
				}
			}
		}

		if (retVal?.valid == false) {
			if (!isNullOrEmpty(customMessage as any)) {
				retVal = retVal || {} as any;
				retVal.errorMessage = customMessage as string;
			}
		}

		return retVal;
	}

	/**
	 * Resets validation state of the viewModel
	 */
	resetValidation() {
		ValidationHelper.resetValidation(this);
		this.validationIncludeDirty = false;
	}
}

interface TryCallApiArgs<TData, TArgs> {
	apiMethod: (data?: TArgs) => Promise<TData>;
	timeout?: number;
	requestArgs?: TArgs;
	showError?: boolean;
	blockRoot?: boolean;
	toggleAuthorization?: boolean;
	toggleAuthorizationOnNullUser?: boolean;
}

export interface TryPostApiResponse<TData> {
	data: TData;
	result: TryCallApiResult;
	error: AjaxError;
}

export interface TryPatchApiResponse<TData> extends TryPostApiResponse<TData> { }
