<?php
/**
 * TamRank REST API — SEO Metadata Controller (read side)
 *
 * Provides the GET endpoint at /tamrank/v1/meta/{id} for reading core and
 * advanced SEO metadata fields, with authentication, per-post capability
 * checks and a transient-based anti-abuse read ceiling — a fixed per-minute
 * limit, identical for every user and independent of any licence state.
 *
 * Write access (POST/PUT /meta/{id} + POST /meta/batch) is a PRO feature and
 * lives in the tam-rank-pro add-on (TamRank_Pro_REST_Meta, cluster-9 strip).
 * That subclass extends this controller and reuses the shared protected helpers
 * below (rate limiting, permission check, response builder). WordPress merges
 * the PRO EDITABLE registration onto this READABLE route on the same path.
 *
 * @package TamRank
 * @since 7.0.0
 */

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

/**
 * Class TamRank_REST_Meta
 *
 * REST controller for reading SEO metadata. Shared helpers are protected so the
 * PRO write subclass can inherit them.
 */
class TamRank_REST_Meta extends WP_REST_Controller {

    /**
     * Rate limiter instance.
     *
     * @var TamRank_Rate_Limiter
     */
    protected $rate_limiter;

    /**
     * Result from the last rate limit check.
     *
     * @var array|null
     */
    protected $last_rate_result;

    /**
     * Constructor — set namespace and rest_base.
     */
    public function __construct() {
        $this->namespace    = 'tamrank/v1';
        $this->rest_base    = 'meta';
        $this->rate_limiter = class_exists( 'TamRank_Rate_Limiter' ) ? new TamRank_Rate_Limiter() : null;
    }

