<?php
/**
 * CAPI Sender — HTTP transport to Meta Graph API.
 *
 * Handles wp_remote_post(), HTTP status classification,
 * rate limiting (dual-layer), and test_event_code support.
 *
 * @package CapiFlow
 */

namespace CapiFlow\Core;

defined( 'ABSPATH' ) || exit;

class CAPI_Sender {

    /**
     * Send event payload to Meta CAPI.
     *
     * @param array  $payload  Complete CAPI payload (data array).
     * @param string $pixel_id Pixel ID to send to.
     * @return array Result: ['success' => bool, 'http_code' => int, 'response' => string]
     */
    public function send( $payload, $pixel_id = '' ) {
        if ( empty( $pixel_id ) ) {
            $pixel_id = get_option( 'capiflow_pixel_id', '' );
        }

        $access_token = $this->get_access_token( $pixel_id );

        if ( empty( $pixel_id ) || empty( $access_token ) ) {
            return array(
                'success'   => false,
                'http_code' => 0,
                'response'  => 'Missing pixel_id or access_token',
            );
        }

        // Rate limit check (proactive — before API call).
        if ( $this->is_rate_limited( $pixel_id ) ) {
            return array(
                'success'   => false,
                'http_code' => 429,
                'response'  => 'Proactive rate limit — throttled',
            );
        }

        // Build API URL.
        $api_version = get_option( 'capiflow_graph_api_version', 'v25.0' );
        $url         = "https://graph.facebook.com/{$api_version}/{$pixel_id}/events";

        // Prepare request body.
        // Meta's official PHP SDK (EventRequest::normalize()) keeps `data`
        // as a raw PHP array. The HTTP transport layer handles encoding.
        // When using wp_remote_post with Content-Type: application/json,
        // wp_json_encode($body) handles the final encoding, producing:
        //   {"data":[{event1},{event2}],"access_token":"..."}
        $body = array(
            'data'         => is_array( $payload ) ? $payload : json_decode( $payload, true ),
            'access_token' => $access_token,
        );

        // Test event code — only sent in Test Mode.
        // Production mode NEVER sends test_event_code, even if the field has a value.
        // This prevents accidental attribution loss from forgetting to clear the code.
        $env_mode = trim( get_option( 'capiflow_environment_mode', 'production' ) );
        if ( 'test' === $env_mode ) {
            $test_code = trim( get_option( 'capiflow_test_event_code', '' ) );
            if ( ! empty( $test_code ) ) {
                $body['test_event_code'] = $test_code;
            }
        }


        $timeout = (int) get_option( 'capiflow_api_timeout', 15 );

        $response = wp_remote_post( $url, array(
            'timeout'   => max( 5, min( $timeout, 30 ) ),
            'sslverify' => true,
            'headers'   => array( 'Content-Type' => 'application/json' ),
            'body'      => wp_json_encode( $body ),
        ) );

        // Handle WP error (timeout, DNS, etc.).
        if ( is_wp_error( $response ) ) {
            $error_msg = $response->get_error_message();
            Logger::log( 'CAPI_Send', '', $payload, 0, $error_msg, 'error' );

            return array(
                'success'   => false,
                'http_code' => 0,
                'response'  => $error_msg,
            );
        }

        $http_code     = (int) wp_remote_retrieve_response_code( $response );
        $response_body = wp_remote_retrieve_body( $response );

        // Log the result.
        $event_name = '';
        if ( is_array( $payload ) && isset( $payload[0]['event_name'] ) ) {
            $event_name = $payload[0]['event_name'];
        }

        $event_id = '';
        if ( is_array( $payload ) && isset( $payload[0]['event_id'] ) ) {
            $event_id = $payload[0]['event_id'];
        }

        $level = ( $http_code >= 200 && $http_code < 300 ) ? 'info' : 'error';
        Logger::log( $event_name, $event_id, $payload, $http_code, $response_body, $level );

        // Increment rate counters.
        $this->rate_increment( $pixel_id );

        return array(
            'success'   => ( $http_code >= 200 && $http_code < 300 ),
            'http_code' => $http_code,
            'response'  => $response_body,
        );
    }

