<?php

declare(strict_types=1);

namespace SiteFort\Modules\Cloudflare;

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

use SiteFort\Includes\SecretManager;
use SiteFort\Includes\SettingsStore;

/**
 * Cloudflare API client.
 *
 * Handles token/zone/account scoped requests, managed list/rule lifecycle,
 * and access-rule fallback operations.
 */
class Api {

    public const BASE_URL = 'https://api.cloudflare.com/client/v4/';
    public const AUTH_MODE_API_TOKEN = 'api_token';
    public const AUTH_MODE_GLOBAL_KEY = 'global_key';

    /**
     * SiteFort managed rule identifier in Cloudflare rulesets.
     */
    public const MANAGED_RULE_REF = 'sitefort_managed_edge_rule';

    /**
     * Prefix used for managed access-rule notes.
     */
    public const MANAGED_ACCESS_NOTE_PREFIX = 'SiteFort Managed';

    private string $api_token;
    private string $global_api_key;
    private string $auth_mode;
    private string $auth_email;
    private string $zone_id;
    private string $account_id;

    public function __construct(
        ?string $api_token = null,
        ?string $zone_id = null,
        ?string $account_id = null,
        ?string $auth_mode = null,
        ?string $auth_email = null,
        ?string $global_api_key = null
    ) {
        $raw_auth_mode = (string) ( $auth_mode ?? '' );
        $this->api_token  = self::normalize_api_token( (string) ( $api_token ?? '' ) );
        $this->global_api_key = self::normalize_global_api_key( (string) ( $global_api_key ?? '' ) );
        $this->auth_mode = $raw_auth_mode === '' ? '' : self::sanitize_auth_mode( $raw_auth_mode );
        $this->auth_email = sanitize_email( (string) ( $auth_email ?? '' ) );
        $this->zone_id    = $zone_id ?? '';
        $this->account_id = $account_id ?? '';

        if ( $this->auth_mode === '' || $this->zone_id === '' || $this->account_id === '' || ! $this->has_local_credentials() ) {
            $settings = SettingsStore::get( SettingsStore::COL_CLOUDFLARE );

            if ( $this->auth_mode === '' ) {
                $this->auth_mode = self::sanitize_auth_mode( (string) ( $settings['auth_mode'] ?? self::AUTH_MODE_API_TOKEN ) );
            }

            if ( $this->auth_mode === self::AUTH_MODE_GLOBAL_KEY && $this->auth_email === '' ) {
                $this->auth_email = sanitize_email( (string) ( $settings['auth_email'] ?? '' ) );
            }

            if ( $this->auth_mode === self::AUTH_MODE_API_TOKEN && $this->api_token === '' ) {
                $token = SecretManager::get( 'cloudflare_api_token' );
                $this->api_token = is_string( $token ) ? self::normalize_api_token( $token ) : '';
            }

            if ( $this->auth_mode === self::AUTH_MODE_GLOBAL_KEY && $this->global_api_key === '' ) {
                $global_key = SecretManager::get( 'cloudflare_global_api_key' );
                $this->global_api_key = is_string( $global_key ) ? self::normalize_global_api_key( $global_key ) : '';
            }

            if ( $this->zone_id === '' ) {
                $this->zone_id = sanitize_text_field( (string) ( $settings['zone_id'] ?? '' ) );
            }

            if ( $this->account_id === '' ) {
                $this->account_id = sanitize_text_field( (string) ( $settings['account_id'] ?? '' ) );
            }
        }

        if ( $this->auth_mode === '' ) {
            $this->auth_mode = self::AUTH_MODE_API_TOKEN;
        }
    }

    // ---------------------------------------------------------------------
    // Basic state
    // ---------------------------------------------------------------------

    public function is_configured(): bool {
        if ( $this->zone_id === '' ) {
            return false;
        }

        if ( $this->auth_mode === self::AUTH_MODE_GLOBAL_KEY ) {
            return $this->global_api_key !== '' && $this->auth_email !== '';
        }

        return $this->api_token !== '';
    }

    public function has_account_id(): bool {
        return $this->account_id !== '';
    }

    public function get_zone_id(): string {
        return $this->zone_id;
    }

    public function get_account_id(): string {
        return $this->account_id;
    }

    public function set_account_id( string $account_id ): void {
        $this->account_id = sanitize_text_field( $account_id );
    }

    public function get_auth_mode(): string {
        return $this->auth_mode;
    }

    // ---------------------------------------------------------------------
    // Core requests
    // ---------------------------------------------------------------------

