import { Api } from "../utils/api";
import { Paginator } from "../utils/paginator";
import { createParams } from "../utils/queryParams";

export class Contacts {
	private api: Api;
	/**
	 * Creates a new Contact model.
	 * @param api - The API client to use for making requests.
	 */
	constructor(api: Api) {
		this.api = api;
	}

	/**
	 * Fetches a list of contacts with optional filtering.
	 * @param parameters - Options for fetching contacts.
	 * @param parameters.email - Optional email address to query on.
	 * @param parameters.family_name - Optional last name or surname to query on.
	 * @param parameters.given_name - Optional first name or forename to query on.
	 * @param parameters.limit - Sets a total of items to return.
	 * @param parameters.offset - Sets a beginning range of items to return.
	 * @param parameters.optional_properties - List of Contact properties to include in the response.
	 * @param parameters.order - Attribute to order items by.
	 * @param parameters.order_direction - How to order the data (ascending or descending).
	 * @param parameters.since - Date to start searching from on LastUpdated (ex. 2017-01-01T22:17:59.039Z).
	 * @param parameters.until - Date to search to on LastUpdated (ex. 2017-01-01T22:17:59.039Z).
	 * @example
	 * const contacts = await contactsInstance.getContacts({
	 *   limit: 5,
	 *   offset: 10,
	 *   order: "date_created",
	 *   order_direction: "DESCENDING"
	 * });
	 * @returns A paginator containing the list of contacts.
	 */
	async getContacts(parameters?: {
		email?: string;
		family_name?: string;
		given_name?: string;
		limit?: number;
		offset?: number;
		optional_properties?: string[];
		order?:
			| "id"
			| "date_created"
			| "last_updated"
			| "name"
			| "firstName"
			| "email";
		order_direction?: "ASCENDING" | "DESCENDING";
		since?: string;
		until?: string;
	}): Promise<Paginator<IContact>> {
		const params = parameters
			? createParams(parameters, [
					"email",
					"limit",
					"offset",
					"family_name",
					"given_name",
					"optional_properties",
					"order",
					"order_direction",
					"since",
					"until",
			  ]).toString()
			: "";

		const response = await this.api.get(`v1/contacts?${params}`);

		const responseObj = response as { contacts: IContact[] | null };

		if (
			!responseObj ||
			!Object.prototype.hasOwnProperty.call(responseObj, "contacts")
		) {
			throw new Error("Invalid response format: missing contacts property");
		}

		if (responseObj.contacts === null) {
			throw new Error("No contacts found in response");
		}

		const contacts = responseObj.contacts.map((contact: IContact) => {
			if (contact === null) {
				throw new Error("Contact data is null");
			}
			return new Contact(this, contact);
		});

		return Paginator.wrap(this.api, { ...response, contacts }, "contacts");
	}
	/**
	 * Creates a new contact in Keap.
	 * Note: Contact must contain at least one item in email_addresses or phone_numbers,
	 * and country_code is required if region is specified on the addresses.
	 * @param contactData - The data to create a contact with.
	 * @returns The newly created contact.
	 * @example
	 * const contact = await contactsInstance.createContact({
	 *   given_name: 'John',
	 *   family_name: 'Doe',
	 *   email_addresses: [
	 *     {
	 *       email: "john.doe@example.com",
	 *       field: "EMAIL1"
	 *     }
	 *   ]
	 * });
	 */
	async createContact(contactData: IContact): Promise<Contact> {
		if (!contactData.email_addresses && !contactData.phone_numbers) {
			throw new Error(
				"Contact must contain at least one item in email_addresses or phone_numbers"
			);
		}
		const addressesWithRegionAndNoCountryCode = contactData.addresses?.filter(
			(address) => address.region && !address.country_code
		);

		if (
			addressesWithRegionAndNoCountryCode &&
			addressesWithRegionAndNoCountryCode.length > 0
		) {
			throw new Error(
				"Contact must contain a country code if a region is provided"
			);
		}
		const response = await this.api.post("v1/contacts", contactData);
		return new Contact(this, response as IContact);
	}
	/**
	 * Updates an existing contact.
	 * @param contactId - The ID of the contact to update.
	 * @param contactData {IContact} - The data to update the contact with.
	 * @returns The updated contact if the API call was successful.
	 * @throws Will throw an error if the API call fails.
	 * @example
	 * const contact = await contacts
	 *  .updateContact(123, {
	 *    given_name: "Jane",
	 *    family_name: "Doe"
	 * });
	 */
	async updateContact(
		contactId: number,
		contactData: IContact
	): Promise<Contact> {
		const r = await this.api.patch(`v1/contacts/${contactId}`, contactData);
		return new Contact(this, r as IContact);
	}