    /**
     * Send a single event directly (fire-and-forget for PageView, ViewContent, etc.).
     *
     * @param array  $event_data Single event data array.
     * @param string $pixel_id   Pixel ID.
     * @return array Result.
     */
    public function send_fire_and_forget( $event_data, $pixel_id = '' ) {
        return $this->send( array( $event_data ), $pixel_id );
    }

    /**
     * Get the decrypted access token for a pixel.
     *
     * @param string $pixel_id Pixel ID.
     * @return string Decrypted access token.
     */
    private function get_access_token( $pixel_id ) {
        // Multi-pixel: check pixel-specific token first.
        $token = get_option( 'capiflow_access_token_' . $pixel_id, '' );

        if ( empty( $token ) ) {
            // Fallback to global token.
            $token = get_option( 'capiflow_access_token', '' );
        }

        /**
         * Filter the encrypted access token for a pixel.
         *
         * Pro's Multi_Pixel_Router provides per-pixel tokens from its rules table
         * without requiring separate wp_options entries per pixel.
         *
         * @param string $token    Encrypted access token.
         * @param string $pixel_id Meta pixel ID.
         */
        $token = apply_filters( 'capiflow_access_token_for_pixel', $token, $pixel_id );

        if ( empty( $token ) ) {
            return '';
        }

        return Encryption::decrypt( $token );
    }

    /**
     * Proactive dual-layer rate limiter.
     *
     * Layer 1: Per-pixel hourly limit.
     * Layer 2: Global hourly limit.
     *
     * @param string $pixel_id Pixel ID.
     * @return bool True if rate limited.
     */
    private function is_rate_limited( $pixel_id ) {
        $pixel_limit  = (int) get_option( 'capiflow_hourly_rate_limit', 2000 );
        $global_limit = (int) get_option( 'capiflow_global_hourly_limit', 10000 );

        $pixel_key  = 'capiflow_rate_' . $pixel_id;
        $global_key = 'capiflow_rate_global';

        $pixel_count  = (int) get_transient( $pixel_key );
        $global_count = (int) get_transient( $global_key );

        if ( $pixel_count >= $pixel_limit || $global_count >= $global_limit ) {
            Logger::warning( 'RateLimiter', "Throttled: pixel={$pixel_count}/{$pixel_limit}, global={$global_count}/{$global_limit}" );
            return true;
        }

        return false;
    }

    /**
     * Increment rate limit counters.
     *
     * @param string $pixel_id Pixel ID.
     */
    private function rate_increment( $pixel_id ) {
        $pixel_key  = 'capiflow_rate_' . $pixel_id;
        $global_key = 'capiflow_rate_global';

        $pixel_count = (int) get_transient( $pixel_key );
        set_transient( $pixel_key, $pixel_count + 1, HOUR_IN_SECONDS );

        $global_count = (int) get_transient( $global_key );
        set_transient( $global_key, $global_count + 1, HOUR_IN_SECONDS );
    }

    /**
     * Classify HTTP response for retry strategy.
     *
     * @param int $http_code HTTP response code.
     * @return string 'success' | 'permanent_fail' | 'rate_limited' | 'retry'
     */
    public function classify_response( $http_code ) {
        if ( $http_code >= 200 && $http_code < 300 ) {
            return 'success';
        }

        // 4xx (except 429) = permanent failure — DO NOT retry.
        if ( $http_code >= 400 && $http_code < 500 && 429 !== $http_code ) {
            return 'permanent_fail';
        }

        // 429 = rate limited — retry with DOUBLE delay.
        if ( 429 === $http_code ) {
            return 'rate_limited';
        }

        // 5xx or timeout (0) = server error — normal retry.
        return 'retry';
    }