    /**
     * @param string               $endpoint Relative endpoint.
     * @param 'GET'|'POST'|'PUT'|'PATCH'|'DELETE' $method
     * @param array<string,mixed>  $body
     * @param array<string,mixed>  $query
     */
    private function request( string $endpoint, string $method = 'GET', array $body = [], array $query = [] ) {
        if ( ! $this->is_configured() ) {
            return new \WP_Error( 'cloudflare_missing_config', 'Cloudflare credentials or zone ID are missing.' );
        }

        $headers = [
            'Content-Type'  => 'application/json',
        ];

        if ( $this->auth_mode === self::AUTH_MODE_GLOBAL_KEY ) {
            if ( $this->auth_email === '' || ! is_email( $this->auth_email ) ) {
                return new \WP_Error(
                    'cloudflare_auth_email_invalid',
                    'Cloudflare auth email is invalid for Global API Key mode.'
                );
            }

            if ( ! self::is_valid_global_api_key( $this->global_api_key ) ) {
                return new \WP_Error(
                    'cloudflare_global_key_invalid_format',
                    'Cloudflare Global API Key format is invalid. Paste raw key value only.'
                );
            }

            $headers['X-Auth-Key'] = $this->global_api_key;
            $headers['X-Auth-Email'] = $this->auth_email;
        } else {
            if ( ! self::is_valid_bearer_token( $this->api_token ) ) {
                return new \WP_Error(
                    'cloudflare_invalid_token_format',
                    'Cloudflare API token format is invalid. Paste the API Token value only (no "Authorization:", "Bearer", spaces, or unsupported symbols).'
                );
            }

            if ( self::looks_like_cloudflare_token_identifier( $this->api_token ) ) {
                return new \WP_Error(
                    'cloudflare_token_likely_id',
                    'Cloudflare credential looks like a Global API Key or Token ID. This field accepts API Token value only.'
                );
            }

            $headers['Authorization'] = 'Bearer ' . $this->api_token;
        }

        $url = self::BASE_URL . ltrim( $endpoint, '/' );
        if ( ! empty( $query ) ) {
            $url = add_query_arg( $query, $url );
        }

        $args = [
            'method'  => $method,
            'headers' => $headers,
            'timeout' => 20,
        ];

        if ( ! empty( $body ) || in_array( $method, [ 'POST', 'PUT', 'PATCH' ], true ) ) {
            $args['body'] = wp_json_encode( $body );
        }

        $response = wp_remote_request( $url, $args );

        if ( is_wp_error( $response ) ) {
            return new \WP_Error(
                'cloudflare_request_failed',
                'Cloudflare request failed: ' . $response->get_error_message(),
                [ 'endpoint' => $endpoint ]
            );
        }

        $code = (int) wp_remote_retrieve_response_code( $response );
        $raw  = (string) wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );

        if ( ! is_array( $data ) ) {
            return new \WP_Error(
                'cloudflare_invalid_response',
                'Cloudflare returned an invalid response payload.',
                [ 'code' => $code, 'endpoint' => $endpoint ]
            );
        }

        if ( $code >= 400 || ( isset( $data['success'] ) && ! $data['success'] ) ) {
            $message = 'Cloudflare API request failed.';
            if ( ! empty( $data['errors'] ) && is_array( $data['errors'] ) ) {
                $first = $data['errors'][0] ?? [];
                if ( is_array( $first ) && ! empty( $first['message'] ) ) {
                    $message = (string) $first['message'];
                }
            }

            if (
                stripos( $message, 'invalid request header' ) !== false
                || stripos( $message, 'invalid request headers' ) !== false
                || stripos( $message, 'invalid format for authorization header' ) !== false
            ) {
                if ( $this->auth_mode === self::AUTH_MODE_GLOBAL_KEY ) {
                    $message = 'Cloudflare rejected Global API Key authentication. Verify Cloudflare auth email and Global API Key.';
                } elseif ( self::looks_like_cloudflare_token_identifier( $this->api_token ) ) {
                    $message = 'Cloudflare rejected the authorization header. The saved credential looks like a Global API Key or Token ID. Use API Token value only.';
                } else {
                    $message = 'Cloudflare rejected the authorization header. Use the API Token value (not Global API Key / Token ID), and do not include "Authorization:", "Bearer", or extra spaces.';
                }
            }

            if (
                $code === 401
                || $code === 403
                || stripos( $message, 'unauthorized to access requested resource' ) !== false
                || stripos( $message, 'not authorized to access requested resource' ) !== false
                || trim( strtolower( $message ) ) === 'authentication error'
            ) {
                $message = $this->map_permission_error_message( $endpoint );
            }

            $resource = $this->get_permission_error_resource_context( $endpoint );

            return new \WP_Error(
                'cloudflare_api_error',
                $message,
                [
                    'code'     => $code,
                    'endpoint' => $endpoint,
                    'errors'   => $data['errors'] ?? [],
                    'resource' => $resource,
                ]
            );
        }

