<?php
/**
 * Cookie Manager — REST proxy response-based cookie setting (cache-safe).
 *
 * Cookies are set via REST endpoint response headers, NOT via PHP init hook,
 * because cached pages (LiteSpeed, Varnish, WP Rocket) skip PHP execution.
 * REST endpoints are always uncacheable → cookies set reliably.
 *
 * @package CapiFlow
 */

namespace CapiFlow\Core;

defined( 'ABSPATH' ) || exit;

class Cookie_Manager {

    /**
     * Cookie names.
     */
    const COOKIE_UID = 'cs_uid';    // Custom stable user identifier.
    const COOKIE_FBC = '_fbc';      // Facebook click ID.
    const COOKIE_FBP = '_fbp';      // Facebook browser ID.

    /**
     * Cookie lifetime (180 days — Meta recommendation).
     *
     * @var int
     */
    const COOKIE_LIFETIME = 180 * DAY_IN_SECONDS;

    /**
     * Set cookies via REST response headers.
     *
     * Called from class-proxy-endpoint.php after processing a tracking request.
     * This ensures cookies are set even on cached pages.
     *
     * @param \WP_REST_Response $response REST response to modify.
     * @return \WP_REST_Response Modified response with Set-Cookie headers.
     */
    public static function set_tracking_cookies( $response ) {
        $domain   = wp_parse_url( home_url(), PHP_URL_HOST );
        $expires  = time() + self::COOKIE_LIFETIME;
        $secure   = is_ssl();
        $samesite = 'Lax';

        // cs_uid — stable user identifier (generate if not exists).
        if ( empty( $_COOKIE[ self::COOKIE_UID ] ) ) {
            $uid = self::generate_uid();
            self::add_cookie_header( $response, self::COOKIE_UID, $uid, $expires, $domain, $secure, $samesite );
        } else {
            // Server-side renewal: extend JS-set cookie lifetime (ITP bypass).
            self::add_cookie_header( $response, self::COOKIE_UID, sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_UID ] ) ), $expires, $domain, $secure, $samesite );
        }

        // ── First-party cookie server-side renewal (Safari ITP bypass) ──
        // Meta's fbevents.js SDK sets _fbp and _fbc via JavaScript (document.cookie).
        // Safari ITP limits JS-set cookies to 7 days (24h from "known trackers").
        // Server-set cookies (Set-Cookie header) are NOT limited by ITP.
        //
        // Strategy: READ existing SDK-set values → RE-SET via Set-Cookie header.
        // - NEVER generate new values (SDK is sole owner of the value).
        // - Only extend the lifetime from 7 days → 180 days.
        // - Used by PixelYourSite, Facebook for WooCommerce, and all major plugins.

        // _fbp — Facebook Browser ID (renew existing, never generate).
        if ( ! empty( $_COOKIE[ self::COOKIE_FBP ] ) ) {
            self::add_cookie_header( $response, self::COOKIE_FBP, sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_FBP ] ) ), $expires, $domain, $secure, $samesite );
        }

        // _fbc — Facebook Click ID (renew existing, never generate).
        if ( ! empty( $_COOKIE[ self::COOKIE_FBC ] ) ) {
            self::add_cookie_header( $response, self::COOKIE_FBC, sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_FBC ] ) ), $expires, $domain, $secure, $samesite );
        }

        return $response;
    }

    /**
     * Add a Set-Cookie header to REST response.
     *
     * @param \WP_REST_Response $response Response object.
     * @param string            $name     Cookie name.
     * @param string            $value    Cookie value.
     * @param int               $expires  Expiry timestamp.
     * @param string            $domain   Cookie domain.
     * @param bool              $secure   HTTPS only.
     * @param string            $samesite SameSite attribute.
     */
    private static function add_cookie_header( $response, $name, $value, $expires, $domain, $secure, $samesite ) {
        $cookie_parts = array(
            rawurlencode( $name ) . '=' . rawurlencode( $value ),
            'expires=' . gmdate( 'D, d M Y H:i:s T', $expires ),
            'path=/',
            'domain=' . $domain,
        );

        if ( $secure ) {
            $cookie_parts[] = 'Secure';
        }

        // NOTE: No HttpOnly for ANY tracking cookie.
        // - _fbc/_fbp: Meta's fbevents.js SDK reads via document.cookie.
        // - cs_uid: JS creates on first visit (cached pages skip PHP),
        //   and must be readable to avoid regenerating on every page load.
        // cs_uid is a random hex identifier (not a session token) — no
        // security benefit from HttpOnly.
        // @see https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
        $cookie_parts[] = 'SameSite=' . $samesite;

        $response->header( 'Set-Cookie', implode( '; ', $cookie_parts ), false );
    }

    /**
     * Generate a unique user identifier.
     *
     * @return string 32-character hex string.
     */
    private static function generate_uid() {
        return bin2hex( random_bytes( 16 ) );
    }

    /**
     * Get current cookie values for payload building.
     *
     * @return array Associative array of cookie values.
     */
    public static function get_cookie_data() {
        return array(
            'cs_uid' => isset( $_COOKIE[ self::COOKIE_UID ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_UID ] ) ) : '',
            'fbc'    => isset( $_COOKIE[ self::COOKIE_FBC ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_FBC ] ) ) : '',
            'fbp'    => isset( $_COOKIE[ self::COOKIE_FBP ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_FBP ] ) ) : '',
        );
    }


    /**
     * Get external_id for order events and lock to order meta (write-once).
     *
     * Priority: WP User ID > cs_uid Cookie > Billing Phone.
     * Locks at order creation to ensure all future events use the same ID.
     *
     * @param \WC_Order $order WooCommerce order.
     * @return string|null Hashed external ID.
     */
    public static function get_external_id_for_order( $order ) {
        // Check if already locked.
        $locked = $order->get_meta( '_capiflow_external_id', true );
        if ( ! empty( $locked ) ) {
            return $locked;
        }

        // Generate and lock (write-once).
        $external_id = null;

        // Priority 1: Logged-in user (stable + browser match).
        if ( $order->get_customer_id() ) {
            $external_id = hash( 'sha256', 'wp_' . $order->get_customer_id() );
        }
        // Priority 2: Cookie (browser events match).
        elseif ( ! empty( $_COOKIE[ self::COOKIE_UID ] ) ) {
            $external_id = hash( 'sha256', sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_UID ] ) ) );
        }
        // Priority 3: Phone fallback.
        elseif ( $order->get_billing_phone() ) {
            $external_id = hash( 'sha256', Data_Normalizer::normalize_phone_e164( $order->get_billing_phone() ) );
        }

        if ( ! empty( $external_id ) ) {
            // Write-once guard — prevent async race conditions.
            $existing = $order->get_meta( '_capiflow_external_id', true );
            if ( empty( $existing ) ) {
                $order->update_meta_data( '_capiflow_external_id', $external_id );
                $order->save_meta_data();
            } else {
                $external_id = $existing; // Another process won the race.
            }
        }

        return $external_id;
    }
}
