<?php

namespace BWDK\Integrations\Bazaar;

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

/**
 * Bazaar iframe compatibility for merchant storefronts embedded in Digify Bazaar.
 *
 * Bazaar loads the merchant WooCommerce storefront inside a third-party iframe.
 * Browsers block cross-origin iframe embedding unless X-Frame-Options is removed
 * and Content-Security-Policy frame-ancestors explicitly allows trusted Bazaar origins.
 *
 * WooCommerce cart/session cookies must use SameSite=None with Secure=true so they
 * are sent inside the third-party iframe context; otherwise cart state is lost.
 *
 * Deployment note: PHP can only remove or adjust headers set during the WordPress
 * request. Headers injected by Nginx, Apache, or Cloudflare (Transform Rules,
 * security headers, Page Rules) are applied outside PHP and cannot be cleared here.
 * If embedding still fails after activating BWDK, verify that X-Frame-Options or
 * a restrictive CSP is not being added at the web server or CDN layer.
 */
class BazaarIframeService {

    /**
     * Trusted Bazaar origins allowed to embed this storefront in an iframe.
     *
     * @var string[]
     */
    private array $allowed_frame_origins = [
        'https://bazaar.staging.digifyteam.ir',
        'https://bazaar.digify.shop',
    ];

    public function register(): void {
        add_action( 'send_headers', [ $this, 'send_frame_ancestors_headers' ], 999 );

        // woocommerce_set_cookie_options was introduced in WooCommerce 6.7.0; older
        // versions have no filter for SameSite cookie options, so skip registration.
        if (
            class_exists( 'WooCommerce' )
            && defined( 'WC_VERSION' )
            && version_compare( WC_VERSION, '6.7.0', '>=' )
        ) {
            add_filter( 'woocommerce_set_cookie_options', [ $this, 'filter_woocommerce_cookie_options' ], 10, 3 );
        }
    }

    /**
     * Allow trusted Bazaar origins to embed pages via CSP frame-ancestors.
     *
     * X-Frame-Options is removed when possible because it would block iframe
     * embedding even when frame-ancestors permits the parent origin.
     *
     * Existing Content-Security-Policy directives (default-src, script-src, etc.)
     * are preserved; only frame-ancestors is replaced or appended. Report-Only CSP
     * headers are not modified.
     */
    public function send_frame_ancestors_headers(): void {
        if ( headers_sent() ) {
            return;
        }

        header_remove( 'X-Frame-Options' );

        $allowed_origins = apply_filters(
            'bwdk_allowed_frame_origins',
            $this->allowed_frame_origins
        );

        if ( ! is_array( $allowed_origins ) ) {
            $allowed_origins = $this->allowed_frame_origins;
        }

        $directives        = $this->normalize_allowed_frame_origins( $allowed_origins );
        $frame_ancestors   = 'frame-ancestors ' . implode( ' ', $directives );
        $existing_csp      = $this->get_existing_csp_header();
        $merged_csp        = $this->merge_frame_ancestors_into_csp( $existing_csp, $frame_ancestors );

        header( 'Content-Security-Policy: ' . $merged_csp, true );
    }

    /**
     * Set SameSite=None and Secure on WooCommerce cart/session cookies for iframe use.
     *
     * SameSite=None is required so cookies are included in third-party iframe requests.
     * Secure=true is mandatory when SameSite=None; browsers reject the cookie otherwise.
     *
     * @param mixed       $options Cookie options array from WooCommerce.
     * @param string|null $name    Cookie name.
     * @param mixed       $value   Cookie value.
     * @return mixed
     */
    public function filter_woocommerce_cookie_options( $options, $name = '', $value = '' ) {
        if ( ! $this->is_woocommerce_session_cookie( (string) $name ) ) {
            return $options;
        }

        if ( ! is_array( $options ) ) {
            return $options;
        }

        $options['secure']   = true;
        $options['samesite'] = 'None';

        return $options;
    }