	/**
	 * Deletes a contact by ID.
	 * @param contactId - The ID of the contact to delete.
	 * @returns True if the contact was successfully deleted.
	 * @example
	 * const result = await contactsInstance.deleteContact(123);
	 * if (result) {
	 *   console.log("Contact deleted successfully");
	 * }
	 */
	async deleteContact(contactId: number): Promise<boolean> {
		await this.api.delete(`v1/contacts/${contactId}`);
		return true;
	}
	/**
	 * Fetches a single contact by ID.
	 * @param contactId - The ID of the contact to fetch.
	 * @returns The contact.
	 * @example
	 * const contact = await contactsInstance.getContact(123);
	 * console.log(contact.given_name); // "Jane"
	 * console.log(contact.family_name); // "Doe"
	 */
	async getContact(contactId: number): Promise<Contact> {
		const response = await this.api.get(`v1/contacts/${contactId}`);
		return new Contact(this, response as IContact);
	}
	/**
	 * Fetches the contact model schema.
	 * @returns The contact model.
	 * @example
	 * const contactModel = await contactsInstance.getContactModel();
	 * console.log(contactModel);
	 */
	async getContactModel(): Promise<ContactModel> {
		const response = await this.api.get("v1/contacts/model");
		return response as ContactModel;
	}

	/**
	 * Creates a new custom contact field.
	 * @param customFieldData - The data to create a custom field with.
	 * @returns The newly created custom field.
	 * @example
	 * const customField = await contactsInstance.createCustomField({
	 *   field_type: "Text",
	 *   label: "Custom Field Name"
	 * });
	 */
	async createCustomField(customFieldData: CustomField): Promise<CustomField> {
		const response = await this.api.post(
			"v1/contacts/customFields",
			customFieldData
		);
		return response as CustomField;
	}

	/**
	 * Creates a new contact if the contact does not exist, or updates the contact if it does.
	 * @param contactData - The data to create or update a contact with.
	 * @returns The newly created or updated contact.
	 * @example
	 * const contact = await contactsInstance.createOrUpdate({
	 *   given_name: "Jane",
	 *   family_name: "Doe"
	 * });
	 * console.log(contact); // {given_name: "Jane", family_name: "Doe", id: 123}
	 */
	async createOrUpdate(contactData: IContact): Promise<Contact> {
		const response = await this.api.put("v1/contacts", contactData);
		return new Contact(this, response as IContact);
	}

	/**
	 * Fetches the credit cards associated with a contact.
	 * @param contactId - The ID of the contact to fetch credit cards for.
	 * @returns The credit cards associated with the contact if the API call was successful.
	 */
	async getCreditCards(contactId: number): Promise<object> {
		const response = await this.api.get(`v1/contacts/${contactId}/creditCards`);
		return response as object;
	}

	/**
	 * Creates a new credit card for a contact.
	 * @param contactId - The ID of the contact to add the credit card to.
	 * @param creditCardData - The data to create the credit card with.
	 * @returns The newly created credit card.
	 */
	async createCreditCard(
		contactId: number,
		creditCardData: object
	): Promise<CreditCard> {
		const response = await this.api.post(
			`v1/contacts/${contactId}/creditCards`,
			creditCardData
		);
		return response as CreditCard;
	}

	/**
	 * Fetches the email addresses associated with a contact.
	 * @param contactId - The ID of the contact to fetch email addresses for.
	 * @returns The email addresses associated with the contact if the API call was successful.
	 */
	async listEmails(
		contactId: number,
		options?: { email?: string; limit?: number; offset?: number }
	): Promise<Paginator<EmailRecord>> {
		const params = options
			? createParams(options, ["email", "limit", "offset"])
			: "";
		const r = await this.api.get(
			`v1/contacts/${contactId}/emails?${params?.toString()}`
		);
		return Paginator.wrap(this.api, r, "emails");
	}

	/**
	 * Creates a new email address for a contact.
	 * @param contactId - The ID of the contact to add the email address to.
	 * @param emailData {EmailRecord} - The data to create the email address with.
	 * @returns The newly created email address if the API call was successful.
	 */
	createEmail(contactId: number, emailData: EmailRecord): Promise<EmailRecord> {
		return this.api.post(
			`v1/contacts/${contactId}/emails`,
			emailData
		) as Promise<EmailRecord>;
	}

	/**
	 * Fetches the tags applied to a contact.
	 * @param contactId - The ID of the contact to fetch tags for.
	 * @param options - The options to use when fetching tags.
	 * @returns The tags applied to the contact if the API call was successful.
	 */
	async listAppliedTags(
		contactId: number,
		options?: { limit?: number; offset?: number }
	): Promise<Paginator<Tag>> {
		const params = options ? createParams(options, ["limit", "offset"]) : "";
		const r = await this.api.get(
			`v1/contacts/${contactId}/tags?${params?.toString()}`
		);
		return Paginator.wrap(this.api, r, "tags");
	}

	/**
	 * Applies a tag to a contact.
	 * @param contactId {number} - The ID of the contact to apply the tag to.
	 * @param tagIds {number[]} - The ID of the tag to apply.
	 * @returns The result of the API call if it was successful.
	 */
	applyTags(contactId: number, tagIds: number[]): Promise<object> {
		return this.api.post(`v1/contacts/${contactId}/tags`, {
			tagIds,
		}) as Promise<object>;
	}

	/**
	 * Removes a tag from a contact.
	 * @param contactId - The ID of the contact to remove the tag from.
	 * @param tagId - The ID of the tag to remove.
	 * @returns The result of the API call if it was successful.
	 */
	removeTag(contactId: number, tagId: number): Promise<object> {
		return this.api.delete(
			`v1/contacts/${contactId}/tags/${tagId}`
		) as Promise<object>;
	}

	/**
	 * Removes multiple tags from a contact.
	 * @param contactId - The ID of the contact to remove the tags from.
	 * @param tagIds - The IDs of the tags to remove.
	 * @param data - Data to be sent with the request.
	 * @returns The result of the API call if it was successful.
	 */
	removeTags(
		contactId: number,
		tagIds: number[],
		data: object
	): Promise<object> {
		return this.api.delete(
			`v1/contacts/${contactId}/tags?tagIds=${tagIds.join(",")}`,
			data
		) as Promise<object>;
	}

	/**
	 * Adds UTM tracking data to a contact.
	 * @param contactId - The ID of the contact to add the UTM data to.
	 * @param utmData - The UTM data to add.
	 * @returns The result of the API call if it was successful.
	 */
	addUTM(contactId: number, utmData: UTM): Promise<UTM> {
		return this.api.post(
			`v1/contacts/${contactId}/utms`,
			utmData
		) as Promise<UTM>;
	}
}

class Contact {
	ScoreValue: string | null = null;
	addresses: Array<{
		country_code: string;
		field: string;
		line1: string;
		line2: string;
		locality: string;
		postal_code: string;
		region: string;
		zip_code: string;
		zip_four: string;
	}> | null = null;
	anniversary: Date | null = null;
	birthday: Date | null = null;
	company: {
		company_name: string;
		id: number;
	} | null = null;
	company_name: string | null = null;
	contact_type: string | null = null;
	custom_fields: Array<{
		content: object;
		id: number;
	}> | null = null;
	date_created: Date | null = null;
	email_addresses: Array<{
		email: string;
		field: string;
	}> | null = null;
	email_opted_in: boolean | null = null;
	email_status: string | null = null;
	family_name: string | null = null;
	fax_numbers: Array<{
		field: string;
		number: string;
		type: string;
	}> | null = null;
	given_name: string | null = null;
	id: number;
	job_title: string | null = null;
	last_updated: Date | null = null;
	lead_source_id: number | null = null;
	middle_name: string | null = null;
	opt_in_reason: string | null = null;
	origin: {
		date: Date;
		ip_address: string;
	} | null = null;
	owner_id: number | null = null;
	phone_numbers: Array<{
		extension: string;
		field: string;
		number: string;
		type: string;
	}> | null = null;
	preferred_locale: string | null = null;
	preferred_name: string | null = null;
	prefix: string | null = null;
	relationships: Array<{
		id: number;
		linked_contact_id: number;
		relationship_type_id: number;
	}> | null = null;
	social_accounts: Array<{
		name: string;
		type: string;
	}> | null = null;
	source_type: string | null = null;
	spouse_name: string | null = null;
	suffix: string | null = null;
	tag_ids: Array<number> | null = null;
	time_zone: string | null = null;
	website: string | null = null;
	private contacts: Contacts;

