<?php
/**
 * Cloud API client.
 *
 * @package KwiroAISalesAgent
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class Kwiro_API_Client
 *
 * Handles all communication with the AI Sales Assistant cloud API.
 */
class Kwiro_API_Client {

	/**
	 * Constructor-provided API URL override. Null means "resolve at call time
	 * from the KWIRO_API_URL constant or the production default". We
	 * intentionally do NOT eagerly resolve in the constructor:
	 *
	 * Some installs place `define('KWIRO_API_URL', …)` AFTER
	 * `require_once wp-settings.php` in wp-config.php. In that ordering the
	 * constant is not defined at `plugins_loaded` time, so an eager resolve
	 * would freeze the URL to the production default even though a dev
	 * override is about to be declared. Lazy resolution via get_api_url()
	 * avoids that class of bug entirely.
	 *
	 * @var string|null
	 */
	private $api_url_override;

	/**
	 * Request timeout in seconds.
	 *
	 * @var int
	 */
	private $timeout = 30;

	/**
	 * Constructor.
	 *
	 * @param string|null $api_url Optional API URL override (for testing).
	 */
	public function __construct( $api_url = null ) {
		$this->api_url_override = $api_url;
	}

	/**
	 * Current API base URL — resolved on every call so `define(KWIRO_API_URL)`
	 * declarations that land after plugins_loaded still take effect.
	 *
	 * @return string
	 */
	public function get_api_url() {
		if ( null !== $this->api_url_override ) {
			return $this->api_url_override;
		}
		if ( defined( 'KWIRO_API_URL' ) ) {
			return KWIRO_API_URL;
		}
		return 'https://api.kwiro.ai';
	}

	/**
	 * Back-compat accessor — `$this->api_url` is referenced in many places in
	 * this class. A magic getter keeps those read sites working with zero
	 * edits while still routing through the lazy resolver.
	 *
	 * @param string $name Property name.
	 * @return mixed
	 */
	public function __get( $name ) {
		if ( 'api_url' === $name ) {
			return $this->get_api_url();
		}
		return null;
	}

	/**
	 * Get the API key.
	 *
	 * @return string
	 */
	private function get_api_key() {
		return get_option( 'kwiro_api_key', '' );
	}

	/**
	 * Get the store ID.
	 *
	 * @return string
	 */
	public function get_store_id() {
		return get_option( 'kwiro_store_id', '' );
	}

