<?php
/**
 * Pipedrive_Client
 *
 * Provides a wrapper for interacting with the Pipedrive REST API.
 * Supports searching, creating, and updating organizations, people, leads, and notes.
 *
 * @package StrategySuite
 */

namespace StrategySuite;

/**
 * Class Pipedrive_Client
 *
 * Handles requests to the Pipedrive API for creating and managing leads, persons,
 * organizations, and notes. Also includes helper methods for working with
 * Pipedrive custom fields and mapping UTM sources to channels.
 */
class Pipedrive_Client {

	/**
	 * The base URL for the Pipedrive API.
	 *
	 * This variable stores the base URL used for making API requests to Pipedrive. The value is typically
	 * the Pipedrive API URL, which is 'https://api.pipedrive.com/v1'.
	 *
	 * @var string
	 */
	private $pipedrive_base;

	/**
	 * The API token used for authenticating requests to Pipedrive.
	 *
	 * This variable holds the API token that is used to authenticate requests made to the Pipedrive API.
	 * The value is either taken from the constant `AMELIA_PIPEDRIVE_API_TOKEN` or from the environment
	 * variable `AMELIA_PIPEDRIVE_API_TOKEN` if the constant is not defined.
	 *
	 * @var string
	 */
	private $token;

	/**
	 * The user ID of the specified owner in Pipedrive
	 *
	 * @var int
	 */
	private $owner_id;

	/**
	 * Creates an instance of the Pipedrive_Client.
	 */
	private function __construct() {
		$this->pipedrive_base = 'https://api.pipedrive.com/v1';
		$this->token = defined( 'AMELIA_PIPEDRIVE_API_TOKEN' )
			? AMELIA_PIPEDRIVE_API_TOKEN
			: getenv( 'AMELIA_PIPEDRIVE_API_TOKEN' );

		$this->owner_id = null;
	}

	/**
	 * Public factory method for creating a client. Requires ABSPATH to be defined.
	 *
	 * @return Pipedrive_Client|null             Returns an instance of the class or null
	 */
	public static function create_client() {
		if ( ! defined( 'ABSPATH' ) ) {
			return null;
		}

		return new Pipedrive_Client();
	}

	/**
	 * Retrieves the data of a user in Pipedrive from the user's name.
	 * This function will also store a reference for a user as a transient. If
	 * the transient for the specified user is present, it will be retrieved
	 * instead of sending a new request.
	 *
	 * @param string $user_name             The name of the user being searched for
	 *
	 * @return array|Object                 Returns the user or null on failure
	 */
	private function get_user( $user_name ) {

		$existing_user = json_decode( get_transient( 'user{' . $user_name . '}' ) );
		if ( $existing_user ) {
			return $existing_user;
		}

		$user = $this->search_user( $user_name );
		if ( $user ) {
			set_transient( 'user{' . $user_name . '}', json_encode( $user ), 86400 );
			return (array) $user;
		}

		return null;
	}

	/**
	 * Formats and executes a cURL request based on given parameters.
	 *
	 * @param string $action                The action being performed (i.e; GET, POST, etc.)
	 * @param string $url                   The URL that the request is being sent to
	 * @param array  $data                   An array of data to be sent as part of a POST request
	 *
	 * @return array                        Returns a JSON-decoded array of data or null on failure
	 */
	private function send_request( $action, $url, $data ) {
		$session = curl_init();

		curl_setopt( $session, CURLOPT_URL, $url );
		curl_setopt( $session, CURLOPT_RETURNTRANSFER, true );
		curl_setopt(
			$session,
			CURLOPT_HTTPHEADER,
			array(
				'Content-Type: application/json',
			)
		);

		if ( ! empty( $data ) ) {
			curl_setopt( $session, CURLOPT_POSTFIELDS, $data );
		}

		switch ( $action ) {
			case 'GET':
				break;
			case 'POST':
				curl_setopt( $session, CURLOPT_POST, true );
				break;
			case 'PUT':
				curl_setopt( $session, CURLOPT_CUSTOMREQUEST, 'PUT' );
				break;
			default:
				return null;
		}

		$response = curl_exec( $session );
		curl_close( $session );

		$json = json_decode( $response, true );

		return $json;
	}

