<?php
/**
 * REST API for geographic data
 *
 * Endpoints:
 *   GET /wp-json/scpwoo/v1/regions              — List of regions
 *   GET /wp-json/scpwoo/v1/regions/{code}        — Region detail + cities
 *   GET /wp-json/scpwoo/v1/cities                — All cities
 *   GET /wp-json/scpwoo/v1/cities/{name}         — City detail + neighborhoods
 *   GET /wp-json/scpwoo/v1/neighborhoods         — All neighborhoods
 *   GET /wp-json/scpwoo/v1/neighborhoods/{city}  — Neighborhoods of a city
 *   GET /wp-json/scpwoo/v1/search?q=             — Global search
 *
 * @package AfriRegions
 */

defined( 'ABSPATH' ) or die( 'Forbidden' );

class SCPWOO_REST_API {

    /**
     * API namespace
     */
    const API_NAMESPACE = 'scpwoo/v1';

    /**
     * HTTP cache duration for geographic data endpoints (1 hour).
     */
    const CACHE_MAX_AGE = 3600;

    /**
     * Regex validating a city/neighborhood name in routes (Unicode letters, spaces, hyphens, apostrophes).
     */
    const NAME_PATTERN = "(?P<%s>[\\p{L}\\p{N}\\s\\-''.]+)";

    /**
     * Constructor: registers the REST hook
     */
    public function __construct() {
        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
        add_filter( 'rest_post_dispatch', array( $this, 'add_security_headers' ), 10, 3 );
    }

    /**
     * Global rate limit: max requests per minute per identity.
     */
    const RATE_LIMIT = 120;

    /**
     * Rate limit window in seconds.
     */
    const RATE_WINDOW = 60;

    public function check_permissions() {
        // Rate limiting: composite identity (IP + User-Agent hash) to handle proxies
        $ip       = $this->get_client_ip();
        $ua       = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
        $rate_key = 'scpwoo_api_rl_' . md5( $ip . '|' . $ua );
        $requests = (int) get_transient( $rate_key );

        if ( $requests >= self::RATE_LIMIT ) {
            SCPWOO_Audit::log_rate_limit( 'rest_api' );
            return new WP_Error(
                'too_many_requests',
                __( 'Rate limit exceeded. Please wait.', 'afriregions-city-neighborhood-for-woocommerce' ),
                array(
                    'status'  => 429,
                    'headers' => array( 'Retry-After' => self::RATE_WINDOW ),
                )
            );
        }
        set_transient( $rate_key, $requests + 1, self::RATE_WINDOW );

        return true;
    }

    /**
     * Add security headers to all scpwoo REST responses.
     *
     * @param WP_REST_Response $response Response object.
     * @param WP_REST_Server   $server   Server instance.
     * @param WP_REST_Request  $request  Request object.
     * @return WP_REST_Response
     */
    public function add_security_headers( $response, $server, $request ): WP_REST_Response {
        $route = $request->get_route();

        // Only apply to our namespace
        if ( strpos( $route, '/' . self::API_NAMESPACE ) !== 0 ) {
            return $response;
        }

        $response->header( 'X-Content-Type-Options', 'nosniff' );

        // Restrict CORS to the site's own domain
        $response->header( 'Access-Control-Allow-Origin', esc_url_raw( home_url() ) );
        $response->header( 'Access-Control-Allow-Methods', 'GET, OPTIONS' );
        $response->header( 'Access-Control-Allow-Credentials', 'false' );

        // Add Retry-After on 429 responses
        if ( $response->get_status() === 429 ) {
            $response->header( 'Retry-After', (string) self::RATE_WINDOW );
        }

        return $response;
    }

    /**
     * Register all REST routes
     */
    public function register_routes(): void {
        // GET /regions
        register_rest_route( self::API_NAMESPACE, '/regions', array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_regions' ),
            'permission_callback' => array( $this, 'check_permissions' ),
            'args'                => self::pagination_args(),
        ) );