    /**
     * Register REST routes (read side).
     *
     * Only the READABLE endpoint + schema live in FREE. The PRO add-on registers
     * the EDITABLE method on this same route (WordPress merges them) and the
     * /meta/batch route via TamRank_Pro_REST_Meta.
     */
    public function register_routes() {
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/(?P<id>[\d]+)',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [ $this, 'get_item' ],
                    'permission_callback' => [ $this, 'get_item_permissions_check' ],
                ],
                'schema' => [ $this, 'get_item_schema' ],
            ]
        );
    }

    /**
     * Check permissions for GET requests.
     *
     * @param WP_REST_Request $request Full request data.
     * @return true|WP_Error True if authorized, WP_Error otherwise.
     */
    public function get_item_permissions_check( $request ) {
        return $this->check_permissions( $request );
    }

    /**
     * Shared permission check: authentication + per-post capability.
     *
     * Returns 401 if unauthenticated, 403 if user lacks edit_post capability.
     * Protected so the PRO write subclass reuses the identical check.
     *
     * @param WP_REST_Request $request Full request data.
     * @return true|WP_Error True if authorized, WP_Error otherwise.
     */
    protected function check_permissions( $request ) {
        $post_id = (int) $request['id'];

        if ( ! is_user_logged_in() ) {
            return new WP_Error(
                'rest_forbidden',
                __( 'Authentication required.', 'tam-rank' ),
                [ 'status' => 401 ]
            );
        }

        if ( ! current_user_can( 'edit_post', $post_id ) ) {
            return new WP_Error(
                'rest_forbidden',
                __( 'You do not have access to this post.', 'tam-rank' ),
                [ 'status' => 403 ]
            );
        }

        return true;
    }

    /**
     * Check rate limit for the current user and request type.
     *
     * Applies a fixed per-minute anti-abuse ceiling selected purely by request
     * type ('read'/'write'/'batch'). The ceiling is identical for every user
     * and independent of any licence state — there is no per-tier limit. Calls
     * the rate limiter and returns a 429 WP_REST_Response if the limit is
     * exceeded, or null if the request is allowed. Protected so the PRO write
     * subclass shares the same limiter and ceilings.
     *
     * @param string $type Request type: 'read', 'write' or 'batch'.
     * @return WP_REST_Response|null 429 response if blocked, null if allowed.
     */
    protected function apply_rate_limit( $type ) {
        // Skip rate limiting if rate limiter is not available or incompatible
        if ( ! $this->rate_limiter || ! method_exists( $this->rate_limiter, 'check' ) ) {
            $this->last_rate_result = [ 'allowed' => true, 'limit' => 999, 'remaining' => 999, 'reset_at' => time() + 60 ];
            return null;
        }

        $user_id = get_current_user_id();

        // Per-minute request ceilings — anti-abuse only, identical for every
        // user and independent of any licence state. The free plugin registers
        // only the read endpoint; the write/batch types are exercised by the
        // separate PRO add-on's write subclass, which inherits this method.
        $ceilings = [
            'read'  => 120,
            'write' => 30,
            'batch' => 5,
        ];
        $limit = isset( $ceilings[ $type ] ) ? $ceilings[ $type ] : 30;

        $result = $this->rate_limiter->check( $user_id, $type, $limit );

        if ( ! $result['allowed'] ) {
            $response = new WP_REST_Response(
                [
                    'code'    => 'rate_limit_exceeded',
                    'message' => __( 'Too many requests. Please try again later.', 'tam-rank' ),
                    'data'    => [ 'status' => 429 ],
                ],
                429
            );
            $this->add_rate_headers( $response, $result );
            return $response;
        }

        $this->last_rate_result = $result;
        return null;
    }

    /**
     * Attach rate limit headers to a REST response.
     *
     * Sets X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset
     * on every response. Also sets Retry-After when the limit is exceeded.
     * Protected so the PRO write subclass attaches identical headers.
     *
     * @param WP_REST_Response $response The response object to modify.
     * @param array            $result   Rate limit result from TamRank_Rate_Limiter::check().
     */
    protected function add_rate_headers( $response, $result ) {
        $response->header( 'X-RateLimit-Limit', $result['limit'] );
        $response->header( 'X-RateLimit-Remaining', $result['remaining'] );
        $response->header( 'X-RateLimit-Reset', $result['reset_at'] );

        if ( ! $result['allowed'] ) {
            $response->header( 'Retry-After', max( 1, $result['reset_at'] - time() ) );
        }
    }

    /**
     * Build the response data array for a post (basic + advanced fields).
     *
     * Used by get_item() (read) and, via inheritance, by the PRO write
     * subclass's update_item() response. Protected so both share one shape.
     *
     * @param int     $post_id Post ID.
     * @param WP_Post $post    Post object.
     * @return array Response data array.
     */
    protected function build_response_data( $post_id, $post ) {
        // Basic fields
        $data = [
            'id'               => $post_id,
            'meta_title'       => get_post_meta( $post_id, '_tam_rank_meta_title', true ),
            'meta_description' => get_post_meta( $post_id, '_tam_rank_meta_description', true ),
            'focus_keyword'    => get_post_meta( $post_id, '_tam_rank_focus_keyword', true ),
            'custom_slug'      => $post->post_name,
            'noindex'          => (bool) get_post_meta( $post_id, '_tam_rank_noindex', true ),
            'nofollow'         => (bool) get_post_meta( $post_id, '_tam_rank_nofollow', true ),
        ];

        // Advanced fields — readable by all authenticated users
        $data['social_title']       = get_post_meta( $post_id, '_tam_rank_social_title', true );
        $data['social_description'] = get_post_meta( $post_id, '_tam_rank_social_description', true );
        $data['social_image']       = get_post_meta( $post_id, '_tam_rank_social_image', true );
        $data['canonical_url']      = get_post_meta( $post_id, '_tam_rank_canonical', true );
        if ( class_exists( 'TAMRANK_Schema_Storage' ) ) {
            $data['schema_auto_type']   = get_post_meta( $post_id, TAMRANK_Schema_Storage::META_AUTO_TYPE, true );
            $data['schema_auto_extras'] = get_post_meta( $post_id, TAMRANK_Schema_Storage::META_AUTO_EXTRAS, true ) ?: [];
        } else {
            $data['schema_auto_type']   = get_post_meta( $post_id, '_tamrank_schema_auto_type', true );
            $data['schema_auto_extras'] = get_post_meta( $post_id, '_tamrank_schema_auto_extras', true ) ?: [];
        }

        // Secondary keywords via TamRank_Keywords helper
        if ( class_exists( 'TamRank_Keywords' ) ) {
            $kw_data                        = TamRank_Keywords::get_keywords( $post_id );
            $data['secondary_keywords']     = isset( $kw_data['secondary'] ) ? $kw_data['secondary'] : [];
        } else {
            $data['secondary_keywords'] = [];
        }

        $data['secondary_keywords_enabled'] = (bool) get_post_meta( $post_id, '_tamrank_secondary_keywords_enabled', true );

        return $data;
    }

    /**
     * Get SEO metadata for a single post.
     *
     * Returns all 14 fields: 6 basic + 8 advanced (social, canonical, schema,
     * secondary keywords). Advanced fields are readable by all authenticated users.
     *
     * @param WP_REST_Request $request Full request data.
     * @return WP_REST_Response|WP_Error Response with SEO metadata or error.
     */
    public function get_item( $request ) {
        $post_id = (int) $request['id'];

        $blocked = $this->apply_rate_limit( 'read' );
        if ( null !== $blocked ) {
            return $blocked;
        }

        $post = get_post( $post_id );

        if ( ! $post ) {
            return new WP_Error(
                'rest_not_found',
                __( 'Post not found.', 'tam-rank' ),
                [ 'status' => 404 ]
            );
        }

        $data     = $this->build_response_data( $post_id, $post );
        $response = rest_ensure_response( $data );
        $this->add_rate_headers( $response, $this->last_rate_result );
        return $response;
    }

    /**
     * Get the JSON Schema for a SEO metadata item.
     *
     * Covers all 14 fields: 6 basic fields writable by all users,
     * 8 advanced fields readable by all but writable only by PRO users.
     * Shared with the PRO write subclass (it builds EDITABLE args from this).
     *
     * @return array JSON Schema definition.
     */
    public function get_item_schema() {
        if ( $this->schema ) {
            return $this->add_additional_fields_schema( $this->schema );
        }

        $schema = [
            '$schema'    => 'http://json-schema.org/draft-04/schema#',
            'title'      => 'tamrank-meta',
            'type'       => 'object',
            'properties' => [

                // --- Basic fields ---

                'id'               => [
                    'description' => __( 'Unique identifier of the post.', 'tam-rank' ),
                    'type'        => 'integer',
                    'context'     => [ 'view', 'edit' ],
                    'readonly'    => true,
                ],
                'meta_title'       => [
                    'description' => __( 'SEO title of the post (max 120 characters).', 'tam-rank' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                    'arg_options' => [
                        'sanitize_callback' => 'sanitize_text_field',
                        'validate_callback' => static function ( $value ) {
                            return mb_strlen( $value ) <= 120;
                        },
                    ],
                ],
                'meta_description' => [
                    'description' => __( 'SEO description of the post (max 320 characters).', 'tam-rank' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                    'arg_options' => [
                        'sanitize_callback' => 'sanitize_textarea_field',
                        'validate_callback' => static function ( $value ) {
                            return mb_strlen( $value ) <= 320;
                        },
                    ],
                ],
                'focus_keyword'    => [
                    'description' => __( 'Primary keyword of the post (max 80 characters).', 'tam-rank' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                    'arg_options' => [
                        'sanitize_callback' => 'sanitize_text_field',
                        'validate_callback' => static function ( $value ) {
                            return mb_strlen( $value ) <= 80;
                        },
                    ],
                ],
                'custom_slug'      => [
                    'description' => __( 'Custom URL slug of the post.', 'tam-rank' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                    'arg_options' => [
                        'sanitize_callback' => 'sanitize_title',
                    ],
                ],
                'noindex'          => [
                    'description' => __( 'Whether search engines should index this post.', 'tam-rank' ),
                    'type'        => 'boolean',
                    'context'     => [ 'view', 'edit' ],
                ],
                'nofollow'         => [
                    'description' => __( 'Whether search engines should follow links in this post.', 'tam-rank' ),
                    'type'        => 'boolean',
                    'context'     => [ 'view', 'edit' ],
                ],

                // --- Advanced fields (PRO-gated writes) ---

                'social_title'       => [
                    'description' => __( 'Social/OG title of the post (max 60 characters).', 'tam-rank' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                    'arg_options' => [
                        'sanitize_callback' => 'sanitize_text_field',
                        'validate_callback' => static function ( $value ) {
                            return mb_strlen( $value ) <= 60;
                        },
                    ],
                ],
                'social_description' => [
                    'description' => __( 'Social/OG description of the post (max 200 characters).', 'tam-rank' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                    'arg_options' => [
                        'sanitize_callback' => 'sanitize_textarea_field',
                        'validate_callback' => static function ( $value ) {
                            return mb_strlen( $value ) <= 200;
                        },
                    ],
                ],
                'social_image'       => [
                    'description' => __( 'Social/OG image URL of the post.', 'tam-rank' ),
                    'type'        => 'string',
                    'format'      => 'uri',
                    'context'     => [ 'view', 'edit' ],
                    'arg_options' => [
                        'sanitize_callback' => 'sanitize_url',
                    ],
                ],
                'canonical_url'      => [
                    'description' => __( 'Canonical URL of the post.', 'tam-rank' ),
                    'type'        => 'string',
                    'format'      => 'uri',
                    'context'     => [ 'view', 'edit' ],
                    'arg_options' => [
                        'sanitize_callback' => 'sanitize_url',
                    ],
                ],
                'schema_auto_type'   => [
                    'description' => __( 'Automatically detected schema type.', 'tam-rank' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                    'arg_options' => [
                        'sanitize_callback' => 'sanitize_text_field',
                    ],
                ],
                'schema_auto_extras' => [
                    'description' => __( 'Automatically detected additional schema types.', 'tam-rank' ),
                    'type'        => 'array',
                    'items'       => [
                        'type' => 'string',
                    ],
                    'context'     => [ 'view', 'edit' ],
                ],
                'secondary_keywords' => [
                    'description' => __( 'Secondary keywords of the post.', 'tam-rank' ),
                    'type'        => 'array',
                    'items'       => [
                        'type' => 'string',
                    ],
                    'context'     => [ 'view', 'edit' ],
                ],
                'secondary_keywords_enabled' => [
                    'description' => __( 'Whether secondary keywords are active.', 'tam-rank' ),
                    'type'        => 'boolean',
                    'context'     => [ 'view' ],
                    'readonly'    => true,
                ],

                // --- Write-result metadata (only populated when this endpoint performs a write) ---

                'pro_fields_skipped' => [
                    'description' => __( 'Names of fields this endpoint did not write.', 'tam-rank' ),
                    'type'        => 'array',
                    'items'       => [
                        'type' => 'string',
                    ],
                    'context'     => [ 'edit' ],
                    'readonly'    => true,
                ],
            ],
        ];

        $this->schema = $schema;

        return $this->add_additional_fields_schema( $this->schema );
    }
}