	/**
	 * Sends a request to the Pipedrive API.
	 *
	 * This helper function sends an HTTP request to the Pipedrive API using the specified HTTP method.
	 * It automatically includes the API token in the query parameters and handles the response.
	 *
	 * @param string     $method The HTTP method to use (e.g., 'GET', 'POST', 'PUT', etc.).
	 * @param string     $endpoint The API endpoint to send the request to.
	 * @param array|null $payload The request body for POST and PUT requests, or null for GET requests.
	 *
	 * @return array The response body from Pipedrive.
	 *
	 * @throws \Exception If an error occurs during the request or if the response code is not successful.
	 */
	private function send_pipedrive_request( $method, $endpoint, $payload = null ) {
		$url = trailingslashit( $this->pipedrive_base ) . ltrim( $endpoint, '/' );

		if ( false === strpos( $url, 'api_token=' ) && $this->token ) {
			$url = add_query_arg( [ 'api_token' => $this->token ], $url );
		}

		$args = [
			'method' => $method,
			'timeout' => 20,
			'headers' => [
				'Accept' => 'application/json',
				'Content-Type' => 'application/json',
			],
		];

		if ( null !== $payload ) {
			$args['body'] = wp_json_encode( $payload );
		}

		$res = wp_remote_request( $url, $args );

		if ( is_wp_error( $res ) ) {
			error_log( '[GravityForms→Pipedrive] HTTP error: ' . esc_html( $res->get_error_message() ) );
			throw new \Exception( 'HTTP error: ' . esc_html( $res->get_error_message() ) );
		}

		$code = wp_remote_retrieve_response_code( $res );
		$body = json_decode( wp_remote_retrieve_body( $res ), true );

		if ( $code < 200 || $code >= 300 ) {
			$msg = isset( $body['error'] ) ? $body['error'] : 'Unexpected response from Pipedrive';
			error_log( esc_html( "[GravityForms→Pipedrive] Pipedrive API error, status {$code}: {$msg}" ) );
			throw new \Exception( esc_html( "Pipedrive API error, status {$code}: {$msg}" ) );
		}

		return $body;
	}

	/**
	 * Formats a URL to be given to send_request().
	 *
	 * @param string $endpoint              The API endpoint that will be receiving the request
	 *
	 * @return string                       The newly-formatted URL string
	 */
	private function format_request_url( $endpoint ) {
		return trailingslashit( $this->pipedrive_base ) . $endpoint . '?api_token=' . $this->token;
	}

	/**
	 * Searches Pipedrive for an existing user via the Pipedrive API.
	 *
	 * @param string $search_term           The component of the user being searched for
	 * @param bool   $search_by_email         Whether or not to be comparing $search_term to the user email
	 *
	 * @return array                        Returns the user if found, otherwise return null
	 */
	public function search_user( $search_term, $search_by_email = false ) {

		$url = $this->format_request_url( 'users/find' );

		$data = array(
			'term'              => $search_term,
			'search_by_email'   => $search_by_email,
		);

		$url .= '&' . http_build_query( $data );

		$response = $this->send_request( 'GET', $url, [] );

		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			foreach ( $response['data'] as $user ) {
				if ( ( $search_by_email && $search_term == $user['email'] ) || ( ! $search_by_email && $search_term == $user['name'] ) ) {
					return $user;
				}
			}
		}

