/**
 * External dependencies
 */
import { isString, isEqual, map, isEmpty } from 'lodash';
import { updatedDiff } from 'deep-object-diff';
import { JsonPointer } from 'json-ptr';

/**
 * WordPress dependencies
 */
import { controls } from '@wordpress/data';
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import { getAjv, WPError } from '@ithemes/security-utils';
import { apiFetch, apiFetchBatch, createNotice } from '../controls';
import { STORE_NAME } from './constant';

export function* editModule( module, edit ) {
	const current = yield controls.select( STORE_NAME, 'getModule', module );

	if ( current ) {
		yield { type: EDIT_MODULE, module, edit: updatedDiff( current, edit ) };
	} else {
		yield { type: EDIT_MODULE, module, edit };
	}
}

/**
 * Saves an edited module.
 *
 * @param {boolean|string|Array<string>} modules The modules to save. By default, all dirty modules will be saved.
 * @return {Array<Object>} The list of saved modules responses.
 */
export function* saveModules( modules = true ) {
	if ( modules === true ) {
		modules = yield controls.select( STORE_NAME, 'getDirtyModules' );
	} else if ( isString( modules ) ) {
		modules = [ modules ];
	}

	if ( ! modules.length ) {
		return [];
	}

	const requests = [];

	for ( const module of modules ) {
		const edits = yield controls.select(
			STORE_NAME,
			'getModuleEdits',
			module
		);

		requests.push( {
			method: 'PUT',
			path: `/ithemes-security/v1/modules/${ module }`,
			body: edits,
		} );
	}

	let responses;

	try {
		yield { type: START_SAVING_MODULES, modules };
		responses = yield apiFetchBatch( requests );
	} catch ( error ) {
		yield { type: FAILED_SAVING_MODULES, modules };
		yield createNotice( 'error', error.message );

		return error;
	}

	const success = [];
	const errors = {};

	for ( let i = 0; i < requests.length; i++ ) {
		const module = modules[ i ];
		const response = responses[ i ];

		if ( response.status >= 400 ) {
			errors[ module ] = response.body;
		} else {
			success.push( module );
			yield receiveModule( response.body );
		}
	}

	if ( ! isEmpty( errors ) ) {
		yield { type: FAILED_SAVING_MODULES, errors };
	}

	if ( success.length ) {
		yield { type: FINISH_SAVING_MODULES, modules: success };
	}

	return responses;
}

/**
 * Resets the edits for a module.
 *
 * @param {boolean|string|Array<string>} modules The modules to reset. By default, all dirty modules will be reset.
 */
export function* resetModuleEdits( modules = true ) {
	if ( modules === true ) {
		modules = yield controls.select( STORE_NAME, 'getDirtyModules' );
	} else if ( isString( modules ) ) {
		modules = [ modules ];
	}

	yield {
		type: RESET_MODULE_EDITS,
		modules,
	};
}

export function* activateModule( module ) {
	try {
		const response = yield updateModule( module, 'active' );
		yield receiveModule( response );
		yield { type: FINISH_SAVING_MODULES, modules: [ module ] };

		if ( response.side_effects ) {
			yield fetchModules();
		}
		yield createNotice(
			'success',
			__( 'Activated feature', 'better-wp-security' ),
			{
				type: 'snackbar',
			}
		);
	} catch ( error ) {
		yield { type: FAILED_SAVING_MODULES, errors: { [ module ]: error } };
	}
}

export function* deactivateModule( module ) {
	try {
		const response = yield updateModule( module, 'inactive' );
		yield receiveModule( response );
		yield { type: FINISH_SAVING_MODULES, modules: [ module ] };

		if ( response.side_effects ) {
			yield fetchModules();
		}
		yield createNotice(
			'success',
			__( 'Deactivated feature', 'better-wp-security' ),
			{
				type: 'snackbar',
			}
		);
	} catch ( error ) {
		yield { type: FAILED_SAVING_MODULES, errors: { [ module ]: error } };
	}
}