	/**
	 * Creates a new Contact instance from the given data and API client.
	 * @param api - The API client to use for making requests.
	 * @param data - The data to use to populate the Contact instance.
	 */
	constructor(contacts: Contacts, data: IContact) {
		this.contacts = contacts;
		Object.assign(this, data);
		if (!data.id) throw new Error("Contact ID is required");
		this.id = data.id;
	}

	/**
	 * Updates the contact with the given data.
	 * @param data - The data to update the contact with.
	 * @returns The updated contact if the API call was successful.
	 */
	async update(data: IContact): Promise<Contact> {
		const r = await this.contacts.updateContact(this.id, data);
		return new Contact(this.contacts, r);
	}

	/**
	 * Deletes the contact.
	 * @returns The result of the API call if it was successful.
	 * @throws Will throw an error if the API call fails.
	 */
	async delete(): Promise<boolean> {
		return this.contacts.deleteContact(this.id);
	}

	/**
	 * Refreshes the contact data from the API.
	 * @returns The updated contact if the API call was successful.
	 */
	async refresh(): Promise<Contact> {
		const contact = await this.contacts.getContact(this.id);
		Object.assign(this, contact);
		return contact;
	}

	/**
	 * Fetches the credit cards associated with this contact.
	 * @returns The credit cards associated with this contact if the API call was successful.
	 */
	getCreditCards(): Promise<object> {
		return this.contacts.getCreditCards(this.id);
	}

	/**
	 * Creates a new credit card for this contact.
	 * @param data - The data to create the credit card with.
	 * @returns The newly created credit card if the API call was successful.
	 */
	addCreditCard(data: object): Promise<CreditCard> {
		return this.contacts.createCreditCard(this.id, data);
	}

	/**
	 * Fetches the email addresses associated with this contact.
	 * @param options - The options to use when fetching email addresses.
	 * @returns The email addresses associated with this contact if the API call was successful.
	 */
	getEmails(options?: {
		email?: string;
		limit?: number;
		offset?: number;
	}): Promise<Paginator<EmailRecord>> {
		return this.contacts.listEmails(this.id, options);
	}

	/**
	 * Adds a new email address to this contact.
	 * @param data - The data to create the email address with.
	 * @returns The newly created email address if the API call was successful.
	 */
	addEmail(data: EmailRecord): Promise<EmailRecord> {
		return this.contacts.createEmail(this.id, data);
	}

	/**
	 * Fetches the tags applied to this contact.
	 * @param options - The options to use when fetching tags.
	 * @returns The tags applied to this contact if the API call was successful.
	 */
	getAppliedTags(options?: {
		limit?: number;
		offset?: number;
	}): Promise<Paginator<Tag>> {
		return this.contacts.listAppliedTags(this.id, options);
	}

	/**
	 * Applies the given tags to this contact.
	 * @param tagIds - The IDs of the tags to apply.
	 * @returns The result of the API call if it was successful.
	 */
	applyTags(tagIds: number[]): Promise<object> {
		return this.contacts.applyTags(this.id, tagIds);
	}

	/**
	 * Removes the given tag from this contact.
	 * @param tagId - The ID of the tag to remove.
	 * @returns The result of the API call if it was successful.
	 */
	removeTag(tagId: number): Promise<object> {
		return this.contacts.removeTag(this.id, tagId);
	}

	/**
	 * Removes multiple tags from this contact.
	 * @param tagIds - The IDs of the tags to remove.
	 * @param data - Data to be sent with the request.
	 * @returns The result of the API call if it was successful.
	 */
	removeTags(tagIds: number[], data: object): Promise<object> {
		return this.contacts.removeTags(this.id, tagIds, data);
	}

	/**
	 * Adds UTM tracking data to this contact.
	 * @param utmData - The UTM data to add.
	 * @returns The result of the API call if it was successful.
	 */
	addUTM(utmData: UTM): Promise<UTM> {
		return this.contacts.addUTM(this.id, utmData);
	}
}
