import {
	IExecuteFunctions,
} from 'n8n-core';

import {
	IDataObject,
	ILoadOptionsFunctions,
	NodeApiError,
	NodeOperationError,
} from 'n8n-workflow';

import {
	OptionsWithUri,
} from 'request';

import {
	flow,
	omit,
} from 'lodash';

import {
	AllFieldsUi,
	FieldWithPrimaryField,
	LinksFieldContainer,
	PersonResponse,
	PetitionResponse,
	Resource,
	Response,
} from './types';

export async function actionNetworkApiRequest(
	this: IExecuteFunctions | ILoadOptionsFunctions,
	method: string,
	endpoint: string,
	body: IDataObject = {},
	qs: IDataObject = {},
) {
	const credentials = await this.getCredentials('actionNetworkApi') as { apiKey: string } | undefined;

	if (credentials === undefined) {
		throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
	}

	const options: OptionsWithUri = {
		headers: {
			'OSDI-API-Token': credentials.apiKey,
		},
		method,
		body,
		qs,
		uri: `https://actionnetwork.org/api/v2${endpoint}`,
		json: true,
	};

	if (!Object.keys(body).length) {
		delete options.body;
	}

	if (!Object.keys(qs).length) {
		delete options.qs;
	}

	try {
		return await this.helpers.request!(options);
	} catch (error) {
		throw new NodeApiError(this.getNode(), error);
	}
}

export async function handleListing(
	this: IExecuteFunctions | ILoadOptionsFunctions,
	method: string,
	endpoint: string,
	body: IDataObject = {},
	qs: IDataObject = {},
	options?: { returnAll: true },
) {
	const returnData: IDataObject[] = [];
	let responseData;

	qs.perPage = 25; // max
	qs.page = 1;

	const returnAll = options?.returnAll ?? this.getNodeParameter('returnAll', 0, false) as boolean;
	const limit = this.getNodeParameter('limit', 0, 0) as number;

	const itemsKey = toItemsKey(endpoint);

	do {
		responseData = await actionNetworkApiRequest.call(this, method, endpoint, body, qs);
		const items = responseData._embedded[itemsKey];
		returnData.push(...items);

		if (!returnAll && returnData.length >= limit) {
			return returnData.slice(0, limit);
		}

		qs.page = responseData.page as number;
	} while (responseData.total_pages && qs.page < responseData.total_pages);

	return returnData;
}


// ----------------------------------------
//              helpers
// ----------------------------------------

/**
 * Convert an endpoint to the key needed to access data in the response.
 */
const toItemsKey = (endpoint: string) => {

	// handle two-resource endpoint
	if (
		endpoint.includes('/signatures') ||
		endpoint.includes('/attendances') ||
		endpoint.includes('/taggings')
	) {
		endpoint = endpoint.split('/').pop()!;
	}

	return `osdi:${endpoint.replace(/\//g, '')}`;
};

export const extractId = (response: LinksFieldContainer) => {
	return response._links.self.href.split('/').pop() ?? 'No ID';
};

export const makeOsdiLink = (personId: string) => {
	return {
		_links: {
			'osdi:person': {
				href: `https://actionnetwork.org/api/v2/people/${personId}`,
			},
		},
	};
};

export const isPrimary = (field: FieldWithPrimaryField) => field.primary;


// ----------------------------------------
//           field adjusters
// ----------------------------------------

function adjustLanguagesSpoken(allFields: AllFieldsUi) {
	if (!allFields.languages_spoken) return allFields;

	return {
		...omit(allFields, ['languages_spoken']),
		languages_spoken: [allFields.languages_spoken],
	};
}

function adjustPhoneNumbers(allFields: AllFieldsUi) {
	if (!allFields.phone_numbers) return allFields;

	return {
		...omit(allFields, ['phone_numbers']),
		phone_numbers: [
			allFields.phone_numbers.phone_numbers_fields,
		],
	};
}

function adjustPostalAddresses(allFields: AllFieldsUi) {
	if (!allFields.postal_addresses) return allFields;

	if (allFields.postal_addresses.postal_addresses_fields.length) {
		const adjusted = allFields.postal_addresses.postal_addresses_fields.map((field) => {
			const copy: IDataObject = {
				...omit(field, ['address_lines', 'location']),
			};

			if (field.address_lines) {
				copy.address_lines = [field.address_lines];
			}

			if (field.location) {
				copy.location = field.location.location_fields;
			}

			return copy;
		});

		return {
			...omit(allFields, ['postal_addresses']),
			postal_addresses: adjusted,
		};
	}
}