	/**
	 * Get a valid JWT token, refreshing if needed.
	 *
	 * @return string|WP_Error JWT token or error.
	 */
	private function get_jwt_token() {
		$token = get_transient( 'kwiro_jwt_token' );
		if ( $token ) {
			return $token;
		}

		$api_key = $this->get_api_key();
		if ( empty( $api_key ) ) {
			return new \WP_Error( 'kwiro_no_key', __( 'API key not configured.', 'kwiro-ai-sales-agent' ) );
		}

		$response = wp_remote_post(
			$this->api_url . '/api/auth/verify',
			array(
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode( array( 'api_key' => $api_key ) ),
				'timeout' => $this->timeout,
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = wp_remote_retrieve_response_code( $response );
		if ( $code < 200 || $code >= 300 ) {
			return new \WP_Error( 'kwiro_auth_failed', __( 'Failed to authenticate with cloud API.', 'kwiro-ai-sales-agent' ) );
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( empty( $body['data']['token'] ) ) {
			return new \WP_Error( 'kwiro_auth_failed', __( 'Failed to authenticate with cloud API.', 'kwiro-ai-sales-agent' ) );
		}

		$token = $body['data']['token'];

		// Store the store_id if returned.
		if ( ! empty( $body['data']['store_id'] ) ) {
			update_option( 'kwiro_store_id', sanitize_text_field( $body['data']['store_id'] ) );
		}

		// Store the public key for widget authentication.
		if ( ! empty( $body['data']['public_key'] ) ) {
			update_option( 'kwiro_public_key', sanitize_text_field( $body['data']['public_key'] ) );
		}

		// Store dashboard token if returned.
		if ( ! empty( $body['data']['dashboard_token'] ) ) {
			update_option( 'kwiro_dashboard_token', sanitize_text_field( $body['data']['dashboard_token'] ) );
		}

		// Cache for 22 hours (token expires in 24h).
		set_transient( 'kwiro_jwt_token', $token, 22 * HOUR_IN_SECONDS );

		return $token;
	}

	/**
	 * Build request headers with JWT auth.
	 *
	 * @return array|WP_Error Headers array or error if auth fails.
	 */
	private function get_headers() {
		$token = $this->get_jwt_token();

		if ( is_wp_error( $token ) ) {
			return $token;
		}

		return array(
			'Content-Type'  => 'application/json',
			'Authorization' => 'Bearer ' . $token,
			'X-Store-ID'    => $this->get_store_id(),
			'X-Plugin-Ver'  => KWIRO_VERSION,
		);
	}

	/**
	 * Make a POST request to the cloud API.
	 *
	 * @param string $endpoint API endpoint path.
	 * @param array  $body     Request body.
	 * @return array|WP_Error Response data or error.
	 */
	public function post( $endpoint, $body = array() ) {
		$headers = $this->get_headers();
		if ( is_wp_error( $headers ) ) {
			return $headers;
		}

		$response = wp_remote_post(
			$this->api_url . $endpoint,
			array(
				'headers' => $headers,
				'body'    => wp_json_encode( $body ),
				'timeout' => $this->timeout,
			)
		);

		// On 401, the cached JWT is stale (server rotated its secret, or the
		// token expired earlier than our 22h transient). Clear the cache,
		// re-auth, and retry once. Without this, sales-tracker / sync /
		// feedback all give up silently for up to 22 hours.
		if ( ! is_wp_error( $response )
			&& 401 === wp_remote_retrieve_response_code( $response ) ) {
			delete_transient( 'kwiro_jwt_token' );
			$retry_headers = $this->get_headers();
			if ( ! is_wp_error( $retry_headers ) ) {
				$response = wp_remote_post(
					$this->api_url . $endpoint,
					array(
						'headers' => $retry_headers,
						'body'    => wp_json_encode( $body ),
						'timeout' => $this->timeout,
					)
				);
			}
		}

		return $this->handle_response( $response );
	}

	/**
	 * Make a GET request to the cloud API.
	 *
	 * @param string $endpoint API endpoint path.
	 * @param array  $params   Query parameters.
	 * @return array|WP_Error Response data or error.
	 */
	public function get( $endpoint, $params = array() ) {
		$headers = $this->get_headers();
		if ( is_wp_error( $headers ) ) {
			return $headers;
		}

		$url = $this->api_url . $endpoint;
		if ( ! empty( $params ) ) {
			$url = add_query_arg( $params, $url );
		}

		$response = wp_remote_get(
			$url,
			array(
				'headers' => $headers,
				'timeout' => $this->timeout,
			)
		);

		// Same 401-retry as POST: clear stale JWT and try once more.
		if ( ! is_wp_error( $response )
			&& 401 === wp_remote_retrieve_response_code( $response ) ) {
			delete_transient( 'kwiro_jwt_token' );
			$retry_headers = $this->get_headers();
			if ( ! is_wp_error( $retry_headers ) ) {
				$response = wp_remote_get(
					$url,
					array(
						'headers' => $retry_headers,
						'timeout' => $this->timeout,
					)
				);
			}
		}

		return $this->handle_response( $response );
	}

	/**
	 * Handle API response.
	 *
	 * @param array|WP_Error $response Raw response.
	 * @return array|WP_Error Parsed data or error.
	 */
	private function handle_response( $response ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

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

		if ( $code < 200 || $code >= 300 ) {
			$message = $data['error'] ?? sprintf(
				/* translators: %d: HTTP status code */
				__( 'API request failed with status %d', 'kwiro-ai-sales-agent' ),
				$code
			);
			return new WP_Error( 'kwiro_api_error', $message, array( 'status' => $code ) );
		}

		return $data;
	}

	/**
	 * Register this store with the cloud service.
	 *
	 * Unauthenticated endpoint — we don't have an API key yet. On success we
	 * persist api_key, store_id, and public_key to wp_options so subsequent
	 * authenticated calls work.
	 *
	 * @param array $store_data { domain, name?, currency? }.
	 * @return array|WP_Error Registration payload { store_id, api_key, public_key, token }.
	 */
	public function register_store( $store_data ) {
		$response = wp_remote_post(
			$this->api_url . '/api/auth/register',
			array(
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode( $store_data ),
				'timeout' => $this->timeout,
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

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

		if ( 409 === $code ) {
			// Two distinct 409 shapes from the cloud as of v1.2.x:
			//
			// STORE_CLAIMED  — domain is linked to a Kwiro user account.
			// Recoverable by signing into that account in
			// the dashboard. Cloud returns a redacted
			// email hint as data.claimed_by_email so we
			// can show "linked to u***@example.com".
			// (older builds) — generic "already registered" with no body.
			//
			// Fresh installs on a domain that previously had a Kwiro store
			// without claiming it now succeed (see registerStore() in the
			// cloud — it auto-rotates the api_key on the existing row), so
			// the only 409 a current cloud emits is STORE_CLAIMED.
			$err_data      = isset( $body['error']['data'] ) && is_array( $body['error']['data'] ) ? $body['error']['data'] : array();
			$claimed_email = isset( $err_data['claimed_by_email'] ) ? sanitize_text_field( $err_data['claimed_by_email'] ) : '';
			$message       = $claimed_email
				/* translators: %s: redacted email like u***@example.com */
				? sprintf( __( 'This domain is already linked to a Kwiro account (%s). Sign in at app.kwiro.ai with that email to retrieve your existing API key, then paste it into the Connection panel below.', 'kwiro-ai-sales-agent' ), $claimed_email )
				: __( 'This domain is already linked to a Kwiro account. Sign in at app.kwiro.ai to retrieve your existing API key, then paste it into the Connection panel below.', 'kwiro-ai-sales-agent' );
			return new WP_Error(
				'kwiro_store_claimed',
				$message,
				array(
					'claimed_by_email' => $claimed_email,
					'recoverable'      => true,
				)
			);
		}

		if ( $code < 200 || $code >= 300 || empty( $body['data'] ) ) {
			$message = isset( $body['error']['message'] ) ? $body['error']['message'] : sprintf(
				/* translators: %d: HTTP status code */
				__( 'Registration failed with status %d', 'kwiro-ai-sales-agent' ),
				$code
			);
			return new WP_Error( 'kwiro_register_failed', $message );
		}

		$data = $body['data'];

		if ( ! empty( $data['api_key'] ) ) {
			update_option( 'kwiro_api_key', sanitize_text_field( $data['api_key'] ) );
		}
		if ( ! empty( $data['store_id'] ) ) {
			update_option( 'kwiro_store_id', sanitize_text_field( $data['store_id'] ) );
		}
		if ( ! empty( $data['public_key'] ) ) {
			update_option( 'kwiro_public_key', sanitize_text_field( $data['public_key'] ) );
		}
		if ( ! empty( $data['token'] ) ) {
			set_transient( 'kwiro_jwt_token', $data['token'], 22 * HOUR_IN_SECONDS );
		}

		return $data;
	}

	/**
	 * Send products to cloud for syncing.
	 *
	 * @param array $products Array of formatted product data.
	 * @param array $args     Optional. Sync metadata: `batch_number`,
	 *                        `total_batches`, `trigger_source`.
	 * @return array|WP_Error Sync result.
	 */
	public function sync_products( $products, $args = array() ) {
		$payload = array( 'products' => $products );
		foreach ( array( 'batch_number', 'total_batches', 'trigger_source' ) as $k ) {
			if ( isset( $args[ $k ] ) ) {
				$payload[ $k ] = $args[ $k ];
			}
		}
		return $this->post( '/api/sync/batch', $payload );
	}

	/**
	 * Tell the cloud the batch loop has ended so it can mark the store
	 * complete and run a final embedding pass. Idempotent.
	 *
	 * @param array $args Optional. Currently supports `free_shipping_threshold`
	 *                    (float|null) — null means no rule; numeric value is the
	 *                    minimum cart total above which the store offers free
	 *                    shipping.
	 * @return array|WP_Error
	 */
	public function sync_finalize( $args = array() ) {
		$payload = array();
		if ( isset( $args['free_shipping_threshold'] ) ) {
			// Server treats null as "no rule"; numeric value is the min cart
			// total above which the store offers free shipping.
			$payload['free_shipping_threshold'] = $args['free_shipping_threshold'];
		}
		return $this->post( '/api/sync/finalize', $payload );
	}

	/**
	 * Record an audit event with no product payload — for WP CRON fires,
	 * manual sync clicks, plugin activation, and error surfacing.
	 *
	 * @param string $event_type cron_fired | manual_sync | activation | error.
	 * @param array  $extra      Optional keys: trigger_source, status, message, products_processed.
	 * @return array|WP_Error
	 */
	public function log_sync_event( $event_type, $extra = array() ) {
		$payload = array_merge( array( 'event_type' => $event_type ), $extra );
		return $this->post( '/api/sync/event', $payload );
	}

	/**
	 * Get the sync status from the cloud.
	 *
	 * @return array|WP_Error Status data.
	 */
	public function get_sync_status() {
		return $this->get( '/api/sync/status' );
	}

	/**
	 * Test the API connection.
	 *
	 * @return array|WP_Error Connection test result.
	 */
	public function test_connection() {
		return $this->get( '/api/health' );
	}

	/**
	 * Check if the API client is configured.
	 *
	 * @return bool
	 */
	public function is_configured() {
		return ! empty( $this->get_api_key() ) && ! empty( $this->get_store_id() );
	}

	/**
	 * Ask the cloud whether this store has been claimed by a Kwiro user.
	 * Used by the admin settings page to flip the "Connect to Kwiro" card
	 * into a "Connected" confirmation once linking is complete.
	 *
	 * Cached in a 2-minute transient — short enough that the UI reflects
	 * the link within a couple minutes of the dashboard completing it,
	 * while avoiding a fresh cloud hit on every admin page load.
	 *
	 * @param bool $force_fresh Bypass the transient cache (used by the
	 *                          "just returned from dashboard" check).
	 * @return array { linked: bool, user_email: ?string } — best-effort;
	 *               on network error returns the last-known cached result
	 *               or a default of linked=false so the plugin stays usable.
	 */
	public function get_link_status( $force_fresh = false ) {
		$cached = get_transient( 'kwiro_link_status' );
		if ( ! $force_fresh && is_array( $cached ) ) {
			return $cached;
		}
		$api_key = $this->get_api_key();
		if ( empty( $api_key ) ) {
			return array(
				'linked'     => false,
				'user_email' => null,
			);
		}

		$response = wp_remote_post(
			$this->api_url . '/api/auth/link-status',
			array(
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode( array( 'api_key' => $api_key ) ),
				'timeout' => 8,
			)
		);

		if ( is_wp_error( $response ) ) {
			// Network blip — return cached value if any, else assume unlinked.
			return is_array( $cached ) ? $cached : array(
				'linked'     => false,
				'user_email' => null,
			);
		}
		$code = wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( $code < 200 || $code >= 300 || empty( $body['data'] ) ) {
			return is_array( $cached ) ? $cached : array(
				'linked'     => false,
				'user_email' => null,
			);
		}

		$status = array(
			'linked'     => ! empty( $body['data']['linked'] ),
			'user_email' => $body['data']['user_email'] ?? null,
		);
		// Cache long when linked (nothing to refresh until explicit change),
		// short when unlinked so the UI flips to "Connected" within 30s of
		// the user completing the dashboard flow.
		$ttl = $status['linked'] ? 30 * MINUTE_IN_SECONDS : 30;
		set_transient( 'kwiro_link_status', $status, $ttl );
		return $status;
	}

	/**
	 * Exchange the stored API key for a short-lived link token used in the
	 * one-click Connect-to-Kwiro flow. Plugin hands the token to the browser
	 * which takes it to the dashboard for redemption under the user's session.
	 *
	 * @return array|WP_Error { link_token, expires_in } or error.
	 */
	public function begin_link() {
		$api_key = $this->get_api_key();
		if ( empty( $api_key ) ) {
			return new WP_Error( 'kwiro_no_key', __( 'API key not set. Wait for registration to finish, then try again.', 'kwiro-ai-sales-agent' ) );
		}

		$response = wp_remote_post(
			$this->api_url . '/api/auth/begin-link',
			array(
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode( array( 'api_key' => $api_key ) ),
				'timeout' => $this->timeout,
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

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

		if ( $code < 200 || $code >= 300 || empty( $body['data']['link_token'] ) ) {
			$message = isset( $body['error']['message'] ) ? $body['error']['message'] : sprintf(
				/* translators: %d: HTTP status code */
				__( 'Failed to start connect flow (status %d)', 'kwiro-ai-sales-agent' ),
				$code
			);
			return new WP_Error( 'kwiro_begin_link_failed', $message );
		}

		return $body['data'];
	}
}
