<?php

namespace AtlasAiDev\Awfah\Service;

use WordPress\AI_Client\AI_Client;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;

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

class AI_Provider_Manager {

    /**
     * Determine whether this plugin still needs WordPress 7.0 "Connectors" approval before it
     * can use the configured AI providers.
     *
     * On WordPress 7.0 the AI plugin requires each consumer plugin to be approved per connector
     * before it may call a provider; until that approval is granted, AI features silently fail.
     * Pre-7.0 (bundled SDK) has no such gate, so this always returns not-needed there.
     *
     * @return array{needed: bool, providers: string[], url: string}
     */
    public static function connector_approval_status(): array {
        $status = [
            'needed'    => false,
            'providers' => [],
            'url'       => admin_url( 'options-connectors.php' ),
        ];

        if (
            ! class_exists( '\WordPress\AI\Connector_Approval\Approvals_Store' )
            || ! defined( 'AWFAH_PLUGIN_FILE' )
        ) {
            return $status;
        }

        $store    = new \WordPress\AI\Connector_Approval\Approvals_Store();
        $basename = plugin_basename( AWFAH_PLUGIN_FILE );

        // The AI plugin records a "pending" entry each time this plugin is denied access to a
        // connector. We use that queue rather than AiClient::isConfigured(), because the latter
        // is itself approval-gated for the calling plugin (it returns false until approved),
        // which would hide the very state this hint needs to surface.
        $pending = get_option( \WordPress\AI\Connector_Approval\Approvals_Store::OPTION_PENDING, [] );

        $needs = [];
        foreach ( (array) $pending as $entry ) {
            if ( ! is_array( $entry ) ) {
                continue;
            }
            if ( ( $entry['caller_basename'] ?? '' ) !== $basename ) {
                continue;
            }
            $connector_id = (string) ( $entry['connector_id'] ?? '' );
            if ( '' !== $connector_id && ! $store->is_approved( $basename, $connector_id ) ) {
                $needs[ $connector_id ] = true;
            }
        }

        $status['providers'] = array_keys( $needs );
        $status['needed']    = ! empty( $status['providers'] );

        return $status;
    }

    /**
     * Simple helper: generate text from prompt with optional JSON schema, temperature, etc.
     *
     * @param string $prompt
     * @param array  $args
     *
     * @return string
     */
    public static function generate_text( string $prompt, array $args = [] ): string {
        $result = self::generate_text_with_usage( $prompt, $args );
        return $result['text'];
    }

    /**
     * Generate text and return both text and token usage info.
     *
     * @param string $prompt
     * @param array  $args   Optional: temperature, json_schema, tool_slug, post_id.
     *
     * @return array{text: string, usage: array{prompt_tokens: int, completion_tokens: int, total_tokens: int}}
     */
    public static function generate_text_with_usage( string $prompt, array $args = [] ): array {
        return self::run_text_generation( $prompt, $args );
    }