    /**
     * Whether the cookie belongs to WooCommerce cart/session state.
     */
    private function is_woocommerce_session_cookie( string $name ): bool {
        if ( $name === 'woocommerce_cart_hash' || $name === 'woocommerce_items_in_cart' ) {
            return true;
        }

        return strpos( $name, 'wp_woocommerce_session_' ) === 0;
    }

    /**
     * Normalize frame-ancestor origins: trim, drop empty, dedupe, single 'self' first.
     *
     * @param array<int, mixed> $allowed_origins Raw origins from defaults or filter.
     * @return string[]
     */
    private function normalize_allowed_frame_origins( array $allowed_origins ): array {
        $normalized = [];

        foreach ( $allowed_origins as $origin ) {
            $origin = trim( (string) $origin );
            if ( $origin === '' || $this->is_self_origin( $origin ) ) {
                continue;
            }

            if ( in_array( $origin, $normalized, true ) ) {
                continue;
            }

            $normalized[] = $origin;
        }

        return array_merge( [ "'self'" ], $normalized );
    }

    /**
     * Whether an origin value represents CSP 'self'.
     */
    private function is_self_origin( string $origin ): bool {
        $lower = strtolower( $origin );

        return $lower === "'self'" || $lower === 'self';
    }

    /**
     * Read the first Content-Security-Policy header queued for this response.
     *
     * Content-Security-Policy-Report-Only is intentionally ignored.
     */
    private function get_existing_csp_header(): ?string {
        if ( ! function_exists( 'headers_list' ) ) {
            return null;
        }

        foreach ( headers_list() as $header ) {
            // Content-Security-Policy-Report-Only is a separate header and must not be modified.
            if ( stripos( $header, 'Content-Security-Policy-Report-Only:' ) === 0 ) {
                continue;
            }

            if ( stripos( $header, 'Content-Security-Policy:' ) !== 0 ) {
                continue;
            }

            $value = trim( substr( $header, strlen( 'Content-Security-Policy:' ) ) );

            return $value !== '' ? $value : null;
        }

        return null;
    }

    /**
     * Merge frame-ancestors into an existing CSP without dropping other directives.
     *
     * Merge logic:
     * 1. Parse the existing CSP into semicolon-separated directives.
     * 2. If frame-ancestors exists, replace that directive only.
     * 3. Otherwise append frame-ancestors at the end.
     * 4. Rejoin all directives (default-src, script-src, style-src, etc.) unchanged.
     *
     * @param string|null $existing_csp           Current CSP value, if any.
     * @param string      $frame_ancestors_directive Full frame-ancestors directive.
     */
    private function merge_frame_ancestors_into_csp( ?string $existing_csp, string $frame_ancestors_directive ): string {
        if ( $existing_csp === null || trim( $existing_csp ) === '' ) {
            return $frame_ancestors_directive . ';';
        }

        $directives = $this->parse_csp_directives( $existing_csp );
        $merged     = [];
        $replaced   = false;

        foreach ( $directives as $directive ) {
            if ( $this->get_csp_directive_name( $directive ) === 'frame-ancestors' ) {
                $merged[] = $frame_ancestors_directive;
                $replaced = true;
                continue;
            }

            $merged[] = $directive;
        }

        if ( ! $replaced ) {
            $merged[] = $frame_ancestors_directive;
        }

        return implode( '; ', $merged );
    }

    /**
     * Split a CSP header value into individual directives.
     *
     * @return string[]
     */
    private function parse_csp_directives( string $csp ): array {
        $directives = [];
        $parts      = explode( ';', $csp );

        foreach ( $parts as $part ) {
            $part = trim( $part );
            if ( $part !== '' ) {
                $directives[] = $part;
            }
        }

        return $directives;
    }

    /**
     * Extract the directive name from a single CSP directive string.
     */
    private function get_csp_directive_name( string $directive ): string {
        if ( preg_match( '/^([a-zA-Z0-9\-]+)/', trim( $directive ), $matches ) ) {
            return strtolower( $matches[1] );
        }

        return '';
    }
}