    /**
     * Test the CAPI connection (for System Health dashboard).
     *
     * Uses the actual CAPI events endpoint with a test event.
     * This only requires ads_management permission (which CAPI tokens have),
     * unlike reading pixel metadata which requires ads_read.
     *
     * @param string $pixel_id     Pixel ID.
     * @param string $access_token Raw (unencrypted) access token.
     * @return array Test result.
     */
    public function test_connection( $pixel_id = '', $access_token = '' ) {
        if ( empty( $pixel_id ) ) {
            $pixel_id = get_option( 'capiflow_pixel_id', '' );
        }

        if ( empty( $access_token ) ) {
            $encrypted = get_option( 'capiflow_access_token', '' );
            $access_token = Encryption::decrypt( $encrypted );
        }

        if ( empty( $pixel_id ) || empty( $access_token ) ) {
            return array( 'success' => false, 'message' => 'Pixel ID or Access Token missing' );
        }

        $api_version = get_option( 'capiflow_graph_api_version', 'v25.0' );
        $url         = "https://graph.facebook.com/{$api_version}/{$pixel_id}/events";

        // Build a lightweight test event.
        $test_event = array(
            array(
                'event_name'    => 'PageView',
                'event_time'    => time(),
                'event_id'      => 'test_' . bin2hex( random_bytes( 8 ) ),
                'action_source' => 'website',
                'user_data'     => array(
                    'client_ip_address' => '0.0.0.0',
                    'client_user_agent' => 'CapiFlow/ConnectionTest',
                ),
            ),
        );

        $body = array(
            'data'         => $test_event,  // Raw array — wp_json_encode($body) handles final encoding.
            'access_token' => $access_token,
        );

        // Use test_event_code if configured (so test events don't appear in production).
        $test_code = get_option( 'capiflow_test_event_code', '' );
        if ( ! empty( $test_code ) ) {
            $body['test_event_code'] = $test_code;
        } else {
            // Generate a temporary test code prefix so it doesn't pollute real data.
            $body['test_event_code'] = 'TEST_CONNECTION_' . wp_rand( 10000, 99999 );
        }

        $response = wp_remote_post( $url, array(
            'timeout'   => 15,
            'sslverify' => true,
            'headers'   => array( 'Content-Type' => 'application/json' ),
            'body'      => wp_json_encode( $body ),
        ) );

        if ( is_wp_error( $response ) ) {
            $error_msg = $response->get_error_message();

            // Provide helpful messages for common shared hosting issues.
            if ( strpos( $error_msg, 'cURL error 28' ) !== false ) {
                return array( 'success' => false, 'message' => 'Timeout — your server may block outbound HTTPS. Contact your host.' );
            }
            if ( strpos( $error_msg, 'cURL error 7' ) !== false ) {
                return array( 'success' => false, 'message' => 'Connection refused — outbound port 443 may be blocked by your hosting firewall.' );
            }
            if ( strpos( $error_msg, 'cURL error 6' ) !== false ) {
                return array( 'success' => false, 'message' => 'DNS resolution failed — your server cannot reach graph.facebook.com.' );
            }

            return array( 'success' => false, 'message' => $error_msg );
        }

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

        // 200 with events_received = success.
        if ( $code >= 200 && $code < 300 ) {
            $received = isset( $body['events_received'] ) ? (int) $body['events_received'] : 0;
            return array(
                'success' => true,
                'message' => "Connected ✅ — Pixel {$pixel_id} accepted {$received} test event(s)",
            );
        }

        // Parse Facebook error for helpful message.
        $error = isset( $body['error']['message'] ) ? $body['error']['message'] : 'Unknown error';
        $error_code = isset( $body['error']['code'] ) ? (int) $body['error']['code'] : 0;

        // Common error code translations.
        $hints = array(
            190 => 'Invalid or expired Access Token. Generate a new one in Meta Events Manager.',
            100 => 'Check that your Pixel ID is correct and your token has ads_management permission.',
            10  => 'Permission denied. Ensure System User has access to this Pixel in Business Manager.',
            17  => 'Rate limited by Meta. Wait a few minutes and try again.',
        );

        if ( isset( $hints[ $error_code ] ) ) {
            $error .= ' — ' . $hints[ $error_code ];
        }

        return array( 'success' => false, 'message' => $error );
    }
}