function adjustLocation(allFields: AllFieldsUi) {
	if (!allFields.location) return allFields;

	const locationFields = allFields.location.postal_addresses_fields;

	const adjusted: IDataObject = {
		...omit(locationFields, ['address_lines', 'location']),
	};

	if (locationFields.address_lines) {
		adjusted.address_lines = [locationFields.address_lines];
	}

	if (locationFields.location) {
		adjusted.location = locationFields.location.location_fields;
	}

	return {
		...omit(allFields, ['location']),
		location: adjusted,
	};
}

function adjustTargets(allFields: AllFieldsUi) {
	if (!allFields.target) return allFields;

	const adjusted = allFields.target.split(',').map(value => ({ name: value }));

	return {
		...omit(allFields, ['target']),
		target: adjusted,
	};
}


// ----------------------------------------
//           payload adjusters
// ----------------------------------------

export const adjustPersonPayload = flow(
	adjustLanguagesSpoken,
	adjustPhoneNumbers,
	adjustPostalAddresses,
);

export const adjustPetitionPayload = adjustTargets;

export const adjustEventPayload = adjustLocation;


// ----------------------------------------
//           resource loaders
// ----------------------------------------

async function loadResource(this: ILoadOptionsFunctions, resource: string) {
	return await handleListing.call(this, 'GET', `/${resource}`, {}, {}, { returnAll: true });
}

export const resourceLoaders = {

	async getTags(this: ILoadOptionsFunctions) {
		const tags = await loadResource.call(this, 'tags') as Array<{ name: string } & LinksFieldContainer>;

		return tags.map((tag) => ({ name: tag.name, value: extractId(tag) }));
	},

	async getTaggings(this: ILoadOptionsFunctions) {
		const tagId = this.getNodeParameter('tagId', 0);
		const endpoint = `/tags/${tagId}/taggings`;

		// two-resource endpoint, so direct call
		const taggings = await handleListing.call(
			this, 'GET', endpoint, {}, {}, { returnAll: true },
		) as LinksFieldContainer[];

		return taggings.map((tagging) => {
			const taggingId = extractId(tagging);

			return {
				name: taggingId,
				value: taggingId,
			};
		});
	},
};


// ----------------------------------------
//          response simplifiers
// ----------------------------------------

export const simplifyResponse = (response: Response, resource: Resource) => {
	if (resource === 'person') {
		return simplifyPersonResponse(response as PersonResponse);
	} else if (resource === 'petition') {
		return simplifyPetitionResponse(response as PetitionResponse);
	}

	const fieldsToSimplify = [
		'identifiers',
		'_links',
		'action_network:sponsor',
		'reminders',
	];

	return {
		id: extractId(response),
		...omit(response, fieldsToSimplify),
	};
};


const simplifyPetitionResponse = (response: PetitionResponse) => {
	const fieldsToSimplify = [
		'identifiers',
		'_links',
		'action_network:hidden',
		'_embedded',
	];

	return {
		id: extractId(response),
		...omit(response, fieldsToSimplify),
		creator: simplifyPersonResponse(response._embedded['osdi:creator']),
	};
};

const simplifyPersonResponse = (response: PersonResponse) => {
	const emailAddress = response.email_addresses.filter(isPrimary);
	const phoneNumber = response.phone_numbers.filter(isPrimary);
	const postalAddress = response.postal_addresses.filter(isPrimary);

	const fieldsToSimplify = [
		'identifiers',
		'email_addresses',
		'phone_numbers',
		'postal_addresses',
		'languages_spoken',
		'_links',
	];

	return {
		id: extractId(response),
		...omit(response, fieldsToSimplify),
		...{ email_address: emailAddress[0].address || '' },
		...{ phone_number: phoneNumber[0].number || '' },
		...{
			postal_address: {
				...postalAddress && omit(postalAddress[0], 'address_lines'),
				address_lines: postalAddress[0].address_lines ?? '',
			},
		},
		language_spoken: response.languages_spoken[0],
	};
};