		return null;
	}

	/**
	 * Creates a new lead in Pipedrive using the REST API.
	 *
	 * This method creates a lead record in Pipedrive and optionally associates it
	 * with an existing person or organization. It also supports including additional
	 * standard or custom field data by passing key-value pairs through the
	 * `$extra_fields` parameter.
	 *
	 * Automatically sets the `owner_id` (if configured), attaches the proper
	 * source channel based on the user's UTM cookie, and merges all
	 * additional mapped form fields into the request body.
	 *
	 * @param string   $title         The title of the new lead.
	 * @param int|null $person_id   Optional. The ID of the person connected to the lead.
	 * @param int|null $org_id      Optional. The ID of the organization connected to the lead.
	 * @param array    $extra_fields   Optional. Additional Pipedrive lead fields to include in the payload.
	 *                                 Use field keys as array keys (e.g., ['value' => 10000, 'custom_field_xxx' => 'Example']).
	 *
	 * @return array|false          Returns the created lead data array on success, or false on failure.
	 */
	public function add_lead( $title, $person_id = null, $org_id = null, $extra_fields = [] ) {

		if ( null == $person_id && null == $org_id ) {
			return false;
		}

		$utm_source = $this->get_cookie_value( 'utm_source' );
		$lead_channel = $this->map_source_channel( $utm_source );
		$lead_channel_id = $this->get_custom_field_option( 'Source channel', $lead_channel );

		$url = $this->format_request_url( 'leads' );

		$data = array_merge(
			[ 'title' => $title ],
			is_array( $extra_fields ) ? $extra_fields : []
		);

		if ( null != $this->owner_id ) {
			$data['owner_id'] = $this->owner_id;
		}

		if ( null != $person_id ) {
			$data['person_id'] = $person_id;
		}

		if ( null != $org_id ) {
			$data['organization_id'] = $org_id;
		}

		if ( $lead_channel_id ) {
			$data['channel'] = $lead_channel_id;
		}

		$response = $this->send_request( 'POST', $url, json_encode( $data ) );
		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			return $response['data'];
		}

		return false;
	}

	/**
	 * Fetch all organizations.
	 *
	 * @return array|null List of organizations, or null on failure.
	 */
	public function get_orgs() {

		$url = $this->format_request_url( 'organizations' );

		$response = $this->send_request( 'GET', $url, [] );
		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			return $response['data'];
		}

		return null;
	}

	/**
	 * Searches for an existing organization via the Pipedrive API.
	 *
	 * @param string $search_term           The component of the user being searched for
	 * @param string $fields                A comma-separated list of fields to search for
	 * @param bool   $exact_match             Whether to do partial matching or exact
	 *
	 * @return array                        Returns the response array if valid, or return null on failure
	 */
	public function search_org( $search_term, $fields = null, $exact_match = false ) {

		$url = $this->format_request_url( 'organizations/search' );

		$data = array(
			'term'          => $search_term,
			'exact_match'   => $exact_match,
		);

		if ( null != $fields ) {
			$data['fields'] = $fields;
		}

		$url .= '&' . http_build_query( $data );

		$response = $this->send_request( 'GET', $url, [] );
		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			foreach ( $response['data']['items'] as $org ) {
				if ( $search_term == $org['item']['name'] ) {
					return $org['item'];
				}
			}
		}

		return null;
	}

	/**
	 * Get an organization by ID.
	 *
	 * @param int $org_id The organization ID.
	 * @return array|null The organization record, or null.
	 */
	public function search_org_by_id( $org_id ) {

		$url = $this->format_request_url( 'organizations/' . $org_id );

		$response = $this->send_request( 'GET', $url, [] );
		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			return $response['data'];
		}

		return null;
	}

	/**
	 * Creates a new organization in Pipedrive using the REST API.
	 *
	 * This method creates an organization record in Pipedrive and accepts both
	 * standard and custom fields through the `$extra_fields` parameter. It will
	 * automatically assign the record to the configured Pipedrive user (`owner_id`)
	 * if one is set.
	 *
	 * Example usage:
	 * ```php
	 * $client->add_org('Acme Construction', [
	 *     'address' => '123 Main St',
	 *     'custom_field_abc123' => 'Preferred Vendor',
	 * ]);
	 * ```
	 *
	 * @param string $name          The name of the organization being added.
	 * @param array  $extra_fields   Optional. Additional organization fields to include in the payload.
	 *                               Use Pipedrive API field keys as array keys.
	 *
	 * @return array|false          Returns the created organization data array on success, or false on failure.
	 */
	public function add_org( $name, $extra_fields = [] ) {

		$url = $this->format_request_url( 'organizations' );

		$data = array_merge(
			[ 'name' => $name ],
			is_array( $extra_fields ) ? $extra_fields : []
		);

		if ( null != $this->owner_id ) {
			$data['owner_id'] = $this->owner_id;
		}

		$response = $this->send_request( 'POST', $url, json_encode( $data ) );

		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			return $response['data'];
		}

		return false;
	}

	/**
	 * Update an organization.
	 *
	 * @param int   $org_id The organization ID.
	 * @param array $params Update parameters.
	 * @return array|false Updated organization data, or false.
	 */
	public function update_org( $org_id, $params ) {

		$url = $this->format_request_url( 'organizations/' . $org_id );

		if ( ! is_array( $params ) ) {
			return false;
		}

		$response = $this->send_request( 'PUT', $url, json_encode( $params ) );

		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			return $response['data'];
		}

		return false;
	}

	/**
	 * Fetch all persons.
	 *
	 * @return array|null List of persons, or null on failure.
	 */
	public function get_persons() {

		$url = $this->format_request_url( '/persons' );

		$response = $this->send_request( 'GET', $url, [] );
		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			return $response['data'];
		}

		return null;
	}

	/**
	 * Searches for an existing person via the Pipedrive API.
	 *
	 * @param string $search_term           The component of the user being searched for
	 * @param string $fields                A comma-separated list of fields to search for
	 * @param bool   $exact_match             Whether to do partial matching or exact
	 *
	 * @return array                        Returns the response array if valid, or return null on failure
	 */
	public function search_person( $search_term, $fields = null, $exact_match = false ) {

		$url = $this->format_request_url( 'persons/search' );

		$data = array(
			'term'          => $search_term,
			'exact_match'   => $exact_match,
		);

		if ( null != $fields ) {
			$data['fields'] = $fields;
		}

		$url .= '&' . http_build_query( $data );

		$response = $this->send_request( 'GET', $url, [] );
		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			foreach ( $response['data']['items'] as $person ) {
				switch ( $fields ) {
					case 'email':
						if ( $search_term == $person['item']['primary_email'] ) {
							return $person['item'];
						} else {
							foreach ( $person['item']['emails'] as $email ) {
								if ( $search_term == $email ) {
									return $person['item'];
								}
							}
						}
						break;
					case 'name':
						if ( $search_term == $person['item']['name'] ) {
							return $person['item'];
						}
						break;
					default:
						if ( $search_term == $person['item']['name'] ) {
							return $person['item'];
						} else if ( $search_term == $person['item']['primary_email'] ) {
							return $person['item'];
						} else {
							foreach ( $person['item']['emails'] as $email ) {
								if ( $search_term == $email ) {
									return $person['item'];
								}
							}
						}
						break;
				}
			}
		}

		return null;
	}

	/**
	 * Get a person by ID.
	 *
	 * @param int $person_id The person ID.
	 * @return array|null The person record, or null.
	 */
	public function search_person_by_id( $person_id ) {

		$url = $this->format_request_url( 'persons/' . $person_id );

		$response = $this->send_request( 'GET', $url, [] );
		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			return $response['data'];
		}

		return null;
	}

	/**
	 * Creates a new person in Pipedrive using the REST API.
	 *
	 * This method creates a person record in Pipedrive, optionally linking it to
	 * an existing organization and populating core contact fields such as name,
	 * email, and phone. It also supports additional mapped form data and custom
	 * Pipedrive fields via the `$extra_fields` parameter.
	 *
	 * Automatically assigns the person to the configured `owner_id` (if set)
	 * and merges all other provided fields into the creation payload.
	 *
	 * Example usage:
	 * ```php
	 * $client->add_person(
	 *     'John Doe',
	 *     42, // org_id
	 *     'john@example.com',
	 *     '555-1234',
	 *     [
	 *         'job_title' => 'Project Manager',
	 *         'custom_field_abc123' => 'VIP Contact',
	 *     ]
	 * );
	 * ```
	 *
	 * @param string      $name          The name of the new person.
	 * @param int|null    $org_id      Optional. The ID of the organization the person is associated with.
	 * @param string|null $email    Optional. The primary email address for the person.
	 * @param string|null $phone    Optional. The primary phone number for the person.
	 * @param array       $extra_fields   Optional. Additional Pipedrive person fields to include in the payload.
	 *                                    Use field keys as array keys (e.g., ['job_title' => 'Manager', 'custom_field_xxx' => 'Value']).
	 *
	 * @return array|false          Returns the created person data array on success, or false on failure.
	 */
	public function add_person( $name, $org_id = null, $email = null, $phone = null, $extra_fields = [] ) {

		$url = $this->format_request_url( 'persons' );

		$data = array_merge(
			[ 'name' => $name ],
			is_array( $extra_fields ) ? $extra_fields : []
		);

		if ( null != $this->owner_id ) {
			$data['owner_id'] = $this->owner_id;
		}

		if ( null != $org_id ) {
			$data['org_id'] = $org_id;
		}

		if ( null != $email ) {
			$data['email'] = array(
				'value' => $email,
				'primary' => true,
			);
		}

		if ( null != $phone ) {
			$data['phone'] = array(
				'value' => $phone,
				'primary' => true,
			);
		}

		$response = $this->send_request( 'POST', $url, json_encode( $data ) );

		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			return $response['data'];
		}

		return false;
	}

	/**
	 * Update a person.
	 *
	 * @param int   $person_id The person ID.
	 * @param array $params    Update parameters.
	 * @return array|false Updated person data, or false.
	 */
	public function update_person( $person_id, $params ) {

		$url = $this->format_request_url( 'persons/' . $person_id );

		if ( ! is_array( $params ) ) {
			return false;
		}

		$response = $this->send_request( 'PUT', $url, json_encode( $params ) );

		if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
			return $response['data'];
		}

		return false;
	}

	/**
	 * Creates a new note in Pipedrive via the Pipedrive API.
	 *
	 * @param string $content               The content of the note (accepts HTML)
	 * @param int    $lead_id                  The ID of a lead to attach the note to
	 * @param int    $person_id                The ID of a person to attach the note to
	 * @param int    $org_id                   The ID of an organization to attach the note to
	 *
	 * @return array|bool                   Return the newly created note or false on failure
	 */
	public function add_note( $content, $lead_id = null, $person_id = null, $org_id = null ) {

		if ( null != $lead_id || null != $person_id || null != $org_id ) {
			$url = $this->format_request_url( 'notes' );

			$data = array(
				'content'   => $content,
				'user_id'   => $this->owner_id,
			);

			if ( null != $lead_id ) {
				$data['lead_id'] = $lead_id;
			}

			if ( null != $person_id ) {
				$data['person_id'] = $person_id;
			}

			if ( null != $org_id ) {
				$data['org_id'] = $org_id;
			}

			$response = $this->send_request( 'POST', $url, json_encode( $data ) );

			if ( is_array( $response ) && isset( $response['success'] ) && $response['success'] ) {
				return $response['data'];
			}
		}

		return false;
	}

	/**
	 * Creates a new lead using the Pipedrive API. Ensures that there is a person
	 * and organization created that can be attached for lead creation.
	 *
	 * @param string       $company_name          The name of the organization attached to the new lead
	 * @param string       $person_name           The name of the person attached to the new lead
	 * @param string       $person_email          The email of the person attached to the new lead
	 * @param string       $person_phone          The phone number of the person attached to the new lead
	 * @param array|string $notes          Any notes that are to be attached to the new lead (must have 'label' and 'value' keys)
	 * @param array        $extra_fields          Any custom pipedrive mapping.
	 *
	 * @return array|bool                   Return lead array on successful lead creation, return false on failure
	 */
	public function construct_lead( $company_name, $person_name, $person_email, $person_phone = null, $notes = array(), $extra_fields = array() ) {
		$lead_created = false;

		if ( null != $company_name || ( null != $person_email && null != $person_name ) ) {

			$org = null;
			$person = null;
			$title = $person_name;

			if ( $company_name ) {
				$org = $this->search_org( $company_name, 'name', true );
				if ( null == $org ) {
					$org = $this->add_org( trim( $company_name ) );
					$title = trim( $company_name );
				}
			}

			if ( $person_email && $person_name ) {
				$person = $this->search_person( $person_email, 'email', true );
				if ( null == $person ) {
					if ( $org ) {
						$person = $this->add_person( trim( $person_name ), $org['id'], $person_email, $person_phone );
					} else {
						$person = $this->add_person( trim( $person_name ), null, $person_email, $person_phone );
					}
					if ( null == $title ) {
						$title = trim( $person_name );
					}
				}
			}

			$lead_title = trim( $title ) . ' Lead';

			if ( ! isset( $person['id'] ) ) {
				$lead_created = $this->add_lead( $lead_title, null, $org['id'] );
			} else if ( ! isset( $org['id'] ) ) {
				$lead_created = $this->add_lead( $lead_title, $person['id'], null );
			} else {
				$lead_created = $this->add_lead( $lead_title, $person['id'], $org['id'] );
			}

			if ( $lead_created ) {

				if ( ! empty( $notes ) ) {

					$note_content = '';
					if ( is_array( $notes ) ) {
						foreach ( $notes as $note ) {
							$note_content .= '<b>' . $note['label'] . '</b>: <br>' . $note['value'];
							$note_content .= '<br><br>';
						}
					} else {
						$note_content = $notes;
					}

					$this->add_note( $note_content, $lead_created['id'] );
				}

				return $lead_created;
			}
		}

		return $lead_created;
	}

	/**
	 * Constructs a lead and related person/org from grouped field arrays.
	 *
	 * @param array  $lead_fields   Key-value pairs for lead creation.
	 * @param array  $person_fields Key-value pairs for person creation.
	 * @param array  $org_fields    Key-value pairs for organization creation.
	 * @param string $note_html     Formatted note content.
	 * @return array|false
	 */
	public function construct_lead_from_groups( array $lead_fields, array $person_fields, array $org_fields, string $note_html = '' ) {

		$org  = null;
		$person = null;

		// Create or update organization
		if ( ! empty( $org_fields['name'] ) ) {
			$existing = $this->search_org( $org_fields['name'], 'name', true );
			if ( $existing ) {
				$org = $this->update_org( $existing['id'], $org_fields );
			} else {
				$org = $this->add_org( $org_fields['name'], $org_fields );
			}
			if ( $org && count( $org_fields ) > 1 ) {
				unset( $org_fields['name'] );
				$this->update_org( $org['id'], $org_fields );
			}
		}

		// Create or update person
		if ( ! empty( $person_fields['name'] ) ) {
			$existing_person = null;

			// Try to find by email if available, else by name
			if ( ! empty( $person_fields['email'] ) ) {
				$existing_person = $this->search_person( $person_fields['email'], 'email', true );
			} else {
				$existing_person = $this->search_person( $person_fields['name'], 'name', true );
			}

			if ( $existing_person ) {
				$person = $this->update_person( $existing_person['id'], $person_fields );
			} else {
				$person = $this->add_person(
					$person_fields['name'],
					$org['id'] ?? null,
					$person_fields['email'] ?? null,
					$person_fields['phone'] ?? null,
					$person_fields
				);
			}
		}

		// Create lead
		if ( empty( $lead_fields['title'] ) && ! empty( $person_fields['name'] ) ) {
			$lead_fields['title'] = $person_fields['name'] . ' Lead';
		}

		$lead = $this->add_lead(
			$lead_fields['title'] ?? 'Untitled Lead',
			$person['id'] ?? null,
			$org['id'] ?? null,
			$lead_fields
		);

		// Add note
		if ( $lead && $note_html ) {
			$this->add_note( $note_html, $lead['id'], $person['id'] ?? null, $org['id'] ?? null );
		}

		return $lead ? $lead : false;
	}

	/**
	 * Retrieve a Pipedrive custom field key, or resolve a single option ID for that field.
	 *
	 * When $label is null, returns the custom field API key for the given field name.
	 * When $label is provided for an enum type field, returns the integer option ID that matches the label.
	 * Uses the {object}Fields endpoint, for example dealFields, personFields, organizationFields, or leadFields.
	 *
	 * @param string|null $target_field_name The display name of the field as seen in Pipedrive, for example "Source channel".
	 * @param string|null $label             The option label to resolve to its numeric ID, pass null to get the field key instead.
	 * @param string      $object            The object type to inspect, for example 'deal', 'person', 'organization', or 'lead'. Default 'deal'.
	 *
	 * @return string|int|null Field key when $label is null, option ID when $label is provided, null when not found.
	 *
	 * @throws \Exception On HTTP or API errors raised by send_pipedrive_request.
	 */
	protected function get_custom_field_option( $target_field_name = null, $label = null, $object = 'deal' ) {
		$data = $this->send_pipedrive_request( 'GET', $object . 'Fields' );

		if ( empty( $data['data'] ) || ! is_array( $data['data'] ) ) {
			return null;
		}

		foreach ( $data['data'] as $field ) {
			if ( ! empty( $field['name'] ) && $field['name'] === $target_field_name ) {
				if ( is_null( $label ) ) {
					return $field['key'];
				} else {
					foreach ( $field['options'] as $opt ) {
						if ( strcasecmp( $opt['label'], $label ) === 0 ) {
							return (int) $opt['id'];
						}
					}
				}
			}
		}

		return null;
	}

	/**
	 * Return a sanitized cookie value or null.
	 *
	 * This method retrieves and sanitizes a cookie value for the given cookie name.
	 *
	 * @param string $name The name of the cookie.
	 *
	 * @return string|null The sanitized cookie value, or null if the cookie is not set or empty.
	 */
	private function get_cookie_value( string $name ): ?string {
		if ( isset( $_COOKIE[ $name ] ) && '' !== $_COOKIE[ $name ] ) {
			return sanitize_text_field( wp_unslash( $_COOKIE[ $name ] ) );
		}

		return null;
	}

	/**
	 * Maps the UTM source to a Pipedrive source channel label.
	 *
	 * This method maps the UTM source parameter to a predefined Pipedrive source channel.
	 * It categorizes sources as Google Ads, Email Marketing, Organic Search, or defaults to "(None)".
	 *
	 * @param string|null $utm_source The value of the utm_source query parameter.
	 *
	 * @return string The corresponding Pipedrive source channel label.
	 */
	private function map_source_channel( ?string $utm_source ): string {
		$src = strtolower( trim( (string) $utm_source ) );
		if ( 'gads' === $src || 'ppc' === $src ) {
			return 'Google Ads';
		}

		if ( 'email' === $src ) {
			return 'Email Marketing';
		}

		if ( '' === $src || null === $utm_source ) {
			return 'Organic Search';
		}

		return '(None)';
	}

	/**
	 * Retrieves all available Pipedrive fields for a given object type.
	 *
	 * Supported types: `person`, `organization`, or `lead`.
	 * Returns both standard and custom fields in an associative array
	 * where the key is the API field key and the value is the display name.
	 *
	 * This method communicates directly with the `/personFields`,
	 * `/organizationFields`, or `/leadFields` Pipedrive API endpoints,
	 * depending on the specified `$type`.
	 *
	 * Example:
	 * ```php
	 * $fields = $client->get_fields('person');
	 * // Returns: ['name' => 'Name', 'email' => 'Email', 'custom_field_abc123' => 'Referral Source']
	 * ```
	 *
	 * @param string $type The object type to retrieve fields for. Supported values:
	 *                     'person', 'organization', or 'lead'. Defaults to 'person'.
	 *
	 * @return array Associative array of field_key => field_label pairs.
	 *
	 * @throws \InvalidArgumentException If an invalid object type is specified.
	 */
	public function get_fields( $type = 'person' ) {
		try {
			switch ( strtolower( $type ) ) {
				case 'person':
					$endpoint = 'personFields';
					break;
				case 'organization':
				case 'org':
					$endpoint = 'organizationFields';
					break;
				case 'lead':
					$endpoint = 'leadFields';
					break;
				default:
					throw new \InvalidArgumentException( 'Invalid field type specified.' );
			}

			$data = $this->send_pipedrive_request( 'GET', $endpoint );

			if ( empty( $data['data'] ) || ! is_array( $data['data'] ) ) {
				return [];
			}

			$fields = [];
			foreach ( $data['data'] as $field ) {
				// Skip deprecated or system-only fields
				if ( ! empty( $field['name'] ) && ! empty( $field['key'] ) ) {
					$fields[ $field['key'] ] = $field['name'];
				}
			}

			// Sort alphabetically by label
			asort( $fields );

			return $fields;

		} catch ( \Exception $e ) {
			error_log( '[GravityForms→Pipedrive] Failed to fetch ' . $type . ' fields: ' . $e->getMessage() );
			return [];
		}
	}
}