export function* setModulesStatus( modules ) {
	const batch = {
		requests: map( modules, ( status, module ) => ( {
			path: `/ithemes-security/v1/modules/${ module }`,
			method: 'PUT',
			body: {
				status: {
					selected: status,
				},
			},
		} ) ),
	};

	const responses = yield apiFetchBatch( batch );

	for ( let i = 0; i < responses.length; i++ ) {
		const response = responses[ i ];

		if ( response.status >= 400 ) {
			yield createNotice( 'error', response.body.message );
		} else {
			yield receiveModule( response.body );
		}
	}
}

export function* editSettings( module, settings ) {
	const current = yield controls.select( STORE_NAME, 'getSettings', module );

	if ( ! current ) {
		yield { type: EDIT_SETTINGS, module, edit: settings };
		return;
	}

	const edit = {};
	let hasChanges = false;

	for ( const setting in settings ) {
		if ( ! settings.hasOwnProperty( setting ) ) {
			continue;
		}

		if ( ! isEqual( settings[ setting ], current[ setting ] ) ) {
			edit[ setting ] = settings[ setting ];
			hasChanges = true;
		}
	}

	if ( hasChanges ) {
		yield { type: EDIT_SETTINGS, module, edit };
	} else {
		yield resetSettingEdits( module );
	}
}

export function* editSetting( module, setting, value ) {
	const current = yield controls.select(
		STORE_NAME,
		'getSetting',
		module,
		setting
	);

	if ( isEqual( current, value ) ) {
		yield { type: RESET_SETTING_EDIT, module, setting };
	} else {
		yield {
			type: EDIT_SETTING,
			module,
			setting,
			value,
		};
	}
}

/**
 * Resets the edited settings for a module.
 *
 * @param {boolean|string|Array<string>} modules The modules to reset. By default, all dirty modules will be reset.
 */
export function* resetSettingEdits( modules = true ) {
	if ( modules === true ) {
		modules = yield controls.select( STORE_NAME, 'getDirtySettings' );
	} else if ( isString( modules ) ) {
		modules = [ modules ];
	}

	yield {
		type: RESET_SETTING_EDITS,
		modules,
	};
}

/**
 * Resets the edited settings for a module.
 *
 * @param {boolean|string|Array<string>} modules    The modules to save. By default, all dirty modules will be saved.
 * @param {boolean}                      [validate] Whether to validate a module's settings before saving.
 * @return {Array<Object>} The list of saved settings responses.
 */
export function* saveSettings( modules = true, validate = false ) {
	if ( modules === true ) {
		modules = yield controls.select( STORE_NAME, 'getDirtySettings' );
	} else if ( isString( modules ) ) {
		modules = [ modules ];
	}

	if ( ! modules.length ) {
		return [];
	}

	const requests = [];
	const savingModules = [];
	const errors = {};

	for ( const module of modules ) {
		if ( validate ) {
			const isValid = yield controls.dispatch( STORE_NAME, 'validateSettings', module );

			if ( isValid !== true ) {
				const error = new WPError( 'local_validation_failed' );
				isValid.errorText.forEach( ( errorText ) => error.add( 'local_validation_failed', errorText ) );
				errors[ module ] = error;
				continue;
			}
		}

		const settings = yield controls.select(
			STORE_NAME,
			'getSettingEdits',
			module
		);

		savingModules.push( module );
		requests.push( {
			method: 'PATCH',
			path: `/ithemes-security/v1/settings/${ module }`,
			body: settings,
		} );
	}

	let responses;

	try {
		yield { type: START_SAVING_SETTINGS, modules };
		responses = yield apiFetchBatch( requests );
	} catch ( error ) {
		yield { type: FAILED_SAVING_SETTINGS, modules };
		yield createNotice( 'error', error.message );

		return error;
	}

	const success = [];

	for ( let i = 0; i < requests.length; i++ ) {
		const module = savingModules[ i ];
		const response = responses[ i ];

		if ( response.status >= 400 ) {
			errors[ module ] = response.body;
		} else {
			success.push( module );
			yield receiveSettings( module, response.body );
		}
	}

	if ( ! isEmpty( errors ) ) {
		yield { type: FAILED_SAVING_SETTINGS, errors };
	}

	if ( success.length ) {
		yield createNotice( 'success', __( 'Saved settings.', 'better-wp-security' ), {
			type: 'snackbar',
		} );
		yield { type: FINISH_SAVING_SETTINGS, modules: success };
	}

	return responses;
}