        return $data;
    }

    private function map_permission_error_message( string $endpoint ): string {
        $endpoint = ltrim( $endpoint, '/' );
        $zone_id = $this->extract_zone_id_from_endpoint( $endpoint );

        if ( $endpoint === 'user/tokens/verify' ) {
            return 'Cloudflare rejected this API token. Confirm you pasted the token value, not the Token ID, and verify that the token is still active.';
        }

        if ( $endpoint === 'user' ) {
            return 'Cloudflare rejected Global API Key authentication. Verify Cloudflare auth email and Global API Key.';
        }

        if ( $zone_id !== '' && preg_match( '#^zones/[^/]+$#', $endpoint ) ) {
            return $this->build_zone_permission_error_message( $zone_id, 'Zone - Zone - Read' );
        }

        if ( preg_match( '#^accounts/[^/]+/rules/lists(?:/.*)?$#', $endpoint ) ) {
            return 'Cloudflare accepted the token, but it cannot access Cloudflare Lists for the owning account. Add Account - Filter Lists - Edit and confirm Account Resources includes the account that owns this zone.';
        }

        if (
            preg_match( '#^zones/[^/]+/rulesets/phases/http_request_firewall_custom/entrypoint$#', $endpoint )
            || preg_match( '#^zones/[^/]+/rulesets/[^/]+/rules(?:/.*)?$#', $endpoint )
        ) {
            if ( $zone_id !== '' ) {
                return $this->build_zone_permission_error_message( $zone_id, 'Zone - WAF - Edit' );
            }

            return 'Cloudflare accepted the token, but it cannot manage edge WAF rules for the configured site zone. Add Zone - WAF - Edit and confirm Zone Resources includes this zone.';
        }

        if (
            preg_match( '#^accounts/[^/]+/firewall/access_rules/rules(?:/.*)?$#', $endpoint )
            || preg_match( '#^zones/[^/]+/firewall/access_rules/rules(?:/.*)?$#', $endpoint )
        ) {
            return 'Cloudflare accepted the token, but fallback Access Rules are unavailable for the owning account. Add Account - Firewall Access Rules - Edit if you want fallback support, and confirm Account Resources includes the account that owns this zone.';
        }

        return 'Cloudflare accepted the credential, but it does not have permission to access this resource. Review the token scopes, zone access, and account access, then verify again.';
    }

    private function build_zone_permission_error_message( string $zone_id, string $required_permission ): string {
        $zone_id = $this->sanitize_resource_id_for_message( $zone_id );

        if ( $this->auth_mode === self::AUTH_MODE_GLOBAL_KEY ) {
            return sprintf(
                'Cloudflare key cannot access Zone ID %1$s. Use a key from the account that owns this site.',
                $zone_id
            );
        }

        return sprintf(
            'API token cannot access Zone ID %1$s. Add this site to token Zone Resources and enable %2$s.',
            $zone_id,
            $required_permission
        );
    }

    /**
     * @return array{type:string,id:string}|null
     */
    private function get_permission_error_resource_context( string $endpoint ): ?array {
        $endpoint = ltrim( $endpoint, '/' );
        $zone_id = $this->extract_zone_id_from_endpoint( $endpoint );

        if ( $zone_id !== '' ) {
            return [
                'type' => 'zone',
                'id'   => $this->sanitize_resource_id_for_message( $zone_id ),
            ];
        }

        if ( preg_match( '#^accounts/([^/]+)(?:/|$)#', $endpoint, $matches ) ) {
            return [
                'type' => 'account',
                'id'   => $this->sanitize_resource_id_for_message( rawurldecode( $matches[1] ) ),
            ];
        }

        return null;
    }

    private function extract_zone_id_from_endpoint( string $endpoint ): string {
        if ( ! preg_match( '#(?:^|/)zones/([^/]+)(?:/|$)#', $endpoint, $matches ) ) {
            return '';
        }

        return $this->sanitize_resource_id_for_message( rawurldecode( $matches[1] ) );
    }

    private function sanitize_resource_id_for_message( string $value ): string {
        return sanitize_text_field( preg_replace( '/[^A-Za-z0-9_-]/', '', $value ) ?? '' );
    }

    // ---------------------------------------------------------------------
    // Account and zone
    // ---------------------------------------------------------------------

    /**
     * Verify credential validity and status based on auth mode.
     *
     * @return array<string,mixed>|\WP_Error
     */
    public function verify_credentials() {
        if ( $this->auth_mode === self::AUTH_MODE_GLOBAL_KEY ) {
            return $this->request( 'user', 'GET' );
        }

        return $this->request( 'user/tokens/verify', 'GET' );
    }

    private static function normalize_api_token( string $token ): string {
        $token = trim( $token );
        if ( $token === '' ) {
            return '';
        }

        // Handle pasted header forms like "Authorization: Bearer <token>".
        $token = preg_replace( '/^authorization\\s*:\\s*/i', '', $token ) ?? $token;
        $token = preg_replace( '/^bearer\\s+/i', '', $token ) ?? $token;

        // Remove wrapping quotes/backticks from clipboard copies.
        $token = trim( $token, "\"'`" );

        $token = preg_replace( '/[\\x00-\\x1F\\x7F]/u', '', $token ) ?? $token;
        $token = preg_replace( '/\\s+/u', '', $token ) ?? $token;

        return trim( $token );
    }

    private static function is_valid_bearer_token( string $token ): bool {
        return (bool) preg_match( '/^[A-Za-z0-9\\-._~+\\/]+=*$/', $token );
    }

    private static function normalize_global_api_key( string $value ): string {
        $value = trim( $value );
        if ( $value === '' ) {
            return '';
        }

        $value = preg_replace( '/^x-auth-key\\s*:\\s*/i', '', $value ) ?? $value;
        $value = trim( $value, "\"'`" );
        $value = preg_replace( '/[\\x00-\\x1F\\x7F]/u', '', $value ) ?? $value;
        $value = preg_replace( '/\\s+/u', '', $value ) ?? $value;

        return trim( $value );
    }

    private static function is_valid_global_api_key( string $value ): bool {
        return (bool) preg_match( '/^[A-Fa-f0-9]{20,64}$/', $value );
    }

    private static function sanitize_auth_mode( string $mode ): string {
        $mode = sanitize_key( $mode );
        if ( ! in_array( $mode, [ self::AUTH_MODE_API_TOKEN, self::AUTH_MODE_GLOBAL_KEY ], true ) ) {
            return self::AUTH_MODE_API_TOKEN;
        }

        return $mode;
    }

    private function has_local_credentials(): bool {
        if ( $this->auth_mode === self::AUTH_MODE_GLOBAL_KEY ) {
            return $this->global_api_key !== '' && $this->auth_email !== '';
        }

        return $this->api_token !== '';
    }

    private static function looks_like_cloudflare_token_identifier( string $token ): bool {
        if ( $token === '' ) {
            return false;
        }

        return strlen( $token ) < 40 && ctype_xdigit( $token );
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function get_zone_details() {
        if ( $this->zone_id === '' ) {
            return new \WP_Error( 'cloudflare_missing_zone', 'Cloudflare Zone ID is missing.' );
        }

        return $this->request( 'zones/' . rawurlencode( $this->zone_id ), 'GET' );
    }

    /**
     * Resolve and cache account_id from zone details.
     *
     * @return string|\WP_Error
     */
    public function discover_account_id() {
        $zone = $this->get_zone_details();
        if ( is_wp_error( $zone ) ) {
            return $zone;
        }

        $account_id = sanitize_text_field( (string) ( $zone['result']['account']['id'] ?? '' ) );
        if ( $account_id === '' ) {
            return new \WP_Error( 'cloudflare_missing_account', 'Cloudflare account ID could not be resolved for this zone.' );
        }

        $this->account_id = $account_id;
        return $account_id;
    }

    // ---------------------------------------------------------------------
    // Lists API
    // ---------------------------------------------------------------------

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function list_lists( int $page = 1, int $per_page = 100 ) {
        if ( ! $this->has_account_id() ) {
            return new \WP_Error( 'cloudflare_missing_account', 'Cloudflare account ID is required for list operations.' );
        }

        return $this->request(
            'accounts/' . rawurlencode( $this->account_id ) . '/rules/lists',
            'GET',
            [],
            [
                'page'     => max( 1, $page ),
                'per_page' => max( 5, min( 100, $per_page ) ),
            ]
        );
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function ensure_list( string $name, string $kind = 'ip', string $description = '' ) {
        if ( ! $this->has_account_id() ) {
            return new \WP_Error( 'cloudflare_missing_account', 'Cloudflare account ID is required for list operations.' );
        }

        $safe_name = $this->sanitize_list_name( $name );
        if ( $safe_name === '' ) {
            return new \WP_Error( 'cloudflare_invalid_list_name', 'Cloudflare list name is invalid.' );
        }

        // Lookup existing list first.
        $page = 1;
        do {
            $lists = $this->list_lists( $page, 100 );
            if ( is_wp_error( $lists ) ) {
                return $lists;
            }

            $result = is_array( $lists['result'] ?? null ) ? $lists['result'] : [];
            foreach ( $result as $item ) {
                $list_name = sanitize_text_field( (string) ( $item['name'] ?? '' ) );
                if ( $list_name === $safe_name ) {
                    return [
                        'id'   => sanitize_text_field( (string) ( $item['id'] ?? '' ) ),
                        'name' => $list_name,
                    ];
                }
            }

            $total_pages = (int) ( $lists['result_info']['total_pages'] ?? 1 );
            $page++;
        } while ( $page <= max( 1, $total_pages ) );

        // Create list when not found.
        $create = $this->request(
            'accounts/' . rawurlencode( $this->account_id ) . '/rules/lists',
            'POST',
            [
                'name'        => $safe_name,
                'kind'        => $kind,
                'description' => sanitize_text_field( $description ),
            ]
        );

        if ( is_wp_error( $create ) ) {
            return $create;
        }

        return [
            'id'   => sanitize_text_field( (string) ( $create['result']['id'] ?? '' ) ),
            'name' => sanitize_text_field( (string) ( $create['result']['name'] ?? $safe_name ) ),
        ];
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function get_list_items( string $list_id, int $page = 1, int $per_page = 100 ) {
        if ( ! $this->has_account_id() ) {
            return new \WP_Error( 'cloudflare_missing_account', 'Cloudflare account ID is required for list operations.' );
        }

        return $this->request(
            'accounts/' . rawurlencode( $this->account_id ) . '/rules/lists/' . rawurlencode( $list_id ) . '/items',
            'GET',
            [],
            [
                'page'     => max( 1, $page ),
                'per_page' => max( 5, min( 100, $per_page ) ),
            ]
        );
    }

    /**
     * Replace all list items.
     *
     * Returns operation metadata when Cloudflare performs async bulk writes.
     *
     * @param string[] $entries
     * @return array<string,mixed>|\WP_Error
     */
    public function replace_ip_list_items( string $list_id, array $entries ) {
        if ( ! $this->has_account_id() ) {
            return new \WP_Error( 'cloudflare_missing_account', 'Cloudflare account ID is required for list operations.' );
        }

        $payload = [];
        foreach ( array_values( array_unique( $entries ) ) as $entry ) {
            $entry = trim( (string) $entry );
            if ( $entry === '' ) {
                continue;
            }

            if ( filter_var( $entry, FILTER_VALIDATE_IP ) === false && ! $this->is_cidr( $entry ) ) {
                continue;
            }

            $payload[] = [
                'ip'      => $entry,
                'comment' => 'Managed by SiteFort',
            ];
        }

        $response = $this->request(
            'accounts/' . rawurlencode( $this->account_id ) . '/rules/lists/' . rawurlencode( $list_id ) . '/items',
            'PUT',
            $payload
        );

        if ( is_wp_error( $response ) ) {
            return $response;
        }

        return [
            'success'      => true,
            'queued'       => ! empty( $response['result']['operation_id'] ),
            'operation_id' => sanitize_text_field( (string) ( $response['result']['operation_id'] ?? '' ) ),
        ];
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function get_list_bulk_operation( string $operation_id ) {
        if ( ! $this->has_account_id() ) {
            return new \WP_Error( 'cloudflare_missing_account', 'Cloudflare account ID is required for list operations.' );
        }

        return $this->request(
            'accounts/' . rawurlencode( $this->account_id ) . '/rules/lists/bulk_operations/' . rawurlencode( $operation_id ),
            'GET'
        );
    }

    // ---------------------------------------------------------------------
    // Rulesets API
    // ---------------------------------------------------------------------

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function get_custom_firewall_entrypoint() {
        if ( $this->zone_id === '' ) {
            return new \WP_Error( 'cloudflare_missing_zone', 'Cloudflare Zone ID is required for ruleset operations.' );
        }

        return $this->request(
            'zones/' . rawurlencode( $this->zone_id ) . '/rulesets/phases/http_request_firewall_custom/entrypoint',
            'GET'
        );
    }

    /**
     * Create or update the SiteFort managed custom firewall rule.
     *
     * @return array<string,mixed>|\WP_Error
     */
    public function ensure_managed_custom_rule( string $expression, string $action, bool $enabled = true ) {
        $entrypoint = $this->get_custom_firewall_entrypoint();
        if ( is_wp_error( $entrypoint ) ) {
            return $entrypoint;
        }

        $ruleset_id = sanitize_text_field( (string) ( $entrypoint['result']['id'] ?? '' ) );
        if ( $ruleset_id === '' ) {
            return new \WP_Error( 'cloudflare_missing_ruleset', 'Cloudflare custom firewall entrypoint ruleset is unavailable.' );
        }

        $existing_rule_id = '';
        $rules = is_array( $entrypoint['result']['rules'] ?? null ) ? $entrypoint['result']['rules'] : [];
        foreach ( $rules as $rule ) {
            $ref = sanitize_text_field( (string) ( $rule['ref'] ?? '' ) );
            if ( $ref === self::MANAGED_RULE_REF ) {
                $existing_rule_id = sanitize_text_field( (string) ( $rule['id'] ?? '' ) );
                break;
            }
        }

        $payload = [
            'action'      => $this->sanitize_rule_action( $action ),
            'expression'  => $expression,
            'description' => 'SiteFort managed edge firewall policy',
            'enabled'     => $enabled,
            'ref'         => self::MANAGED_RULE_REF,
        ];

        if ( $existing_rule_id !== '' ) {
            $update = $this->request(
                'zones/' . rawurlencode( $this->zone_id )
                . '/rulesets/' . rawurlencode( $ruleset_id )
                . '/rules/' . rawurlencode( $existing_rule_id ),
                'PATCH',
                $payload
            );

            if ( is_wp_error( $update ) ) {
                return $update;
            }

            return [
                'ruleset_id' => $ruleset_id,
                'rule_id'    => $existing_rule_id,
                'updated'    => true,
            ];
        }

        $create = $this->request(
            'zones/' . rawurlencode( $this->zone_id ) . '/rulesets/' . rawurlencode( $ruleset_id ) . '/rules',
            'POST',
            $payload
        );

        if ( is_wp_error( $create ) ) {
            return $create;
        }

        return [
            'ruleset_id' => $ruleset_id,
            'rule_id'    => sanitize_text_field( (string) ( $create['result']['id'] ?? '' ) ),
            'updated'    => false,
        ];
    }

    /**
     * Delete managed custom rule if present.
     *
     * @return array<string,mixed>|\WP_Error
     */
    public function delete_managed_custom_rule() {
        $entrypoint = $this->get_custom_firewall_entrypoint();
        if ( is_wp_error( $entrypoint ) ) {
            return $entrypoint;
        }

        $ruleset_id = sanitize_text_field( (string) ( $entrypoint['result']['id'] ?? '' ) );
        if ( $ruleset_id === '' ) {
            return [ 'deleted' => false ];
        }

        $rules = is_array( $entrypoint['result']['rules'] ?? null ) ? $entrypoint['result']['rules'] : [];
        $rule_id = '';
        foreach ( $rules as $rule ) {
            $ref = sanitize_text_field( (string) ( $rule['ref'] ?? '' ) );
            if ( $ref === self::MANAGED_RULE_REF ) {
                $rule_id = sanitize_text_field( (string) ( $rule['id'] ?? '' ) );
                break;
            }
        }

        if ( $rule_id === '' ) {
            return [ 'deleted' => false ];
        }

        $delete = $this->request(
            'zones/' . rawurlencode( $this->zone_id )
            . '/rulesets/' . rawurlencode( $ruleset_id )
            . '/rules/' . rawurlencode( $rule_id ),
            'DELETE'
        );

        if ( is_wp_error( $delete ) ) {
            return $delete;
        }

        return [
            'deleted'    => true,
            'ruleset_id' => $ruleset_id,
            'rule_id'    => $rule_id,
        ];
    }

    // ---------------------------------------------------------------------
    // IP Access Rules fallback
    // ---------------------------------------------------------------------

    private function get_access_rules_base_path(): string {
        if ( $this->has_account_id() ) {
            return 'accounts/' . rawurlencode( $this->account_id ) . '/firewall/access_rules/rules';
        }

        return 'zones/' . rawurlencode( $this->zone_id ) . '/firewall/access_rules/rules';
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function get_access_rules( array $params = [] ) {
        return $this->request(
            $this->get_access_rules_base_path(),
            'GET',
            [],
            $params
        );
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function create_access_rule( string $value, string $mode = 'block', string $note = '' ) {
        $value = trim( $value );

        $target = $this->is_cidr( $value ) ? 'ip_range' : 'ip';

        $payload = [
            'mode'          => $mode,
            'configuration' => [
                'target' => $target,
                'value'  => $value,
            ],
            'notes'         => sanitize_text_field( $note ),
        ];

        return $this->request(
            $this->get_access_rules_base_path(),
            'POST',
            $payload
        );
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function delete_access_rule( string $rule_id ) {
        return $this->request(
            $this->get_access_rules_base_path() . '/' . rawurlencode( $rule_id ),
            'DELETE'
        );
    }

    /**
     * Sync a managed set of access rules, used when list API is unavailable.
     *
     * @param string[] $block_entries
     * @param string[] $allow_entries
     * @return array<string,mixed>|\WP_Error
     */
    public function sync_managed_access_rules( array $block_entries, array $allow_entries, string $owner_tag = '' ) {
        $existing_owned = [];
        $existing_external = [];
        $conflicting_targets = [];
        $owner_tag = $this->sanitize_owner_tag( $owner_tag );

        $page = 1;
        do {
            $response = $this->get_access_rules([
                'page'     => $page,
                'per_page' => 100,
            ]);

            if ( is_wp_error( $response ) ) {
                return $response;
            }

            $results = is_array( $response['result'] ?? null ) ? $response['result'] : [];
            foreach ( $results as $rule ) {
                $notes = (string) ( $rule['notes'] ?? '' );
                $mode  = sanitize_text_field( (string) ( $rule['mode'] ?? '' ) );
                $value = sanitize_text_field( (string) ( $rule['configuration']['value'] ?? '' ) );
                $id    = sanitize_text_field( (string) ( $rule['id'] ?? '' ) );
                if ( $mode === '' || $value === '' || $id === '' ) {
                    continue;
                }

                $key = $mode . ':' . $value;
                if ( $this->is_owned_managed_access_rule( $notes, $owner_tag ) ) {
                    if ( ! isset( $existing_owned[ $key ] ) ) {
                        $existing_owned[ $key ] = [];
                    }
                    $existing_owned[ $key ][] = $id;
                    continue;
                }

                $existing_external[ $key ] = true;
            }

            $total_pages = (int) ( $response['result_info']['total_pages'] ?? 1 );
            $page++;
        } while ( $page <= max( 1, $total_pages ) );

        $desired = [];
        $keep_existing = [];

        foreach ( array_unique( $allow_entries ) as $entry ) {
            $entry = trim( $entry );
            if ( $entry === '' ) {
                continue;
            }
            $key = 'whitelist:' . $entry;
            $desired[ $key ] = [
                'value' => $entry,
                'mode'  => 'whitelist',
                'note'  => $this->build_managed_access_note( $owner_tag, 'allow' ),
            ];

            if ( ! empty( $existing_owned[ $key ] ) ) {
                $keep_existing[ $key ] = array_shift( $existing_owned[ $key ] );
                continue;
            }

            if ( ! empty( $existing_external[ $key ] ) ) {
                $keep_existing[ $key ] = 'external';
                continue;
            }

            if ( ! empty( $existing_external[ 'block:' . $entry ] ) || ! empty( $existing_owned[ 'block:' . $entry ] ) ) {
                $conflicting_targets[] = $entry;
                $keep_existing[ $key ] = 'conflict';
            }
        }

        foreach ( array_unique( $block_entries ) as $entry ) {
            $entry = trim( $entry );
            if ( $entry === '' ) {
                continue;
            }
            $key = 'block:' . $entry;
            $desired[ $key ] = [
                'value' => $entry,
                'mode'  => 'block',
                'note'  => $this->build_managed_access_note( $owner_tag, 'block' ),
            ];

            if ( ! empty( $existing_owned[ $key ] ) ) {
                $keep_existing[ $key ] = array_shift( $existing_owned[ $key ] );
                continue;
            }

            if ( ! empty( $existing_external[ $key ] ) ) {
                $keep_existing[ $key ] = 'external';
                continue;
            }

            if ( ! empty( $existing_external[ 'whitelist:' . $entry ] ) || ! empty( $existing_owned[ 'whitelist:' . $entry ] ) ) {
                $conflicting_targets[] = $entry;
                $keep_existing[ $key ] = 'conflict';
            }
        }

        foreach ( $existing_owned as $rule_ids ) {
            foreach ( $rule_ids as $rule_id ) {
                $delete = $this->delete_access_rule( $rule_id );
                if ( is_wp_error( $delete ) ) {
                    return $delete;
                }
            }
        }

        foreach ( $desired as $key => $rule ) {
            if ( isset( $keep_existing[ $key ] ) ) {
                continue;
            }

            $create = $this->create_access_rule(
                (string) $rule['value'],
                (string) $rule['mode'],
                (string) $rule['note']
            );
            if ( is_wp_error( $create ) ) {
                $error_data = $create->get_error_data();
                $error_code = (string) $create->get_error_code();
                $error_texts = [ strtolower( $create->get_error_message() ) ];

                if ( is_array( $error_data ) ) {
                    $error_texts[] = strtolower( (string) ( $error_data['error'] ?? '' ) );

                    if ( ! empty( $error_data['errors'] ) && is_array( $error_data['errors'] ) ) {
                        foreach ( $error_data['errors'] as $api_error ) {
                            if ( ! is_array( $api_error ) ) {
                                continue;
                            }

                            $error_texts[] = strtolower( (string) ( $api_error['message'] ?? '' ) );
                            $error_texts[] = strtolower( (string) ( $api_error['code'] ?? '' ) );
                        }
                    }
                }

                if ( in_array( $error_code, [ 'cloudflare_request_failed', 'cloudflare_api_error' ], true ) ) {
                    foreach ( $error_texts as $error_text ) {
                        if ( $error_text !== '' && str_contains( $error_text, 'duplicate_of_existing' ) ) {
                            $keep_existing[ $key ] = 'external';
                            continue 2;
                        }
                    }
                }

                return $create;
            }
        }

        $conflicting_targets = array_values( array_unique( array_map( 'sanitize_text_field', $conflicting_targets ) ) );

        return [
            'success'           => true,
            'mode'              => 'access_rules_fallback',
            'managed'           => max( 0, count( $desired ) - count( $conflicting_targets ) ),
            'warning_message'   => $this->build_conflicting_access_rule_message( $conflicting_targets ),
            'warning_targets'   => $conflicting_targets,
        ];
    }

    // ---------------------------------------------------------------------
    // Helpers
    // ---------------------------------------------------------------------

    private function sanitize_rule_action( string $action ): string {
        unset( $action );
        return 'block';
    }

    private function sanitize_list_name( string $name ): string {
        $name = strtolower( $name );
        $name = preg_replace( '/[^a-z0-9_]/', '_', $name ) ?: '';
        $name = trim( $name, '_' );
        if ( $name === '' ) {
            return '';
        }

        // Cloudflare list names have a practical limit; keep it bounded.
        return substr( $name, 0, 50 );
    }

    private function is_cidr( string $value ): bool {
        if ( strpos( $value, '/' ) === false ) {
            return false;
        }

        [ $subnet, $bits ] = explode( '/', $value, 2 );
        if ( filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
            return ctype_digit( $bits ) && (int) $bits >= 0 && (int) $bits <= 32;
        }

        if ( filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
            return ctype_digit( $bits ) && (int) $bits >= 0 && (int) $bits <= 128;
        }

        return false;
    }

    /**
     * @param string[] $targets
     */
    private function build_conflicting_access_rule_message( array $targets ): string {
        $targets = array_values(
            array_filter(
                array_map( 'sanitize_text_field', $targets ),
                static fn( string $target ): bool => $target !== ''
            )
        );

        if ( $targets === [] ) {
            return '';
        }

        if ( count( $targets ) === 1 ) {
            return sprintf(
                'An existing Cloudflare rule for %s uses the opposite action. Remove or update the conflicting Cloudflare rule, then retry.',
                $targets[0]
            );
        }

        return sprintf(
            'Existing Cloudflare rules with the opposite action were found for %d IPs or networks. Remove or update the conflicting Cloudflare rules, then retry.',
            count( $targets )
        );
    }

    private function sanitize_owner_tag( string $owner_tag ): string {
        $owner_tag = strtolower( sanitize_key( $owner_tag ) );
        return substr( $owner_tag, 0, 24 );
    }

    private function build_managed_access_note( string $owner_tag, string $mode ): string {
        $mode = $mode === 'allow' ? 'allow' : 'block';
        if ( $owner_tag === '' ) {
            return self::MANAGED_ACCESS_NOTE_PREFIX . ' ' . $mode;
        }

        return sprintf( '%s [%s] %s', self::MANAGED_ACCESS_NOTE_PREFIX, $owner_tag, $mode );
    }

    private function is_owned_managed_access_rule( string $notes, string $owner_tag ): bool {
        if ( $owner_tag === '' ) {
            return false;
        }

        return str_starts_with( $notes, self::MANAGED_ACCESS_NOTE_PREFIX . ' [' . $owner_tag . '] ' );
    }
}
