<?php
/**
 * Native Salesforce integration — plugin client.
 *
 * Owns the Connect/Disconnect calls, per-lead push, and payload assembly. The
 * middleware is the confidential OAuth client + push engine (one SiteStaffr-owned
 * Connected App); this class only talks to it over the existing HMAC channel and
 * records the per-lead sync status on the call row.
 *
 * Real-time push is fired non-blocking: on `sitestaffr_call_completed` (after the
 * qualified-lead flag) we schedule a single WP-Cron event, so the conversation
 * finalize is never delayed. The cron handler does the blocking push and stores
 * the returned status/Lead Id.
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 * @since      1.36.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter
class SiteStaffr_Salesforce {

	/**
	 * Cron hook that runs a single lead push off the request path.
	 */
	const PUSH_HOOK = 'sitestaffr_sf_push_lead';

	/**
	 * Transient caching the connection status (avoids a round-trip per lead).
	 */
	const STATUS_TRANSIENT = 'sitestaffr_sf_status';

	/**
	 * Option flag: has this site ever connected Salesforce? Lets the common
	 * (never-connected) case short-circuit without any middleware round-trip, so
	 * non-Salesforce sites pay zero network cost and the Settings page never
	 * blocks on a Salesforce status call.
	 */
	const CONNECTED_FLAG = 'sitestaffr_sf_connected_once';

	/**
	 * On a completed conversation, schedule a Salesforce push if the call is a
	 * qualified lead and the site has Salesforce connected. Non-blocking.
	 *
	 * @param int   $call_id        Call record ID.
	 * @param string $status        Final status (unused; signature parity).
	 * @param array $contact_fields Function-call contact fields.
	 * @return void
	 */
	public static function maybe_push_on_completed( $call_id, $status = '', $contact_fields = array() ) {
		$call_id = absint( $call_id );
		if ( ! $call_id ) {
			return;
		}
		// Only qualified leads, and only for sites that have connected Salesforce.
		// Gate on the local "ever connected" flag (not a live status call): a
		// transient status-endpoint blip must not silently drop the push — let it
		// schedule so push_lead attempts it and records synced/queued/failed
		// (visible + retryable) rather than vanishing. The flag read is free.
		if ( ! class_exists( 'SiteStaffr_Leads' ) || ! SiteStaffr_Leads::is_qualified( $contact_fields ) ) {
			return;
		}
		if ( ! get_option( self::CONNECTED_FLAG ) ) {
			return;
		}
		if ( ! wp_next_scheduled( self::PUSH_HOOK, array( $call_id ) ) ) {
			wp_schedule_single_event( time(), self::PUSH_HOOK, array( $call_id ) );
			// Nudge cron so the push is effectively real-time under any traffic.
			if ( function_exists( 'spawn_cron' ) ) {
				spawn_cron();
			}
		}
	}

	/**
	 * Push (or re-push) a single lead to Salesforce and record the result.
	 *
	 * Idempotent at the middleware (keyed on session_id) — safe to call again for
	 * a retry or a resumed conversation. Stores salesforce_sync + salesforce_lead_id
	 * on the call row.
	 *
	 * @param int $call_id Call record ID.
	 * @return string Resulting sync status ('synced'|'queued'|'skipped'|'failed').
	 */
	public static function push_lead( $call_id ) {
		$call_id = absint( $call_id );
		if ( ! $call_id ) {
			return 'failed';
		}

		$call = self::get_call( $call_id );
		if ( ! $call || empty( $call->is_qualified_lead ) ) {
			return 'skipped';
		}

		$payload  = self::build_lead_payload( $call );
		$response = self::call_middleware( '/api/integrations/salesforce/push-lead', $payload );

		$sync_status = 'failed';
		$lead_id     = '';
		if ( is_array( $response ) && ! empty( $response['status'] ) ) {
			$sync_status = sanitize_key( $response['status'] );
			if ( 'synced' === $sync_status && ! empty( $response['salesforce_lead_id'] ) ) {
				$lead_id = substr( sanitize_text_field( $response['salesforce_lead_id'] ), 0, 32 );
			}
			// A reconnect-required result means the connection is dead — bust the
			// status cache so the admin surfaces the reconnect nudge promptly.
			if ( ! empty( $response['reason'] ) && 'reconnect_required' === $response['reason'] ) {
				delete_transient( self::STATUS_TRANSIENT );
			}
		}

		self::store_sync_result( $call_id, $sync_status, $lead_id );
		return $sync_status;
	}

	/**
	 * Assemble the push-lead payload from a call row.
	 *
	 * @param object $call Call row (needs id, session_id, lead_*, ai_summary, service_type, started_at).
	 * @return array
	 */
	public static function build_lead_payload( $call ) {
		$settings = get_option( 'sitestaffr_settings', array() );
		$company  = ! empty( $settings['business_name'] ) ? stripslashes( $settings['business_name'] ) : '';

		// voice = WebRTC (webrtc_*/phone), text = chat_* — see conversation-usage.
		$service = isset( $call->service_type ) ? (string) $call->service_type : '';
		$channel = ( 0 === strpos( $service, 'chat' ) ) ? 'text' : 'voice';

		$recap = isset( $call->ai_summary ) ? wp_strip_all_tags( stripslashes( (string) $call->ai_summary ) ) : '';

		// 30 days, the same as the owner's own notification link. This one lands in a
		// third-party CRM, so there is no case for it living longer. It was 90 days —
		// the longest-lived transcript link the plugin produced — until 1.40.0.
		$transcript_url = '';
		if ( class_exists( 'SiteStaffr_Magic_Link' ) ) {
			$transcript_url = SiteStaffr_Magic_Link::generate_transcript_url( (int) $call->id );
		}

		return array(
			'session_id'    => isset( $call->session_id ) ? (string) $call->session_id : '',
			'name'          => isset( $call->lead_name ) ? (string) $call->lead_name : '',
			'phone'         => isset( $call->lead_phone ) ? (string) $call->lead_phone : '',
			'email'         => isset( $call->lead_email ) ? (string) $call->lead_email : '',
			'lead_interest' => isset( $call->lead_interest ) ? (string) $call->lead_interest : '',
			'company'       => $company,
			'recap'         => $recap,
			'occurred_at'   => self::to_iso_eastern( isset( $call->started_at ) ? $call->started_at : 0 ),
			'channel'       => $channel,
			'transcript_url' => $transcript_url,
		);
	}

	/**
	 * Whether a call row should offer a manual push to Salesforce.
	 *
	 * Includes leads that already synced. Their transcript link expires like any
	 * other and nothing re-syncs it — the push is one-shot on conversation
	 * completion — so without a re-push the URL in the CRM is dead for good, and the
	 * person hitting it is a rep inside Salesforce who may have no WordPress login.
	 * The middleware push is idempotent on session_id, so re-pushing updates the same
	 * Lead with a fresh link rather than creating a duplicate.
	 *
	 * 'skipped' and never-attempted rows are excluded: a re-push would only skip again.
	 *
	 * @since  1.40.0
	 * @param  object $call Call row (needs is_qualified_lead, salesforce_sync).
	 * @return bool
	 */
	public static function can_repush( $call ) {
		if ( ! is_object( $call ) || empty( $call->is_qualified_lead ) ) {
			return false;
		}

		$sync = isset( $call->salesforce_sync ) ? (string) $call->salesforce_sync : '';

		return in_array( $sync, array( 'failed', 'queued', 'synced' ), true );
	}

	/**
	 * Request a Salesforce authorize URL (browser then redirects to it).
	 *
	 * @return string Authorize URL, or '' on failure.
	 */
	public static function get_authorize_url() {
		$response = self::call_middleware( '/api/integrations/salesforce/authorize-url', array() );
		return ( is_array( $response ) && ! empty( $response['authorize_url'] ) )
			? esc_url_raw( $response['authorize_url'] )
			: '';
	}

	/**
	 * Get the connection status (cached). Shape: connected, org_name, connected_at,
	 * edition_ok, needs_reconnect.
	 *
	 * @param bool $force Bypass the cache.
	 * @return array
	 */
	public static function get_status( $force = false ) {
		if ( ! $force ) {
			$cached = get_transient( self::STATUS_TRANSIENT );
			if ( is_array( $cached ) ) {
				return $cached;
			}
		}

		$response = self::call_middleware( '/api/integrations/salesforce/status', array() );
		$status   = array( 'connected' => false );
		if ( is_array( $response ) ) {
			$status = array(
				'connected'       => ! empty( $response['connected'] ),
				'org_name'        => isset( $response['org_name'] ) ? sanitize_text_field( $response['org_name'] ) : '',
				'connected_at'    => isset( $response['connected_at'] ) ? sanitize_text_field( $response['connected_at'] ) : '',
				'edition_ok'      => ! isset( $response['edition_ok'] ) || ! empty( $response['edition_ok'] ),
				'needs_reconnect' => ! empty( $response['needs_reconnect'] ),
			);
			// A live connected result means this site has connected at least once —
			// enables the fast-path short-circuit for future checks.
			if ( ! empty( $status['connected'] ) && ! get_option( self::CONNECTED_FLAG ) ) {
				update_option( self::CONNECTED_FLAG, 1, false );
			}
		}
		set_transient( self::STATUS_TRANSIENT, $status, 5 * MINUTE_IN_SECONDS );
		return $status;
	}

	/**
	 * Disconnect Salesforce (revoke + clear at the middleware) and drop the cache.
	 *
	 * @return bool
	 */
	public static function disconnect() {
		$response = self::call_middleware( '/api/integrations/salesforce/disconnect', array() );
		delete_transient( self::STATUS_TRANSIENT );
		delete_option( self::CONNECTED_FLAG );
		return is_array( $response ) && ! empty( $response['disconnected'] );
	}

	/**
	 * Signed POST to a middleware Salesforce endpoint; returns the decoded body
	 * array, or null on transport/config failure.
	 *
	 * @param string $path Middleware path (leading slash).
	 * @param array  $body Request body (site_token + site_url are added).
	 * @return array|null
	 */
	private static function call_middleware( $path, $body ) {
		if ( ! class_exists( 'SiteStaffr_Public_Middleware_Client' ) ) {
			return null;
		}
		$api_secret = get_option( 'sitestaffr_site_api_secret', get_option( 'sitestaffr_api_secret', '' ) );
		$preflight  = SiteStaffr_Public_Middleware_Client::get_public_preflight_config( $api_secret );
		if ( ! empty( $preflight['missing_api_secret'] ) || ! empty( $preflight['missing_middleware_url'] ) ) {
			return null;
		}

		$body['site_token'] = get_option( 'sitestaffr_site_token', '' );
		$body['site_url']   = esc_url_raw( get_site_url() );

		$endpoint = SiteStaffr_Public_Middleware_Client::build_middleware_endpoint( $preflight['middleware_url'], $path );
		$response = SiteStaffr_Public_Middleware_Client::post_signed_json_with_json_response(
			$endpoint,
			wp_json_encode( $body ),
			$api_secret,
			null,
			'',
			10,
			SiteStaffr_Public_Middleware_Client::extra_headers_json()
		);

		if ( is_wp_error( $response ) ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Salesforce: middleware call failed — ' . $response->get_error_message() );
			return null;
		}
		$status_code = isset( $response['status_code'] ) ? (int) $response['status_code'] : 0;
		$decoded     = isset( $response['decoded_body'] ) ? $response['decoded_body'] : null;
		if ( 200 !== $status_code || ! is_array( $decoded ) ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Salesforce: middleware returned status ' . $status_code . ' for ' . $path );
			return null;
		}
		return $decoded;
	}

	/**
	 * Persist the per-lead sync outcome on the call row.
	 *
	 * @param int    $call_id Call ID.
	 * @param string $status  Sync status.
	 * @param string $lead_id Salesforce Lead Id (when synced).
	 * @return void
	 */
	private static function store_sync_result( $call_id, $status, $lead_id ) {
		global $wpdb;
		$calls_table = $wpdb->prefix . 'phoneease_calls';
		$data        = array( 'salesforce_sync' => substr( $status, 0, 20 ) );
		$format      = array( '%s' );
		if ( '' !== $lead_id ) {
			$data['salesforce_lead_id'] = $lead_id;
			$format[]                   = '%s';
		}
		$wpdb->update( $calls_table, $data, array( 'id' => $call_id ), $format, array( '%d' ) );
	}

	/**
	 * Read the minimal call fields needed for a push.
	 *
	 * @param int $call_id Call ID.
	 * @return object|null
	 */
	private static function get_call( $call_id ) {
		global $wpdb;
		$calls_table = $wpdb->prefix . 'phoneease_calls';
		return $wpdb->get_row(
			$wpdb->prepare(
				'SELECT id, session_id, service_type, started_at, ai_summary, is_qualified_lead, lead_name, lead_phone, lead_email, lead_interest FROM %i WHERE id = %d',
				$calls_table,
				$call_id
			)
		);
	}

	/**
	 * Format a started_at timestamp (seconds or ms) as ISO 8601 in Eastern time.
	 *
	 * @param mixed $started_at Unix timestamp.
	 * @return string
	 */
	private static function to_iso_eastern( $started_at ) {
		$ts = (int) $started_at;
		if ( $ts > 9999999999 ) {
			$ts = (int) floor( $ts / 1000 );
		}
		if ( $ts <= 0 ) {
			return '';
		}
		try {
			$dt = new DateTime( '@' . $ts );
			$dt->setTimezone( new DateTimeZone( 'America/New_York' ) );
			return $dt->format( 'c' );
		} catch ( Exception $e ) {
			return '';
		}
	}
}
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter
