<?php
/**
 * TrackSharp REST API
 * 
 * Handles REST API endpoint registration and request processing.
 *
 * @package TrackSharp
 */

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

class TrackSharp_REST
{
    /** @var TrackSharp_Consent */
    private $consent;

    /** @var TrackSharp_Hasher */
    private $hasher;

    /** @var TrackSharp_GA4 */
    private $ga4;

    /** @var TrackSharp_Meta */
    private $meta;

    /** @var TrackSharp_Events_Log */
    private $events_log;

    /**
     * Constructor.
     *
     * @param TrackSharp_Consent         $consent    Consent handler.
     * @param TrackSharp_Hasher|null     $hasher     Hasher utility (Pro only).
     * @param TrackSharp_GA4             $ga4        GA4 client.
     * @param TrackSharp_Meta|null       $meta       Meta client (Pro only).
     * @param TrackSharp_Events_Log      $events_log Events logger.
     */
    public function __construct(
        TrackSharp_Consent $consent,
        ?object $hasher,
        TrackSharp_GA4 $ga4,
        ?object $meta,
        TrackSharp_Events_Log $events_log
    ) {
        $this->consent = $consent;
        $this->hasher = $hasher;
        $this->ga4 = $ga4;
        $this->meta = $meta;
        $this->events_log = $events_log;
    }

    /**
     * Register REST routes.
     *
     * @return void
     */
    public function register_routes()
    {
        // Canonical endpoint
        register_rest_route('tracksharp/v1', '/p', [
            'methods' => 'POST',
            'callback' => [$this, 'handle_collect'],
            'permission_callback' => '__return_true',
        ]);

        // Legacy alias
        register_rest_route('tracksharp/v1', '/collect', [
            'methods' => 'POST',
            'callback' => [$this, 'handle_collect'],
            'permission_callback' => '__return_true',
        ]);
    }