    /**
     * Generate text from a prompt plus an image (multimodal/vision) and return text + usage.
     *
     * The image is read from disk and attached to the prompt as an inline base64 data URI
     * part, so the model receives the actual image bytes rather than a textual reference.
     *
     * @param string $prompt     The text prompt.
     * @param string $image_path Absolute path to a local image file.
     * @param string $mime_type  Image MIME type (e.g. 'image/jpeg').
     * @param array  $args       Optional: temperature, json_schema, tool_slug, post_id, provider, model.
     *
     * @return array{text: string, usage: array{prompt_tokens: int, completion_tokens: int, total_tokens: int}}
     */
    public static function generate_text_with_image_usage( string $prompt, string $image_path, string $mime_type, array $args = [] ): array {
        $empty = [
            'text'  => '',
            'usage' => [ 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 ],
        ];

        if ( '' === $mime_type || ! file_exists( $image_path ) || ! is_readable( $image_path ) ) {
            Logger::log( 'AI vision generation failed: image file unreadable or MIME type missing.', [ 'path' => $image_path ] );
            return $empty;
        }

        $contents = file_get_contents( $image_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
        if ( false === $contents ) {
            Logger::log( 'AI vision generation failed: could not read image file.', [ 'path' => $image_path ] );
            return $empty;
        }

        // Same data-URI pattern the official AI plugin uses for its alt text ability.
        $data_uri = 'data:' . $mime_type . ';base64,' . base64_encode( $contents );

        return self::run_text_generation( $prompt, $args, $data_uri, $mime_type );
    }

    /**
     * Shared text-generation runner for text-only and multimodal (image) prompts.
     *
     * @param string $prompt         The text prompt.
     * @param array  $args           Optional: temperature, json_schema, tool_slug, post_id, provider, model.
     * @param string $file_data_uri  Optional inline file (base64 data URI) to attach as a multimodal part.
     * @param string $file_mime_type MIME type of the attached file (required when $file_data_uri is set).
     *
     * @return array{text: string, usage: array{prompt_tokens: int, completion_tokens: int, total_tokens: int}}
     */
    private static function run_text_generation( string $prompt, array $args = [], string $file_data_uri = '', string $file_mime_type = '' ): array {
        $empty = [
            'text'  => '',
            'usage' => [ 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 ],
        ];

        // WordPress 7.0+ provides the AI Client in core (snake_case builder, WP_Error on
        // failure). On WordPress < 7.0 the bundled SDK facade is used (camelCase, throws).
        $is_core = function_exists( 'wp_ai_client_prompt' );

        // Build the prompt builder. $skip_temperature lets us retry without the temperature
        // parameter, which some providers/models (e.g. certain OpenAI reasoning models) reject.
        $build_builder = static function ( bool $skip_temperature ) use ( $prompt, $args, $is_core, $file_data_uri, $file_mime_type ) {
            if ( $is_core ) {
                $builder = wp_ai_client_prompt( $prompt );
            } elseif ( class_exists( AI_Client::class ) ) {
                $builder = AI_Client::prompt( $prompt );
            } else {
                return null;
            }

            // Attach the inline image so vision-capable models receive the actual bytes.
            if ( '' !== $file_data_uri ) {
                $builder = $is_core
                    ? $builder->with_file( $file_data_uri, $file_mime_type )
                    : $builder->withFile( $file_data_uri, $file_mime_type );
            }

            if ( ! $skip_temperature && isset( $args['temperature'] ) ) {
                $temperature = (float) $args['temperature'];
                $builder     = $is_core
                    ? $builder->using_temperature( $temperature )
                    : $builder->usingTemperature( $temperature );
            }

            $builder = self::apply_preference( $builder, $args, $is_core );

            if ( isset( $args['json_schema'] ) && is_array( $args['json_schema'] ) ) {
                $builder = $is_core
                    ? $builder->as_json_response( $args['json_schema'] )
                    : $builder->asJsonResponse( $args['json_schema'] );
            }

            return $builder;
        };

        // First pass uses temperature (if provided); a second pass drops it on a temperature error.
        $passes = isset( $args['temperature'] ) ? [ false, true ] : [ false ];

        foreach ( $passes as $skip_temperature ) {
            $builder = $build_builder( $skip_temperature );
            if ( null === $builder ) {
                return $empty;
            }

            $supported = $is_core
                ? $builder->is_supported_for_text_generation()
                : ( ! method_exists( $builder, 'isSupportedForTextGeneration' ) || $builder->isSupportedForTextGeneration() );

            if ( ! $supported ) {
                Logger::log( 'Prompt not supported for text generation. No provider/model available.' );
                return $empty;
            }

            try {
                // generate_text_result()/generateTextResult() return a GenerativeAiResult (token
                // usage included) or, on core, a WP_Error.
                $result = $is_core ? $builder->generate_text_result() : $builder->generateTextResult();

                if ( is_wp_error( $result ) ) {
                    $error = $result->get_error_message();
                    if ( ! $skip_temperature && self::is_temperature_error( $error ) ) {
                        continue; // Retry once without temperature.
                    }
                    Logger::log( 'AI text generation failed', [ 'error' => $error ] );
                    return $empty;
                }

                $text  = method_exists( $result, 'toText' ) ? (string) $result->toText() : '';
                $usage = self::extract_token_usage( $result );

                // Persist usage to DB if context provided.
                if ( ! empty( $args['tool_slug'] ) ) {
                    self::record_usage( $args['tool_slug'], $usage, $args['post_id'] ?? 0 );
                }

                return [ 'text' => $text, 'usage' => $usage ];
            } catch ( \Throwable $e ) {
                if ( ! $skip_temperature && self::is_temperature_error( $e->getMessage() ) ) {
                    continue; // Retry once without temperature.
                }
                Logger::log(
                    'AI text generation failed',
                    [
                        'error' => $e->getMessage(),
                        'type'  => get_class( $e ),
                    ]
                );
                return $empty;
            }
        }

        return $empty;
    }

    /**
     * Whether an AI error message indicates the model rejected the `temperature` parameter.
     *
     * @param string $message Error message.
     * @return bool
     */
    private static function is_temperature_error( string $message ): bool {
        return false !== stripos( $message, 'temperature' );
    }

    /**
     * Apply an optional model preference to the builder, supporting both the core
     * (snake_case) and bundled (camelCase) AI Client builders.
     *
     * @param object $builder Prompt builder.
     * @param array  $args    Caller args; reads optional 'model_preference' (string|array of model ids).
     * @param bool   $is_core Whether $builder is core's WP_AI_Client_Prompt_Builder.
     * @return object The (possibly) modified builder.
     */
    private static function apply_preference( $builder, array $args, bool $is_core ) {
        $default = self::get_default_preference();

        // Provider: a concrete per-request provider wins; 'auto' or empty falls through to the
        // saved default; if that is also 'auto'/empty, the client auto-selects.
        $req_provider = isset( $args['provider'] ) ? (string) $args['provider'] : '';
        $provider     = ( '' !== $req_provider && 'auto' !== $req_provider )
            ? $req_provider
            : (string) ( $default['provider'] ?? 'auto' );

        // Model preference list: a concrete per-request model wins; 'auto'/empty falls through to
        // the default model.
        $req_model = isset( $args['model'] ) ? (string) $args['model'] : '';
        $models    = [];
        if ( ! empty( $args['model_preference'] ) ) {
            $models = array_values( array_filter( array_map( 'strval', (array) $args['model_preference'] ) ) );
        } elseif ( '' !== $req_model && 'auto' !== $req_model ) {
            $models = [ $req_model ];
        } elseif ( ! empty( $default['model'] ) ) {
            $models = [ (string) $default['model'] ];
        }

        if ( '' !== $provider && 'auto' !== $provider ) {
            if ( $is_core ) {
                $builder = $builder->using_provider( $provider );
            } elseif ( method_exists( $builder, 'usingProvider' ) ) {
                $builder = $builder->usingProvider( $provider );
            }
        }

        if ( ! empty( $models ) ) {
            if ( $is_core ) {
                $builder = $builder->using_model_preference( ...$models );
            } elseif ( method_exists( $builder, 'usingModelPreference' ) ) {
                $builder = $builder->usingModelPreference( ...$models );
            }
        }

        return $builder;
    }

    /**
     * Get the saved global default AI preference (provider + model). Used as the fallback when a
     * request does not specify its own provider/model.
     *
     * @return array{provider: string, model: string}
     */
    public static function get_default_preference(): array {
        $pref = get_option( 'awfah_ai_preference', [] );
        if ( ! is_array( $pref ) ) {
            $pref = [];
        }

        return [
            'provider' => isset( $pref['provider'] ) ? (string) $pref['provider'] : 'auto',
            'model'    => isset( $pref['model'] ) ? (string) $pref['model'] : '',
        ];
    }

    /**
     * Persist the global default AI preference.
     *
     * @param string $provider Provider id ('auto' for no preference).
     * @param string $model    Model id (empty for the provider default).
     */
    public static function update_default_preference( string $provider, string $model ): void {
        update_option(
            'awfah_ai_preference',
            [
                'provider' => sanitize_text_field( $provider ),
                'model'    => sanitize_text_field( $model ),
            ],
            false
        );
    }

    /**
     * Extract token usage from a GenerativeAiResult.
     *
     * @param object $result GenerativeAiResult instance.
     * @return array{prompt_tokens: int, completion_tokens: int, total_tokens: int}
     */
    private static function extract_token_usage( $result ): array {
        $usage = [ 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 ];

        if ( ! method_exists( $result, 'getTokenUsage' ) ) {
            return $usage;
        }

        $token_usage = $result->getTokenUsage();
        if ( ! $token_usage ) {
            return $usage;
        }

        if ( method_exists( $token_usage, 'getPromptTokens' ) ) {
            $usage['prompt_tokens'] = (int) $token_usage->getPromptTokens();
        }
        if ( method_exists( $token_usage, 'getCompletionTokens' ) ) {
            $usage['completion_tokens'] = (int) $token_usage->getCompletionTokens();
        }
        if ( method_exists( $token_usage, 'getTotalTokens' ) ) {
            $usage['total_tokens'] = (int) $token_usage->getTotalTokens();
        }

        return $usage;
    }

    /**
     * Record AI usage to the database for dashboard reporting.
     *
     * @param string $tool_slug Tool identifier (e.g. 'generate-excerpt').
     * @param array  $usage     Token usage data.
     * @param int    $post_id   Related post ID (0 if none).
     */
    public static function record_usage( string $tool_slug, array $usage, int $post_id = 0 ): void {
        global $wpdb;
        $table = $wpdb->prefix . 'awfah_ai_usage';

        // Ensure table exists.
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            self::create_usage_table();
        }

        $wpdb->insert(
            $table,
            [
                'tool_slug'         => $tool_slug,
                'post_id'           => $post_id,
                'user_id'           => get_current_user_id(),
                'prompt_tokens'     => $usage['prompt_tokens'] ?? 0,
                'completion_tokens' => $usage['completion_tokens'] ?? 0,
                'total_tokens'      => $usage['total_tokens'] ?? 0,
                'created_at'        => current_time( 'mysql' ),
            ],
            [ '%s', '%d', '%d', '%d', '%d', '%d', '%s' ]
        );
    }