export function* updateSettings( module, settings ) {
	yield { type: START_SAVING_SETTINGS, modules: [ module ] };

	let response;

	try {
		response = yield apiFetch( {
			path: `/ithemes-security/v1/settings/${ module }`,
			method: 'PATCH',
			data: settings,
		} );
		yield receiveSettings( module, response );
	} catch ( error ) {
		yield { type: FAILED_SAVING_SETTINGS, errors: { [ module ]: error } };

		return error;
	}

	yield { type: FINISH_SAVING_SETTINGS, modules: [ module ] };

	return response;
}

export const validateSettings = ( moduleId ) => async ( { select, resolveSelect } ) => {
	const schema = await resolveSelect.getSettingsConditionalSchema( moduleId );

	if ( ! schema ) {
		return true;
	}

	const settings = select.getEditedSettings( moduleId );
	const ajv = getAjv();

	const isValid = ajv.validate( schema, settings );

	if ( isValid ) {
		return true;
	}

	return {
		errors: ajv.errors,
		errorText: convertSchemaErrorToText( ajv.errors, moduleId, schema ),
	};
};

function convertSchemaErrorToText( errors, moduleId, schema ) {
	const text = [];

	for ( const { message, schemaPath, instancePath } of errors ) {
		let ptr = JsonPointer.create( schemaPath );
		let parent = ptr.parent( schema );

		while ( parent && ! parent.title ) {
			ptr = JsonPointer.create(
				ptr.path.slice( 0, ptr.path.length - 1 )
			);
			parent = ptr.parent( schema );
		}

		if ( parent?.title ) {
			text.push( `${ parent.title } ${ message }.` );
		} else {
			text.push( `${ moduleId }${ instancePath } ${ message }.` );
		}
	}

	return text;
}

function updateModule( module, status ) {
	return apiFetch( {
		method: 'PUT',
		path: `/ithemes-security/v1/modules/${ module }`,
		data: {
			status: {
				selected: status,
			},
		},
	} );
}

export function* fetchModules() {
	const modules = yield apiFetch( {
		path: '/ithemes-security/v1/modules?context=edit&_embed=1',
	} );
	yield receiveModules( modules );
}

export function receiveModules( modules ) {
	return {
		type: RECEIVE_MODULES,
		modules,
	};
}

export function receiveModule( module ) {
	return {
		type: RECEIVE_MODULE,
		module,
	};
}

export function receiveSettings( module, settings ) {
	return {
		type: RECEIVE_SETTINGS,
		module,
		settings,
	};
}

export const RECEIVE_MODULES = 'RECEIVE_MODULES';
export const RECEIVE_MODULE = 'RECEIVE_MODULE';
export const EDIT_MODULE = 'EDIT_MODULE';
export const RESET_MODULE_EDITS = 'RESET_MODULE_EDITS';

export const START_SAVING_MODULES = 'START_SAVING_MODULES';
export const FAILED_SAVING_MODULES = 'FAILED_SAVING_MODULES';
export const FINISH_SAVING_MODULES = 'FINISH_SAVING_MODULES';

export const RECEIVE_SETTINGS = 'RECEIVE_SETTINGS';
export const EDIT_SETTINGS = 'EDIT_SETTINGS';
export const EDIT_SETTING = 'EDIT_SETTING';
export const RESET_SETTING_EDIT = 'RESET_SETTING_EDIT';
export const RESET_SETTING_EDITS = 'RESET_SETTING_EDITS';

export const START_SAVING_SETTINGS = 'START_SAVING_SETTINGS';
export const FAILED_SAVING_SETTINGS = 'FAILED_SAVING_SETTINGS';
export const FINISH_SAVING_SETTINGS = 'FINISH_SAVING_SETTINGS';
