<?php

/**
 * REST API surface consumed by the admin JavaScript.
 *
 * @package BulkAltText
 */

declare(strict_types=1);

namespace BulkAltText\Rest;

use BulkAltText\Ai\AiService;
use BulkAltText\Licensing\LicenseManager;
use BulkAltText\Media\AltWriter;
use WP_Error;
use WP_Query;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

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

class RestController
{
    private const NS = 'bulk-alt-text-ai/v1';

    private AiService $ai;
    private LicenseManager $license;

    public function __construct()
    {
        $this->ai      = new AiService();
        $this->license = LicenseManager::instance();
    }

    public function register(): void
    {
        add_action('rest_api_init', [$this, 'register_routes']);
    }

    public function register_routes(): void
    {
        register_rest_route(self::NS, '/alt-text', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [$this, 'single'],
            'permission_callback' => [$this, 'can_upload'],
            'args'                => [
                'attachment_id' => ['type' => 'integer', 'required' => true],
                'apply'         => ['type' => 'boolean', 'required' => false, 'default' => true],
            ],
        ]);

        register_rest_route(self::NS, '/bulk', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [$this, 'bulk'],
            'permission_callback' => [$this, 'can_upload'],
        ]);

        register_rest_route(self::NS, '/test-connection', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [$this, 'test_connection'],
            'permission_callback' => [$this, 'can_upload'],
        ]);

        register_rest_route(self::NS, '/images', [
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => [$this, 'images'],
            'permission_callback' => [$this, 'can_upload'],
            'args'                => [
                'filter'   => ['type' => 'string', 'required' => false],
                'page'     => ['type' => 'integer', 'required' => false],
                'per_page' => ['type' => 'integer', 'required' => false],
            ],
        ]);

        register_rest_route(self::NS, '/manual', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [$this, 'manual'],
            'permission_callback' => [$this, 'can_upload'],
            'args'                => [
                'text' => ['type' => 'string', 'required' => true],
                'ids'  => ['type' => 'array', 'required' => true],
            ],
        ]);

        register_rest_route(self::NS, '/revert', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [$this, 'revert'],
            'permission_callback' => [$this, 'can_upload'],
        ]);

        register_rest_route(self::NS, '/stats', [
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => [$this, 'stats'],
            'permission_callback' => [$this, 'can_upload'],
        ]);
    }

    /* --------------------------- permissions --------------------------- */

    public function can_upload(): bool
    {
        return current_user_can('upload_files');
    }

    /* ----------------------------- routes ------------------------------ */

    /**
     * Generate (and optionally apply) alt text for one attachment.
     *
     * @return WP_REST_Response
     */
    public function single(WP_REST_Request $request): WP_REST_Response
    {
        $gate = $this->license->can_generate();
        if (is_wp_error($gate)) {
            return $this->error_response($gate, 402);
        }

        $id = (int) $request->get_param('attachment_id');
        if (!$id || get_post_type($id) !== 'attachment') {
            return $this->error_response(
                new WP_Error('batai_bad_attachment', __('Attachment not found.', 'bulk-alt-text-ai')),
                404
            );
        }

        $settings = $this->settings();
        $text = $this->ai->alt_text(AltWriter::context($id), $settings);
        if (is_wp_error($text)) {
            return $this->error_response($text, 500);
        }

        $applied = false;
        if ($request->get_param('apply')) {
            AltWriter::apply($id, $text);
            $applied = true;
        }

        return new WP_REST_Response([
            'attachment_id' => $id,
            'text'          => $text,
            'applied'       => $applied,
        ]);
    }

    /**
     * Send a tiny "hi" prompt to the configured AI provider so the user can
     * verify the connection works before running anything for real.
     *
     * Does not count against the free quota — it is a connectivity check.
     *
     * @return WP_REST_Response
     */
    public function test_connection(): WP_REST_Response
    {
        $gate = $this->license->can_generate();
        if (is_wp_error($gate)) {
            return $this->error_response($gate, 402);
        }

        if (!AiService::is_available()) {
            return $this->error_response(
                new WP_Error(
                    'batai_no_client',
                    __('No AI connector is configured. Set up an AI provider under Settings → AI in WordPress first.', 'bulk-alt-text-ai')
                ),
                503
            );
        }

        $start = microtime(true);
        $text  = $this->ai->generate(
            'This is a connectivity test from a WordPress plugin. Reply with one short friendly greeting sentence.'
        );
        $elapsed = (int) round(( microtime(true) - $start ) * 1000);

        if (is_wp_error($text)) {
            return $this->error_response($text, 500);
        }

        return new WP_REST_Response([
            'ok'         => true,
            'response'   => $text,
            'elapsed_ms' => $elapsed,
        ]);
    }

    /**
     * Paginated accessibility audit of image attachments.
     *
     * @return WP_REST_Response
     */
    public function images(WP_REST_Request $request): WP_REST_Response
    {
        $filter  = sanitize_key((string) $request->get_param('filter'));
        $filter  = in_array($filter, ['missing', 'all', 'generated'], true) ? $filter : 'missing';
        $page    = max(1, (int) $request->get_param('page'));
        $perPage = min(50, max(1, (int) $request->get_param('per_page') ?: 20));

        $args = [
            'post_type'      => 'attachment',
            'post_status'    => 'inherit',
            'post_mime_type' => 'image',
            'posts_per_page' => $perPage,
            'paged'          => $page,
            'fields'         => 'ids',
            'orderby'        => 'date',
            'order'          => 'ASC',
        ];

        if ($filter === 'missing') {
            $args['meta_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
                'relation' => 'OR',
                ['key' => AltWriter::ALT_META, 'compare' => 'NOT EXISTS'],
                ['key' => AltWriter::ALT_META, 'value' => '', 'compare' => '='],
            ];
        } elseif ($filter === 'generated') {
            $args['meta_key'] = AltWriter::FLAG_META; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
        }

        $query = new WP_Query($args);

        $items = [];
        foreach ($query->posts as $id) {
            $id   = (int) $id;
            $file = get_attached_file($id);
            $alt  = (string) get_post_meta($id, AltWriter::ALT_META, true);

            $items[] = [
                'id'        => $id,
                'thumb'     => (string) ( wp_get_attachment_image_url($id, 'thumbnail') ?: '' ),
                'filename'  => $file ? basename($file) : (string) get_the_title($id),
                'alt'       => $alt,
                'generated' => get_post_meta($id, AltWriter::FLAG_META, true) !== '',
            ];
        }

        return new WP_REST_Response([
            'items' => $items,
            'total' => (int) $query->found_posts,
            'pages' => (int) $query->max_num_pages,
            'page'  => $page,
        ]);
    }

    /**
     * Manual bulk: apply a user-supplied text to the selected images WITHOUT
     * calling the AI. Lets people verify the whole write/undo mechanics before
     * connecting a provider. Free, no quota.
     *
     * @return WP_REST_Response
     */
    public function manual(WP_REST_Request $request): WP_REST_Response
    {
        $text = sanitize_text_field((string) $request->get_param('text'));
        $ids  = $request->get_param('ids');

        if ($text === '') {
            return $this->error_response(
                new WP_Error('batai_no_text', __('Type the text you want to apply first.', 'bulk-alt-text-ai')),
                400
            );
        }
        if (!is_array($ids) || $ids === []) {
            return $this->error_response(
                new WP_Error('batai_no_ids', __('Select at least one image first.', 'bulk-alt-text-ai')),
                400
            );
        }

        $applied = [];
        foreach ($ids as $id) {
            $id = (int) $id;
            if (get_post_type($id) !== 'attachment') {
                continue;
            }
            if (strpos((string) get_post_mime_type($id), 'image') !== 0) {
                continue;
            }
            AltWriter::apply($id, $text);
            $applied[] = $id;
        }

        return new WP_REST_Response([
            'applied' => count($applied),
            'ids'     => $applied,
            'text'    => $text,
        ]);
    }

    /**
     * Bulk-generate alt text.
     *
     * Two modes:
     *  - "ids": generate for an explicit selection. Available on the free plan,
     *    each image gated against the monthly quota.
     *  - scope/date filters over the whole library: Pro only.
     *
     * @return WP_REST_Response
     */
    public function bulk(WP_REST_Request $request): WP_REST_Response
    {
        $ids = $request->get_param('ids');
        if (is_array($ids) && $ids !== []) {
            return $this->bulk_selected($ids);
        }

        if (!$this->license->is_pro()) {
            return $this->error_response(
                new WP_Error(
                    'batai_pro_only',
                    __('Bulk processing is a Pro feature. Upgrade to process your whole media library in one click.', 'bulk-alt-text-ai'),
                    ['upgrade_url' => $this->license->upgrade_url()]
                ),
                402
            );
        }

        $scope    = $request->get_param('scope') === 'all' ? 'all' : 'missing';
        $dateFrom = sanitize_text_field((string) $request->get_param('date_from'));
        $dateTo   = sanitize_text_field((string) $request->get_param('date_to'));
        $batch    = min(20, max(1, (int) $request->get_param('batch') ?: 5));
        $settings = $this->settings();

        $query = $this->build_query($scope, $dateFrom, $dateTo, $batch);

        $done = [];
        foreach ($query->posts as $id) {
            $id   = (int) $id;
            $text = $this->ai->alt_text(AltWriter::context($id), $settings);
            if (is_wp_error($text)) {
                continue;
            }
            AltWriter::apply($id, $text);
            $done[] = ['id' => $id, 'text' => $text];
        }

        // Recount remaining with the same filter after applying this batch.
        $remaining = $this->build_query($scope, $dateFrom, $dateTo, 1)->found_posts;

        return new WP_REST_Response([
            'processed'       => $done,
            'remaining_total' => (int) $remaining,
        ]);
    }

    /**
     * Generate alt text for an explicit list of attachment ids.
     *
     * Free-plan friendly: every image is gated against the monthly quota and
     * processing stops (with a clear error) once the quota runs out.
     *
     * @param array<int|string> $ids
     */
    private function bulk_selected(array $ids): WP_REST_Response
    {
        $gate = $this->license->can_generate();
        if (is_wp_error($gate)) {
            return $this->error_response($gate, 402);
        }

        $ids      = array_slice(array_map('intval', $ids), 0, 20);
        $settings = $this->settings();
        $done     = [];
        $failed   = 0;

        foreach ($ids as $id) {
            if (get_post_type($id) !== 'attachment' || strpos((string) get_post_mime_type($id), 'image') !== 0) {
                continue;
            }

            $text = $this->ai->alt_text(AltWriter::context($id), $settings);
            if (is_wp_error($text)) {
                $failed++;
                continue;
            }

            AltWriter::apply($id, $text);
            $done[] = ['id' => $id, 'text' => $text];
        }

        return new WP_REST_Response([
            'processed'       => $done,
            'failed'          => $failed,
            'remaining_total' => 0,
        ]);
    }

    /**
     * Restore original alt text for images we generated (batched).
     *
     * @return WP_REST_Response
     */
    public function revert(WP_REST_Request $request): WP_REST_Response
    {
        $batch = min(50, max(1, (int) $request->get_param('batch') ?: 25));

        $query = new WP_Query([
            'post_type'      => 'attachment',
            'post_status'    => 'inherit',
            'post_mime_type' => 'image',
            'posts_per_page' => $batch,
            'fields'         => 'ids',
            'meta_key'       => AltWriter::FLAG_META, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
        ]);

        $reverted = 0;
        foreach ($query->posts as $id) {
            if (AltWriter::revert((int) $id)) {
                $reverted++;
            }
        }

        return new WP_REST_Response([
            'reverted'        => $reverted,
            'remaining_total' => max(0, (int) $query->found_posts - $reverted),
        ]);
    }

    /**
     * @return WP_REST_Response
     */
    public function stats(): WP_REST_Response
    {
        return new WP_REST_Response($this->collect_stats());
    }

    /* ----------------------------- helpers ----------------------------- */

    /**
     * @return WP_Query Query for image attachments matching the scope/date filter.
     */
    private function build_query(string $scope, string $dateFrom, string $dateTo, int $perPage): WP_Query
    {
        $args = [
            'post_type'      => 'attachment',
            'post_status'    => 'inherit',
            'post_mime_type' => 'image',
            'posts_per_page' => $perPage,
            'fields'         => 'ids',
            'orderby'        => 'date',
            'order'          => 'ASC',
        ];

        if ($scope === 'all') {
            // Regenerate everything we have not already generated (so runs terminate).
            $args['meta_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
                ['key' => AltWriter::FLAG_META, 'compare' => 'NOT EXISTS'],
            ];
        } else {
            // Only images missing alt text.
            $args['meta_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
                'relation' => 'OR',
                ['key' => AltWriter::ALT_META, 'compare' => 'NOT EXISTS'],
                ['key' => AltWriter::ALT_META, 'value' => '', 'compare' => '='],
            ];
        }

        $dateQuery = [];
        if ($dateFrom !== '') {
            $dateQuery['after'] = $dateFrom;
        }
        if ($dateTo !== '') {
            $dateQuery['before'] = $dateTo;
        }
        if (!empty($dateQuery)) {
            $dateQuery['inclusive'] = true;
            $args['date_query']     = [$dateQuery];
        }

        return new WP_Query($args);
    }

    /**
     * @return array{total:int,missing:int,generated:int,pro:bool}
     */
    private function collect_stats(): array
    {
        $total = new WP_Query([
            'post_type'      => 'attachment',
            'post_status'    => 'inherit',
            'post_mime_type' => 'image',
            'posts_per_page' => 1,
            'fields'         => 'ids',
        ]);

        return [
            'total'     => (int) $total->found_posts,
            'missing'   => (int) $this->build_query('missing', '', '', 1)->found_posts,
            'generated' => AltWriter::generated_count(),
            'pro'       => $this->license->is_pro(),
        ];
    }

    /**
     * @return array{language:string,max_length:int}
     */
    private function settings(): array
    {
        $s = \BulkAltText\Media\MediaIntegration::settings();

        return ['language' => $s['language'], 'max_length' => $s['max_length']];
    }

    private function error_response(WP_Error $error, int $status): WP_REST_Response
    {
        $data = $error->get_error_data();

        return new WP_REST_Response([
            'error'       => $error->get_error_message(),
            'code'        => $error->get_error_code(),
            'upgrade_url' => is_array($data) && isset($data['upgrade_url']) ? $data['upgrade_url'] : null,
        ], $status);
    }
}