    /**
     * Create the AI usage tracking table.
     */
    public static function create_usage_table(): void {
        global $wpdb;
        $table   = $wpdb->prefix . 'awfah_ai_usage';
        $charset = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE IF NOT EXISTS {$table} (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            tool_slug varchar(100) NOT NULL,
            post_id bigint(20) unsigned NOT NULL DEFAULT 0,
            user_id bigint(20) unsigned NOT NULL DEFAULT 0,
            prompt_tokens int(11) NOT NULL DEFAULT 0,
            completion_tokens int(11) NOT NULL DEFAULT 0,
            total_tokens int(11) NOT NULL DEFAULT 0,
            created_at datetime NOT NULL,
            PRIMARY KEY (id),
            KEY tool_slug (tool_slug),
            KEY created_at (created_at),
            KEY user_id (user_id)
        ) {$charset};";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );
    }

    /**
     * Get aggregated AI usage for a date range.
     *
     * @param string $from Start date (Y-m-d).
     * @param string $to   End date (Y-m-d).
     * @return array Usage data grouped by tool_slug.
     */
    public static function get_usage_stats( string $from, string $to ): array {
        global $wpdb;
        $table = $wpdb->prefix . 'awfah_ai_usage';

        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return [];
        }

        $results = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT
                    tool_slug,
                    COUNT(*) as request_count,
                    SUM(prompt_tokens) as total_prompt_tokens,
                    SUM(completion_tokens) as total_completion_tokens,
                    SUM(total_tokens) as total_total_tokens
                FROM {$table}
                WHERE created_at >= %s AND created_at < %s
                GROUP BY tool_slug
                ORDER BY total_total_tokens DESC",
                $from . ' 00:00:00',
                $to . ' 23:59:59'
            ),
            ARRAY_A
        );

        return $results ?: [];
    }

    /**
     * Generate an image from a prompt using the configured AI provider.
     *
     * Uses a 120-second request timeout to accommodate longer image generation times.
     * Returns a File object with getBase64Data() and getMimeType() on success.
     *
     * @param string $prompt The image generation prompt.
     * @param array  $args   Optional arguments (temperature, model_preferences).
     *
     * @return File|null File object on success, null on failure.
     */
    public static function generate_image( string $prompt, array $args = [] ): ?File {
        $is_core = function_exists( 'wp_ai_client_prompt' );

        if ( $is_core ) {
            $builder = wp_ai_client_prompt( $prompt );
        } elseif ( class_exists( AI_Client::class ) ) {
            $builder = AI_Client::prompt( $prompt );
        } else {
            return null;
        }

        // On WP < 7.0 the bundled SDK takes request options for the timeout; on 7.0 the
        // http_request_args filter below enforces the 120s timeout instead.
        if ( ! $is_core && method_exists( $builder, 'usingRequestOptions' ) ) {
            $builder = $builder->usingRequestOptions(
                RequestOptions::fromArray( [ RequestOptions::KEY_TIMEOUT => 120 ] )
            );
        }

        if ( isset( $args['temperature'] ) ) {
            $temperature = (float) $args['temperature'];
            $builder     = $is_core
                ? $builder->using_temperature( $temperature )
                : $builder->usingTemperature( $temperature );
        }

        $builder = self::apply_preference( $builder, $args, $is_core );

        // Check if image generation is supported by the configured provider.
        $supported = $is_core
            ? $builder->is_supported_for_image_generation()
            : ( ! method_exists( $builder, 'isSupportedForImageGeneration' ) || $builder->isSupportedForImageGeneration() );

        if ( ! $supported ) {
            Logger::log( 'No provider/model available for image generation.' );
            return null;
        }

        // Hook into WordPress HTTP API to force 120s timeout on AI provider requests.
        $timeout_filter = static function ( $parsed_args, $url ) {
            $ai_endpoints = [
                'api.openai.com/v1/images',
                'generativelanguage.googleapis.com',
                'api.anthropic.com',
            ];

            foreach ( $ai_endpoints as $endpoint ) {
                if ( false !== strpos( $url, $endpoint ) ) {
                    $parsed_args['timeout'] = 120;
                    break;
                }
            }

            return $parsed_args;
        };

        add_filter( 'http_request_args', $timeout_filter, 9999, 2 );

        try {
            $file = $is_core
                ? $builder->generate_image()
                : ( method_exists( $builder, 'generateImage' ) ? $builder->generateImage() : null );

            if ( is_wp_error( $file ) ) {
                Logger::log( 'AI image generation failed', [ 'error' => $file->get_error_message() ] );
                return null;
            }

            if ( $file instanceof File && $file->isImage() ) {
                return $file;
            }

            return null;
        } catch ( \Throwable $e ) {
            Logger::log( 'AI image generation failed', [
                'error' => $e->getMessage(),
                'type'  => get_class( $e ),
            ] );
            return null;
        } finally {
            remove_filter( 'http_request_args', $timeout_filter, 9999 );
        }
    }
}