    /**
     * Handle collect endpoint.
     *
     * @param \WP_REST_Request $req Request object.
     * @return \WP_REST_Response Response.
     */
    public function handle_collect(\WP_REST_Request $req)
    {
        $payload = json_decode($req->get_body(), true);

        if (!is_array($payload)) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'invalid json'], 400);
        }

        // Audit-only: log browser event without sending anywhere
        if (!empty($payload['audit_only'])) {
            return $this->handle_audit_only($payload);
        }

        // Server-side consent gate
        $want_meta = $this->meta && $this->meta->is_configured();
        $want_ga4 = $this->ga4->is_configured();

        $server_allows = [
            'meta' => $want_meta && $this->consent->server_consent_granted_for('meta'),
            'ga4' => $want_ga4 && $this->consent->server_consent_granted_for('ga4'),
        ];

        // If neither destination is allowed, short-circuit
        if (!$server_allows['meta'] && !$server_allows['ga4']) {
            return new \WP_REST_Response([
                'queued' => false,
                'reason' => 'no server consent',
                'server_consent' => $server_allows,
                'client_consent' => isset($payload['consent']) ? $payload['consent'] : null
            ], 202);
        }

        // Process the event
        return $this->process_event($payload, $server_allows);
    }

    /**
     * Handle audit-only events.
     *
     * @param array $payload Request payload.
     * @return \WP_REST_Response Response.
     */
    private function handle_audit_only(array $payload)
    {
        $txid = null;
        if (!empty($payload['ecom']['transaction_id'])) {
            $txid = sanitize_text_field($payload['ecom']['transaction_id']);
        }

        $ename = sanitize_text_field($payload['event'] ?? 'unknown');
        $event_id = sanitize_text_field($payload['event_id'] ?? '');
        $channel = in_array(($payload['channel'] ?? ''), ['ga4', 'meta'], true) ? (string) $payload['channel'] : 'ga4';

        $ga4_browser_sent = !empty($payload['ga4_browser_sent']);

        // When TrackSharp successfully sends a browser-side GA4 purchase, mark it on the order
        // so server-side fallbacks can skip to avoid double counting.
        //
        // Note: We do NOT mark this for "receipt-only" breadcrumbs (server-only mode or when
        // the browser could not send). Those receipts are still logged for auditing/dashboard,
        // but they should not suppress the server fallback.
        if ($channel === 'ga4' && $ga4_browser_sent && $txid && ctype_digit((string) $txid) && in_array($ename, ['purchase', 'purchase_sst'], true)) {
            $order_id = (int) $txid;
            update_post_meta($order_id, '_ws_ga4_browser_purchase_sent', '1');
            if (!empty($payload['event_id'])) {
                update_post_meta($order_id, '_ws_ga4_browser_purchase_event_id', sanitize_text_field($payload['event_id']));
            }
            update_post_meta($order_id, '_ws_ga4_browser_purchase_event', $ename);

            // Best-effort: if a fallback cron was scheduled, clear it now.
            if (function_exists('wp_clear_scheduled_hook')) {
                wp_clear_scheduled_hook('tracksharp_ga4_purchase_fallback', [$order_id]);
            }
            delete_post_meta($order_id, '_ws_ga4_purchase_fallback_scheduled');
        }

        if ($this->events_log->should_log_event($channel, 'browser', $ename)) {
            $already_logged = false;
            if ($txid && $event_id !== '') {
                global $wpdb;
                $table = esc_sql($this->events_log->table_name());
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped; values are prepared below.
                $sql = "SELECT 1 FROM {$table} WHERE channel=%s AND source=%s AND event_name=%s AND txid=%s AND event_id=%s ORDER BY id DESC LIMIT 1";
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query uses $wpdb->prepare() with placeholders.
                $already_logged = (bool) $wpdb->get_var($wpdb->prepare($sql, $channel, 'browser', $ename, $txid, $event_id));
            }

            if (!$already_logged) {
                $this->events_log->log_event([
                    'channel' => $channel,
                    'source' => 'browser',
                    'event_name' => $ename,
                    'event_id' => $event_id,
                    'txid' => $txid,
                    'ok' => 1,
                    'http_status' => null,
                    'consent' => ($channel === 'ga4' && in_array($ename, ['purchase', 'purchase_sst'], true))
                        ? ($ga4_browser_sent ? 'sent' : 'receipt')
                        : null,
                ]);
            }
        }

        return new \WP_REST_Response(['ok' => true, 'audit' => true], 202);
    }

    /**
     * Process and send event to destinations.
     *
     * @param array $payload       Request payload.
     * @param array $server_allows Allowed channels.
     * @return \WP_REST_Response Response.
     */
    private function process_event(array $payload, array $server_allows)
    {
        $event_id = sanitize_text_field($payload['event_id'] ?? wp_generate_uuid4());
        $ga4_client_id = sanitize_text_field($payload['ids']['ga4_client_id'] ?? '');
        $fbp = sanitize_text_field($payload['ids']['fbp'] ?? '');
        $fbc = sanitize_text_field($payload['ids']['fbc'] ?? '');
        $event = sanitize_text_field($payload['event'] ?? 'custom_event');
        $session_id = sanitize_text_field($payload['ids']['session_id'] ?? (isset($_COOKIE['ws_session_id']) ? wp_unslash($_COOKIE['ws_session_id']) : ''));
        $currency = sanitize_text_field($payload['ecom']['currency'] ?? (function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'EUR'));
        $value = floatval($payload['ecom']['value'] ?? 0);
        $items = is_array($payload['ecom']['items'] ?? null) ? $payload['ecom']['items'] : [];

        // Client IP & UA
        $client_ip = $this->get_client_ip();
        $client_ua = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';

        // Hash PII
        $user = is_array($payload['user'] ?? null) ? $payload['user'] : [];
        $hashed = $this->hash_user_data($user, $session_id);

        // Build GA4 user_data
        $ga_user_data = $this->build_ga4_user_data($hashed, $user);
        $user_id = $this->build_user_id($session_id);

        // Send GA4
        $ga4_result = ['ok' => false, 'reason' => 'ga4 blocked by consent'];
        if ($server_allows['ga4']) {
            $page_url = sanitize_url($payload['page_url'] ?? '');
            $page_referrer = sanitize_url($payload['page_referrer'] ?? '');
            $user_agent = sanitize_text_field($payload['user_agent'] ?? $client_ua);

            $ga4_browser_sent = !empty($payload['ga4_browser_sent']);
            $mode = sanitize_text_field($payload['ga4_attribution_mode'] ?? '');

            // In reporting-first mode, if the browser hit was sent, we skip the server-side send
            // to avoid duplication. The browser hit is canonical for attribution.
            if ($mode === 'reporting_first' && $ga4_browser_sent) {
                $ga4_result = ['ok' => true, 'source' => 'browser_primary', 'reason' => 'skipped (browser sent)'];
            } else {
                $ga4_result = $this->send_ga4_event(
                    $payload,
                    $event,
                    $event_id,
                    $ga4_client_id,
                    $session_id,
                    $currency,
                    $value,
                    $items,
                    $ga_user_data,
                    $user_id,
                    $page_url,
                    $page_referrer,
                    $user_agent
                );
            }
        }

        // Send Meta
        $meta_result = ['ok' => false, 'reason' => 'meta blocked by consent'];
        if ($server_allows['meta']) {
            $meta_result = $this->send_meta_event($payload, $event, $event_id, $hashed, $fbp, $fbc, $currency, $value, $items, $client_ip, $client_ua);
        }

        return new \WP_REST_Response([
            'ok' => true,
            'ga4' => $ga4_result,
            'meta' => $meta_result,
            'event_id' => $event_id
        ], 202);
    }

    /**
     * Hash user data fields.
     *
     * @param array  $user       User data from payload.
     * @param string $session_id Session ID.
     * @return array Hashed values.
     */
    private function hash_user_data(array $user, string $session_id)
    {
        $addr = is_array($user['address'] ?? null) ? $user['address'] : [];
        $country_raw = isset($addr['country']) ? strtolower(trim((string) $addr['country'])) : null;

        // If hasher is not available (free version), return pre-hashed values only
        if (!$this->hasher) {
            return [
                'em' => $user['email_hashed'] ?? null,
                'ph' => $user['phone_hashed'] ?? null,
                'fn' => $user['first_name_hashed'] ?? null,
                'ln' => $user['last_name_hashed'] ?? null,
                'ct' => $addr['city_hashed'] ?? null,
                'st' => $addr['region_hashed'] ?? null,
                'zp' => $addr['postal_hashed'] ?? null,
                'country' => $addr['country_hashed'] ?? null,
                'country_raw' => $country_raw,
                'external_id' => null,
                'addr' => $addr,
            ];
        }

        $raw_email = isset($user['email']) ? $this->hasher->norm_email($user['email']) : null;
        $raw_phone = isset($user['phone']) ? $this->hasher->norm_phone($user['phone']) : null;

        $em_h = $raw_email ? $this->hasher->h256_or_null($raw_email) : ($user['email_hashed'] ?? null);
        $ph_h = $raw_phone ? $this->hasher->h256_or_null($raw_phone) : ($user['phone_hashed'] ?? null);

        $fn_h = isset($user['first_name']) ? $this->hasher->h256_or_null($this->hasher->norm_name($user['first_name'])) : ($user['first_name_hashed'] ?? null);
        $ln_h = isset($user['last_name']) ? $this->hasher->h256_or_null($this->hasher->norm_name($user['last_name'])) : ($user['last_name_hashed'] ?? null);

        $ct_h = isset($addr['city']) ? $this->hasher->h256_or_null($this->hasher->norm_str($addr['city'])) : ($addr['city_hashed'] ?? null);
        $st_h = isset($addr['region']) ? $this->hasher->h256_or_null($this->hasher->norm_str($addr['region'])) : ($addr['region_hashed'] ?? null);
        $zp_h = isset($addr['postal']) ? $this->hasher->h256_or_null($this->hasher->norm_zip($addr['postal'])) : ($addr['postal_hashed'] ?? null);
        $street_h = isset($addr['street']) ? $this->hasher->h256_or_null($this->hasher->norm_street($addr['street'])) : ($addr['street_hashed'] ?? null);

        $country_h = $country_raw ? $this->hasher->h256_or_null($country_raw) : ($addr['country_hashed'] ?? null);

        $external_id_h = !empty($session_id) ? $this->hasher->h256_or_null($session_id) : null;

        return [
            'em' => $em_h,
            'ph' => $ph_h,
            'fn' => $fn_h,
            'ln' => $ln_h,
            'ct' => $ct_h,
            'st' => $st_h,
            'zp' => $zp_h,
            'street' => $street_h,
            'country' => $country_h,
            'country_raw' => $country_raw,
            'external_id' => $external_id_h,
            'addr' => $addr,
        ];
    }

    /**
     * Build GA4 user_data structure.
     *
     * @param array $hashed Hashed user data.
     * @param array $user   Raw user data.
     * @return array GA4 user_data.
     */
    private function build_ga4_user_data(array $hashed, array $user)
    {
        // Pro feature gate: Enhanced Conversions (user_data hashing) requires Pro
        if (!TrackSharp_Features::is_enhanced_conversions_available()) {
            return []; // Free users don't get Enhanced Conversions
        }

        $addr = $hashed['addr'];

        // For GA4, city/region/postal need normalized but unhashed values
        // If hasher is not available, use raw values
        $city = isset($addr['city']) && $this->hasher ? $this->hasher->norm_str($addr['city']) : ($addr['city'] ?? null);
        $region = isset($addr['region']) && $this->hasher ? $this->hasher->norm_str($addr['region']) : ($addr['region'] ?? null);
        $postal = isset($addr['postal']) && $this->hasher ? $this->hasher->norm_zip($addr['postal']) : ($addr['postal'] ?? null);

        $ga_user_address = array_filter([
            'sha256_first_name' => $hashed['fn'] ?: null,
            'sha256_last_name' => $hashed['ln'] ?: null,
            'sha256_street' => $hashed['street'] ?: null,
            'city' => $city,
            'region' => $region,
            'postal_code' => $postal,
            'country' => $hashed['country_raw'] ? strtoupper($hashed['country_raw']) : null,
        ]);

        return array_filter([
            'sha256_email_address' => $hashed['em'] ?: null,
            'sha256_phone_number' => $hashed['ph'] ?: null,
            'address' => $ga_user_address ?: null,
        ]);
    }

    /**
     * Build user ID for GA4.
     *
     * @param string $session_id Session ID.
     * @return string User ID.
     */
    private function build_user_id(string $session_id)
    {
        if (function_exists('is_user_logged_in') && is_user_logged_in()) {
            $uid = get_current_user_id();
            if ($uid) {
                return 'uid_' . intval($uid);
            }
        }

        if (!empty($session_id)) {
            return 'sid_' . substr($session_id, 0, 36);
        }

        return '';
    }

    /**
     * Send GA4 event.
     *
     * @param array  $payload      Original payload.
     * @param string $event        Event name.
     * @param string $event_id     Event ID.
     * @param string $ga4_client_id GA4 client ID.
     * @param string $session_id   Session ID.
     * @param string $currency     Currency code.
     * @param float  $value        Event value.
     * @param array  $items         Items array.
     * @param array  $ga_user_data  GA4 user data.
     * @param string $user_id       User ID.
     * @param string $page_location Optional page URL.
     * @param string $page_referrer Optional page referrer.
     * @param string $user_agent    Optional user agent.
     * @return array Result.
     */
    private function send_ga4_event($payload, $event, $event_id, $ga4_client_id, $session_id, $currency, $value, $items, $ga_user_data, $user_id, $page_location = '', $page_referrer = '', $user_agent = '')
    {
        $params = [
            'currency' => $currency,
            'value' => $value,
            'items' => $items,
            'event_id' => $event_id,
            'engagement_time_msec' => 1,
            'page_location' => $page_location ?: null,
            'page_referrer' => $page_referrer ?: null,
            'user_agent' => $user_agent ?: null,
            'ua' => $user_agent ?: null, // Some implementations expect 'ua' parameter
        ];

        if (!empty($payload['ids']['ga_session_id'])) {
            $params['session_id'] = (int) $payload['ids']['ga_session_id'];
        }
        if (!empty($payload['ids']['ga_session_number'])) {
            $params['session_number'] = (int) $payload['ids']['ga_session_number'];
        }

        // Pro feature gate: Google Ads attribution requires Pro
        if (TrackSharp_Features::is_google_ads_available()) {
            foreach (['gclid', 'wbraid', 'gbraid'] as $k) {
                if (!empty($payload['ids'][$k])) {
                    $params[$k] = sanitize_text_field($payload['ids'][$k]);
                }
            }
        }

        $ga4_payload = [
            'client_id' => $ga4_client_id ?: ($session_id ?: (time() . '.' . wp_rand(100000, 999999))),
            'user_id' => $ga_user_data ? $user_id : null,
            'user_data' => $ga_user_data ?: null,
            'events' => [['name' => $event, 'params' => $params]]
        ];

        // Build sGTM-style cookie header. When the browser calls this REST endpoint its cookies
        // are available in $_COOKIE. Forwarding them as HTTP headers on the outbound MP call
        // mirrors how sGTM proxies browser hits, enabling proper session-based ads attribution.
        $cookie_parts = [];
        foreach ($_COOKIE as $cookie_key => $cookie_val) {
            $k = (string) $cookie_key;
            if (($k === '_ga' || strpos($k, '_ga_') === 0 || strpos($k, '_gcl_') === 0) && !empty($cookie_val)) {
                $cookie_parts[] = $k . '=' . sanitize_text_field(wp_unslash($cookie_val));
            }
        }
        $http_context = array_filter([
            'cookie'     => $cookie_parts ? implode('; ', $cookie_parts) : null,
            'client_ip'  => $this->get_client_ip() ?: null,
            'user_agent' => $user_agent ?: null,
        ]);
        if ($http_context) {
            $ga4_payload['_http_context'] = $http_context;
        }

        $result = $this->ga4->send($ga4_payload);

        if ($this->events_log->should_log_event('ga4', 'server', $event)) {
            // Log marketing consent status - this shows orders that GTM would have missed
            // (GTM requires marketing consent, but we send with statistics consent)
            $marketing_category = apply_filters('tracksharp_consent_category_marketing', 'marketing');
            $marketing_granted = $this->consent->wp_consent_has($marketing_category);
            $this->events_log->log_event([
                'channel' => 'ga4',
                'source' => 'server',
                'event_name' => $event,
                'event_id' => $event_id,
                'txid' => $payload['ecom']['transaction_id'] ?? null,
                'ok' => !empty($result['ok']),
                'http_status' => isset($result['status']) ? (int) $result['status'] : null,
                'consent' => $marketing_granted ? 'granted' : 'denied',
            ]);
        }

        return $result;
    }

    /**
     * Send Meta event.
     *
     * @param array  $payload   Original payload.
     * @param string $event     Event name.
     * @param string $event_id  Event ID.
     * @param array  $hashed    Hashed user data.
     * @param string $fbp       Facebook browser ID.
     * @param string $fbc       Facebook click ID.
     * @param string $currency  Currency code.
     * @param float  $value     Event value.
     * @param array  $items     Items array.
     * @param string $client_ip Client IP.
     * @param string $client_ua Client user agent.
     * @return array Result.
     */
    private function send_meta_event($payload, $event, $event_id, $hashed, $fbp, $fbc, $currency, $value, $items, $client_ip, $client_ua)
    {
        // Guard: Meta requires Pro (hasher and meta classes)
        if (!$this->meta || !$this->hasher) {
            return ['ok' => false, 'reason' => 'meta requires pro'];
        }

        $user_data = array_filter([
            'em' => $hashed['em'],
            'ph' => $hashed['ph'],
            'fn' => $hashed['fn'],
            'ln' => $hashed['ln'],
            'ct' => $hashed['ct'],
            'st' => $hashed['st'],
            'zp' => $hashed['zp'],
            'country' => $hashed['country'],
            'external_id' => $hashed['external_id'],
            'fbp' => $fbp ?: null,
            'fbc' => $fbc ?: null,
            'client_user_agent' => $client_ua,
            'client_ip_address' => $client_ip,
        ]);

        $val = $this->hasher->validate_user_data_for_meta($user_data);
        $user_data = $val['clean'];

        $result = $this->meta->send([
            'event_name' => $this->meta->event_map($event),
            'event_id' => $event_id,
            'event_time' => time(),
            'action_source' => 'website',
            'event_source_url' => !empty($payload['page_url'])
                ? esc_url_raw($payload['page_url'])
                : ((is_ssl() ? 'https://' : 'http://') . sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'] ?? '')) . sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? ''))),
            'user_data' => $user_data,
            'custom_data' => [
                'currency' => $currency,
                'value' => $value,
                'contents' => array_map(function ($i) {
                    return [
                        'id' => sanitize_text_field($i['item_id'] ?? ''),
                        'quantity' => intval($i['quantity'] ?? 1),
                        'item_price' => floatval($i['price'] ?? 0),
                    ];
                }, $items),
                'content_type' => 'product'
            ],
        ]);

        $this->events_log->log_event([
            'channel' => 'meta',
            'source' => 'server',
            'event_name' => $this->meta->event_map($event),
            'event_id' => $event_id,
            'txid' => $payload['ecom']['transaction_id'] ?? null,
            'ok' => !empty($result['ok']),
            'http_status' => isset($result['status']) ? (int) $result['status'] : null,
            'consent' => $this->consent->server_consent_granted_for('meta') ? 'granted' : 'denied',
        ]);

        return $result;
    }

    /**
     * Get client IP address.
     *
     * @return string Client IP.
     */
    private function get_client_ip()
    {
        foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'] as $k) {
            if (!empty($_SERVER[$k])) {
                $iplist = explode(',', sanitize_text_field(wp_unslash($_SERVER[$k])));
                return trim($iplist[0]);
            }
        }
        return '';
    }
}