        // GET /regions/{code}
        register_rest_route( self::API_NAMESPACE, '/regions/(?P<code>[A-Z]{2})', array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_region' ),
            'permission_callback' => array( $this, 'check_permissions' ),
            'args'                => array(
                'code' => array(
                    'required'          => true,
                    'validate_callback' => function( $param ) {
                        return preg_match( '/^[A-Z]{2}$/', $param );
                    },
                ),
            ),
        ) );

        // GET /cities
        register_rest_route( self::API_NAMESPACE, '/cities', array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_cities' ),
            'permission_callback' => array( $this, 'check_permissions' ),
            'args'                => array_merge( array(
                'region' => array(
                    'required'          => false,
                    'type'              => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                    'validate_callback' => function( $param ) {
                        return empty( $param ) || preg_match( '/^[A-Z0-9\-]{1,10}$/i', $param );
                    },
                ),
            ), self::pagination_args() ),
        ) );

        // GET /cities/{name}
        register_rest_route( self::API_NAMESPACE, '/cities/' . sprintf( self::NAME_PATTERN, 'name' ), array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_city' ),
            'permission_callback' => array( $this, 'check_permissions' ),
        ) );

        // GET /neighborhoods
        register_rest_route( self::API_NAMESPACE, '/neighborhoods', array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_neighborhoods' ),
            'permission_callback' => array( $this, 'check_permissions' ),
            'args'                => array_merge( array(
                'city' => array(
                    'required'          => false,
                    'type'              => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                ),
            ), self::pagination_args() ),
        ) );

        // GET /neighborhoods/{city}
        register_rest_route( self::API_NAMESPACE, '/neighborhoods/' . sprintf( self::NAME_PATTERN, 'city' ), array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_neighborhoods_by_city' ),
            'permission_callback' => array( $this, 'check_permissions' ),
        ) );

        // GET /search?q=
        register_rest_route( self::API_NAMESPACE, '/search', array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'search' ),
            'permission_callback' => array( $this, 'check_permissions' ),
            'args'                => array(
                'q' => array(
                    'required'          => true,
                    'type'              => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                    'validate_callback' => function( $param ) {
                        return is_string( $param ) && mb_strlen( $param ) <= 200;
                    },
                ),
                'limit' => array(
                    'required'          => false,
                    'type'              => 'integer',
                    'default'           => 20,
                    'sanitize_callback' => 'absint',
                    'validate_callback' => function( $param ) {
                        return is_numeric( $param ) && (int) $param >= 1 && (int) $param <= 50;
                    },
                ),
            ),
        ) );
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    /**
     * Get the client IP address safely.
     *
     * Uses only REMOTE_ADDR (the TCP-level peer address) to prevent
     * rate-limit bypass via spoofed X-Forwarded-For or X-Client-IP headers.
     * Those headers are trivially forged by any HTTP client.
     *
     * @return string
     */
    private function get_client_ip(): string {
        $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
        if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
            return $ip;
        }
        return 'unknown';
    }

    // =========================================================================
    // Data loading
    // =========================================================================

    /**
     * Load regions
     *
     * @return array
     */
    private function load_states(): array {
        return SCPWOO_Geo_Repository::get_states_by_country();
    }

    /**
     * Get allowed country codes (supported + WooCommerce allowed).
     *
     * @return array
     */
    private function get_allowed_country_codes(): array {
        return SCPWOO_Geo_Repository::get_country_codes();
    }

    /**
     * Load cities (merged)
     *
     * @return array
     */
    private function load_places(): array {
        return SCPWOO_Geo_Repository::get_merged_places_by_country();
    }

    /**
     * Load neighborhoods (merged)
     *
     * @return array
     */
    private function load_neighborhoods(): array {
        return SCPWOO_Geo_Repository::get_merged_neighborhoods_by_country();
    }

    /**
     * Wrap data in a REST response with Cache-Control headers.
     *
     * @param mixed $data Response data.
     * @return WP_REST_Response
     */
    private function cached_response( $data ) {
        $response = rest_ensure_response( $data );
        $response->header( 'Cache-Control', 'public, max-age=' . self::CACHE_MAX_AGE );
        return $response;
    }

    /**
     * Paginate an array of results and return response with pagination headers.
     *
     * @param array            $data    Full results array.
     * @param WP_REST_Request  $request Request object (reads page, per_page).
     * @return WP_REST_Response
     */
    private function paginated_response( array $data, $request ) {
        $page     = max( 1, absint( $request->get_param( 'page' ) ) );
        $per_page = min( 100, max( 1, absint( $request->get_param( 'per_page' ) ) ) );
        $total    = count( $data );
        $pages    = max( 1, (int) ceil( $total / $per_page ) );
        $offset   = ( $page - 1 ) * $per_page;

        $slice    = array_slice( $data, $offset, $per_page );

        $response = rest_ensure_response( $slice );
        $response->header( 'Cache-Control', 'public, max-age=' . self::CACHE_MAX_AGE );
        $response->header( 'X-WP-Total', $total );
        $response->header( 'X-WP-TotalPages', $pages );

        return $response;
    }

    /**
     * Standard pagination args for route registration.
     *
     * @return array
     */
    private static function pagination_args(): array {
        return array(
            'page' => array(
                'required'          => false,
                'type'              => 'integer',
                'default'           => 1,
                'sanitize_callback' => 'absint',
                'validate_callback' => function( $param ) {
                    return is_numeric( $param ) && (int) $param >= 1;
                },
            ),
            'per_page' => array(
                'required'          => false,
                'type'              => 'integer',
                'default'           => 50,
                'sanitize_callback' => 'absint',
                'validate_callback' => function( $param ) {
                    return is_numeric( $param ) && (int) $param >= 1 && (int) $param <= 100;
                },
            ),
        );
    }

    // =========================================================================
    // Endpoint callbacks
    // =========================================================================

    /**
     * GET /regions — List of regions
     */
    public function get_regions( $request ) {
        $all_states = $this->load_states();
        $all_places = $this->load_places();

        $data = array();
        foreach ( $all_states as $country_code => $country_states ) {
            $country_places = isset( $all_places[ $country_code ] ) ? $all_places[ $country_code ] : array();
            foreach ( $country_states as $code => $name ) {
                $city_count = is_array( $country_places ) && isset( $country_places[ $code ] ) ? count( $country_places[ $code ] ) : 0;
                $data[] = array(
                    'code'       => $code,
                    'name'       => $name,
                    'country'    => $country_code,
                    'city_count' => $city_count,
                );
            }
        }

        return $this->paginated_response( $data, $request );
    }

    /**
     * GET /regions/{code} — Region detail + its cities
     */
    public function get_region( $request ) {
        $code       = strtoupper( $request['code'] );
        $all_states = $this->load_states();

        $found_country = null;
        $region_name   = null;
        foreach ( $all_states as $country_code => $country_states ) {
            if ( isset( $country_states[ $code ] ) ) {
                $found_country = $country_code;
                $region_name   = $country_states[ $code ];
                break;
            }
        }

        if ( ! $found_country ) {
            return new WP_Error( 'not_found', __( 'Region not found.', 'afriregions-city-neighborhood-for-woocommerce' ), array( 'status' => 404 ) );
        }

        $all_places        = $this->load_places();
        $all_neighborhoods = $this->load_neighborhoods();
        $country_places    = $all_places[ $found_country ] ?? array();
        $country_hoods     = $all_neighborhoods[ $found_country ] ?? array();
        $cities            = is_array( $country_places ) && isset( $country_places[ $code ] ) ? $country_places[ $code ] : array();

        $cities_data = array();
        foreach ( $cities as $city ) {
            $hoods = is_array( $country_hoods ) && isset( $country_hoods[ $city ] ) ? $country_hoods[ $city ] : array();
            $cities_data[] = array(
                'name'               => $city,
                'neighborhood_count' => count( $hoods ),
            );
        }

        return $this->cached_response( array(
            'code'       => $code,
            'name'       => $region_name,
            'country'    => $found_country,
            'city_count' => count( $cities ),
            'cities'     => $cities_data,
        ) );
    }

    /**
     * GET /cities — All cities (filterable by region)
     */
    public function get_cities( $request ) {
        $all_states        = $this->load_states();
        $all_places        = $this->load_places();
        $all_neighborhoods = $this->load_neighborhoods();
        $region_filter     = $request->get_param( 'region' );

        $data = array();
        foreach ( $all_places as $country_code => $country_places ) {
            $country_states = $all_states[ $country_code ] ?? array();
            $country_hoods  = $all_neighborhoods[ $country_code ] ?? array();
            if ( is_array( $country_places ) ) {
                foreach ( $country_places as $region_code => $cities ) {
                    if ( $region_filter && strtoupper( $region_filter ) !== $region_code ) {
                        continue;
                    }
                    $region_name = $country_states[ $region_code ] ?? $region_code;
                    foreach ( $cities as $city ) {
                        $hoods = is_array( $country_hoods ) && isset( $country_hoods[ $city ] ) ? $country_hoods[ $city ] : array();
                        $data[] = array(
                            'name'               => $city,
                            'region_code'        => $region_code,
                            'region_name'        => $region_name,
                            'country'            => $country_code,
                            'neighborhood_count' => count( $hoods ),
                        );
                    }
                }
            }
        }

        return $this->paginated_response( $data, $request );
    }

    /**
     * GET /cities/{name} — City detail + its neighborhoods
     */
    public function get_city( $request ) {
        $city_name         = sanitize_text_field( $request['name'] );
        $all_states        = $this->load_states();
        $all_places        = $this->load_places();
        $all_neighborhoods = $this->load_neighborhoods();

        $found_region  = null;
        $found_country = null;
        foreach ( $all_places as $country_code => $country_places ) {
            if ( is_array( $country_places ) ) {
                foreach ( $country_places as $region_code => $cities ) {
                    if ( in_array( $city_name, $cities, true ) ) {
                        $found_region  = $region_code;
                        $found_country = $country_code;
                        break 2;
                    }
                }
            }
        }

        if ( ! $found_region ) {
            return new WP_Error( 'not_found', __( 'City not found.', 'afriregions-city-neighborhood-for-woocommerce' ), array( 'status' => 404 ) );
        }

        $country_states = $all_states[ $found_country ] ?? array();
        $country_hoods  = $all_neighborhoods[ $found_country ] ?? array();
        $hoods          = is_array( $country_hoods ) && isset( $country_hoods[ $city_name ] ) ? $country_hoods[ $city_name ] : array();

        return $this->cached_response( array(
            'name'          => $city_name,
            'region_code'   => $found_region,
            'region_name'   => $country_states[ $found_region ] ?? $found_region,
            'country'       => $found_country,
            'neighborhoods' => $hoods,
        ) );
    }

    /**
     * GET /neighborhoods — All neighborhoods (filterable by city)
     */
    public function get_neighborhoods( $request ) {
        $all_neighborhoods = $this->load_neighborhoods();
        $city_filter       = $request->get_param( 'city' );

        $data = array();
        foreach ( $all_neighborhoods as $country_code => $country_hoods ) {
            if ( is_array( $country_hoods ) ) {
                foreach ( $country_hoods as $city => $hoods ) {
                    if ( $city_filter && $city !== $city_filter ) {
                        continue;
                    }
                    foreach ( $hoods as $hood ) {
                        $data[] = array(
                            'name'    => $hood,
                            'city'    => $city,
                            'country' => $country_code,
                        );
                    }
                }
            }
        }

        return $this->paginated_response( $data, $request );
    }

    /**
     * GET /neighborhoods/{city} — Neighborhoods of a city
     */
    public function get_neighborhoods_by_city( $request ) {
        $city_name         = sanitize_text_field( urldecode( $request['city'] ) );
        $all_neighborhoods = $this->load_neighborhoods();

        foreach ( $all_neighborhoods as $country_code => $country_hoods ) {
            if ( is_array( $country_hoods ) && isset( $country_hoods[ $city_name ] ) ) {
                return $this->cached_response( array(
                    'city'          => $city_name,
                    'country'       => $country_code,
                    'neighborhoods' => $country_hoods[ $city_name ],
                    'count'         => count( $country_hoods[ $city_name ] ),
                ) );
            }
        }

        return new WP_Error( 'not_found', __( 'No neighborhoods found for this city.', 'afriregions-city-neighborhood-for-woocommerce' ), array( 'status' => 404 ) );
    }

    // =========================================================================
    // Search — split into focused sub-methods
    // =========================================================================

    /**
     * GET /search?q= — Global search (regions, cities, neighborhoods)
     */
    public function search( $request ) {
        $raw_query = $request->get_param( 'q' );
        $limit     = min( absint( $request->get_param( 'limit' ) ), 50 );
        $limit     = max( 1, $limit );

        if ( mb_strlen( $raw_query ) < 2 ) {
            return new WP_Error( 'invalid_query', __( 'The search query must contain at least 2 characters.', 'afriregions-city-neighborhood-for-woocommerce' ), array( 'status' => 400 ) );
        }

        if ( ! SCPWOO_Geo_Repository::needs_migration() ) {
            return $this->search_via_sql( $raw_query, $limit );
        }

        // Legacy fallback: PHP loops over static files
        $query             = mb_strtolower( $raw_query );
        $query_normalized  = $this->normalize( $query );
        $all_states        = $this->load_states();
        $all_places        = $this->load_places();
        $all_neighborhoods = $this->load_neighborhoods();

        $results = $this->search_regions( $all_states, $query_normalized, $limit );

        if ( count( $results ) < $limit ) {
            $results = array_merge( $results, $this->search_cities( $all_states, $all_places, $query_normalized, $limit - count( $results ) ) );
        }

        if ( count( $results ) < $limit ) {
            $results = array_merge( $results, $this->search_neighborhoods( $all_neighborhoods, $query_normalized, $limit - count( $results ) ) );
        }

        return rest_ensure_response( array(
            'query'   => $raw_query,
            'count'   => count( $results ),
            'results' => $results,
        ) );
    }

    /**
     * Fast SQL-backed search (used when the DB migration has run).
     *
     * @param string $raw_query Original query string.
     * @param int    $limit     Max results.
     * @return WP_REST_Response
     */
    private function search_via_sql( string $raw_query, int $limit ): WP_REST_Response {
        global $wpdb;

        $table   = SCPWOO_Geo_Repository::get_table_name();
        $allowed = $this->get_allowed_country_codes();

        if ( empty( $allowed ) ) {
            return rest_ensure_response( array( 'query' => $raw_query, 'count' => 0, 'results' => array() ) );
        }

        $like         = '%' . $wpdb->esc_like( $raw_query ) . '%';
        $placeholders = implode( ', ', array_fill( 0, count( $allowed ), '%s' ) );
        $args         = array_merge( array( $like ), $allowed, array( $limit ) );

        // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders
        $rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT type, name, region_code, parent_name, country_code
             FROM {$table}
             WHERE name LIKE %s AND is_deleted = 0 AND country_code IN ({$placeholders})
             ORDER BY FIELD(type, 'region', 'city', 'neighborhood'), name
             LIMIT %d",
            ...$args
        ), ARRAY_A );

        $states  = SCPWOO_Geo_Repository::get_states_by_country();
        $results = array();

        foreach ( $rows as $row ) {
            if ( $row['type'] === 'region' ) {
                $results[] = array(
                    'type'        => 'region',
                    'name'        => $row['name'],
                    'region_code' => $row['region_code'],
                    'region_name' => $row['name'],
                    'country'     => $row['country_code'],
                );
            } elseif ( $row['type'] === 'city' ) {
                $region_name = isset( $states[ $row['country_code'] ][ $row['region_code'] ] )
                    ? $states[ $row['country_code'] ][ $row['region_code'] ]
                    : $row['region_code'];
                $results[] = array(
                    'type'        => 'city',
                    'name'        => $row['name'],
                    'region_code' => $row['region_code'],
                    'region_name' => $region_name,
                    'country'     => $row['country_code'],
                );
            } else {
                $results[] = array(
                    'type'    => 'neighborhood',
                    'name'    => $row['name'],
                    'city'    => $row['parent_name'],
                    'country' => $row['country_code'],
                );
            }
        }

        return rest_ensure_response( array( 'query' => $raw_query, 'count' => count( $results ), 'results' => $results ) );
    }

    /**
     * Search matching regions.
     *
     * @param array  $all_states       States data keyed by country code.
     * @param string $query_normalized Normalized search term.
     * @param int    $limit            Max results.
     * @return array
     */
    private function search_regions( array $all_states, string $query_normalized, int $limit ): array {
        $results = array();
        foreach ( $all_states as $country_code => $country_states ) {
            foreach ( $country_states as $code => $name ) {
                if ( strpos( $this->normalize( $name ), $query_normalized ) !== false ) {
                    $results[] = array(
                        'type'        => 'region',
                        'name'        => $name,
                        'region_code' => $code,
                        'region_name' => $name,
                        'country'     => $country_code,
                    );
                    if ( count( $results ) >= $limit ) return $results;
                }
            }
        }
        return $results;
    }

    /**
     * Search matching cities.
     *
     * @param array  $all_states       States data keyed by country code.
     * @param array  $all_places       Places data keyed by country code.
     * @param string $query_normalized Normalized search term.
     * @param int    $limit            Max results.
     * @return array
     */
    private function search_cities( array $all_states, array $all_places, string $query_normalized, int $limit ): array {
        $results = array();
        foreach ( $all_places as $country_code => $country_places ) {
            $country_states = $all_states[ $country_code ] ?? array();
            if ( ! is_array( $country_places ) ) continue;

            foreach ( $country_places as $region_code => $cities ) {
                $region_name = $country_states[ $region_code ] ?? $region_code;
                foreach ( $cities as $city ) {
                    if ( strpos( $this->normalize( $city ), $query_normalized ) !== false ) {
                        $results[] = array(
                            'type'        => 'city',
                            'name'        => $city,
                            'region_code' => $region_code,
                            'region_name' => $region_name,
                            'country'     => $country_code,
                        );
                        if ( count( $results ) >= $limit ) return $results;
                    }
                }
            }
        }
        return $results;
    }

    /**
     * Search matching neighborhoods.
     *
     * @param array  $all_neighborhoods Neighborhoods data keyed by country code.
     * @param string $query_normalized  Normalized search term.
     * @param int    $limit             Max results.
     * @return array
     */
    private function search_neighborhoods( array $all_neighborhoods, string $query_normalized, int $limit ): array {
        $results = array();
        foreach ( $all_neighborhoods as $country_code => $country_hoods ) {
            if ( ! is_array( $country_hoods ) ) continue;

            foreach ( $country_hoods as $city => $hoods ) {
                foreach ( $hoods as $hood ) {
                    if ( strpos( $this->normalize( $hood ), $query_normalized ) !== false ) {
                        $results[] = array(
                            'type'    => 'neighborhood',
                            'name'    => $hood,
                            'city'    => $city,
                            'country' => $country_code,
                        );
                        if ( count( $results ) >= $limit ) return $results;
                    }
                }
            }
        }
        return $results;
    }

    /**
     * Normalize a string (lowercase, without accents)
     *
     * @param string $str
     * @return string
     */
    private function normalize( string $str ): string {
        $str = mb_strtolower( $str );
        if ( function_exists( 'normalizer_normalize' ) ) {
            $str = normalizer_normalize( $str, Normalizer::FORM_D );
        }
        return preg_replace( '/[\x{0300}-\x{036f}]/u', '', $str );
    }
}
