<?php
/**
 * Costudio_Claude_Adapter — Anthropic Claude API adapter.
 *
 * @package Contentosapp_Studio
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages go to PHP error log, never to HTML output.

class Costudio_Claude_Adapter implements Costudio_AI_Client {

    const API_URL           = 'https://api.anthropic.com/v1/messages';
    const ANTHROPIC_VERSION = '2023-06-01';

    private string $api_key;
    private string $model;
    private int    $max_tokens      = 8096;
    private int    $thinking_budget = -1;
    private ?float $temperature     = null;

    /** Set to true after one fallback retry to prevent infinite loops. */
    private bool $fallback_attempted = false;

    /** Durable-worker hooks. They are unset for the existing synchronous path. */
    private $before_dispatch_callback = null;
    private $response_received_callback = null;
    private ?int $http_timeout = null;
    private bool $model_fallback_enabled = true;

    /** @var array Last API call usage metadata. */
    private array $last_usage = [
        'input_tokens'                => 0,
        'output_tokens'               => 0,
        'cache_creation_input_tokens' => 0,
        'cache_read_input_tokens'     => 0,
    ];

    const DEFAULT_MODEL = 'claude-sonnet-4-6';

    /**
     * Cache TTL for the system prompt. Anthropic charges 1.25× for 5m writes
     * and 2× for 1h writes; reads cost 0.1×. Default '5m' breaks even at 2
     * reads. Change to '1h' if your workload batches many productions of the
     * same agent within an hour (agencies, scheduled publishing).
     */
    const CACHE_TTL = '5m';

    public function __construct( string $api_key, string $model = '' ) {
        if ( empty( $model ) ) {
            $model = self::DEFAULT_MODEL;
        }
        $this->api_key = $api_key;
        $this->model   = $model;
    }

    /**
     * Set the thinking budget for extended reasoning.
     *
     * @param int $budget Token budget for thinking (-1 = disabled).
     */
    public function set_thinking_budget( int $budget ): void {
        $this->thinking_budget = $budget;
    }

    public function set_temperature( ?float $temperature ): void {
        $this->temperature = $temperature;
    }

    public function set_durable_transport_hooks( callable $before_dispatch, callable $response_received ): void {
        $this->before_dispatch_callback  = $before_dispatch;
        $this->response_received_callback = $response_received;
    }

    public function set_http_timeout( int $seconds ): void {
        $this->http_timeout = max( 1, $seconds );
    }

    public function disable_model_fallback(): void {
        $this->model_fallback_enabled = false;
    }

    // ── Public interface ─────────────────────────────────────────────────────

    public function generate( string $prompt, array $context = [] ): string {
        return $this->call_api( $prompt, $context );
    }

    public function generate_via_agent( int $agent_id, array $inputs, array $context = [] ): string {
        throw new LogicException( 'Secure agent execution is only available via ContentOS Cloud adapter.' );
    }

    public function generateWithSearch( string $prompt, array $context = [] ): string {
        $search_engine = new Costudio_Search_Engine();
        $query         = mb_substr( wp_strip_all_tags( $prompt ), 0, 160 );
        $results       = $search_engine->search( $query );

        if ( ! empty( $results ) ) {
            $prompt = "=== RESULTADOS DE BUSCA NA WEB ===\n\n{$results}\n\n=== INSTRUÇÃO ===\n\n{$prompt}";
        }

        return $this->call_api( $prompt, $context );
    }

    public function get_model(): string {
        return $this->model;
    }

    /**
     * Get token usage from the last API call.
     *
     * @return array {
     *   input_tokens: int,
     *   output_tokens: int,
     *   cache_creation_input_tokens: int,  // tokens written to cache (1.25× for 5m, 2× for 1h)
     *   cache_read_input_tokens: int       // tokens read from cache (0.1×)
     * }
     */
    public function get_last_usage(): array {
        return $this->last_usage;
    }

    // ── Internal ─────────────────────────────────────────────────────────────

    /**
     * Build the request body shared by call_api() and generate_streaming().
     *
     * Architecture: the agent prompt (the large, stable-ish chunk — agent
     * template + interpolated variables) goes into the `system` block with
     * cache_control. Context turns become regular `messages`, followed by a
     * short trigger asking the model to execute the system instructions.
     *
     * Why this layout: Anthropic's prompt cache is a prefix match. Placing
     * the prompt in `system` (rendered before `messages`) means the cached
     * prefix is JUST the prompt — subsequent volatile context doesn't
     * invalidate the system cache. Cache hits across retries and same-agent
     * short-window batches; cross-production hits require identical
     * interpolated values (keyword/niche) — a future fix can separate
     * template from variables for broader hit rate.
     *
     * @param  string $prompt  Fully-assembled agent prompt (interpolated).
     * @param  array  $context Pipeline-built context turns.
     * @return array           Request body without `stream` or `thinking`.
     */
    private function build_request_body( string $prompt, array $context ): array {
        $messages = [];

        foreach ( $context as $turn ) {
            $role       = ( $turn['role'] ?? 'user' ) === 'model' ? 'assistant' : 'user';
            $messages[] = [ 'role' => $role, 'content' => $turn['content'] ];
        }

        // The API requires the messages array to end with a `user` turn.
        // Append a short trigger; Claude merges consecutive same-role turns,
        // so this is safe even when the last context entry is already `user`.
        $messages[] = [
            'role'    => 'user',
            'content' => __( 'Execute the task described in the system prompt above.', 'contentosapp-studio' ),
        ];

        return [
            'model'      => $this->model,
            'max_tokens' => $this->max_tokens,
            'system'     => [
                [
                    'type'          => 'text',
                    'text'          => $prompt,
                    'cache_control' => [
                        'type' => 'ephemeral',
                        'ttl'  => self::CACHE_TTL,
                    ],
                ],
            ],
            'messages'   => $messages,
        ];
    }

    private function call_api( string $prompt, array $context ): string {
        $body = $this->build_request_body( $prompt, $context );

        if ( null !== $this->temperature ) {
            // Anthropic accepts temperature in the 0.0–1.0 range.
            $body['temperature'] = max( 0.0, min( 1.0, $this->temperature ) );
        }

        if ( $this->thinking_budget > 0 ) {
            $body['thinking'] = [
                'type'         => 'enabled',
                'budget_tokens' => $this->thinking_budget,
            ];
            // When thinking is enabled, max_tokens must cover both thinking + response.
            $body['max_tokens'] = max( $this->max_tokens, $this->thinking_budget + 8096 );
        }

        // Long pillars + extended thinking can exceed the old 120s ceiling
        // (non-streaming returns 0 bytes until done → cURL error 28). Mirror the
        // Gemini adapter's filterable 300s HTTP timeout.

        $timeout = null !== $this->http_timeout
            ? $this->http_timeout
            : (int) apply_filters( 'costudio_claude_http_timeout', 300 );

        $this->notify_before_dispatch();
        $response = wp_remote_post( self::API_URL, [
            'timeout' => $timeout,
            'headers' => [
                'x-api-key'         => $this->api_key,
                'anthropic-version' => self::ANTHROPIC_VERSION,
                'content-type'      => 'application/json',
            ],
            'body' => wp_json_encode( $body ),
        ] );

        if ( is_wp_error( $response ) ) {
            throw new Exception( __( 'Claude API error:', 'contentosapp-studio' ) . ' ' . $response->get_error_message() );
        }

        $this->notify_response_received();

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

        if ( $code !== 200 ) {
            $msg = $data['error']['message'] ?? wp_remote_retrieve_body( $response );

            // One-shot fallback when the configured model is rejected as
            // unknown/deprecated — retry with the adapter's safe default.
            if ( $this->model_fallback_enabled
                 && ! $this->fallback_attempted
                 && $this->model !== self::DEFAULT_MODEL
                 && class_exists( 'Costudio_Model_Fallback' )
                 && Costudio_Model_Fallback::is_deprecated_model_error( $code, (string) $msg ) ) {

                $this->fallback_attempted = true;
                Costudio_Model_Fallback::record( 'claude', $this->model, self::DEFAULT_MODEL, (string) $msg );
                $this->model = self::DEFAULT_MODEL;
                return $this->call_api( $prompt, $context );
            }

            throw new Exception( "Claude API HTTP {$code}: {$msg}" );
        }

        // Extract token usage from response, including cache metrics.
        if ( ! empty( $data['usage'] ) ) {
            $this->last_usage = [
                'input_tokens'                => (int) ( $data['usage']['input_tokens'] ?? 0 ),
                'output_tokens'               => (int) ( $data['usage']['output_tokens'] ?? 0 ),
                'cache_creation_input_tokens' => (int) ( $data['usage']['cache_creation_input_tokens'] ?? 0 ),
                'cache_read_input_tokens'     => (int) ( $data['usage']['cache_read_input_tokens'] ?? 0 ),
            ];
        }

        // Extract text blocks, skipping thinking blocks when extended thinking is enabled.
        $text = '';
        foreach ( ( $data['content'] ?? [] ) as $block ) {
            if ( ( $block['type'] ?? '' ) === 'text' ) {
                $text .= $block['text'] ?? '';
            }
        }
        return $text ?: ( $data['content'][0]['text'] ?? '' );
    }

    public function generate_streaming( string $prompt, array $context, callable $on_chunk ): string {
        $result = $this->call_api( $prompt, $context );
        $on_chunk( $result );
        return $result;
    }

    private function notify_before_dispatch(): void {
        if ( is_callable( $this->before_dispatch_callback ) ) {
            call_user_func( $this->before_dispatch_callback );
        }
    }

    private function notify_response_received(): void {
        if ( is_callable( $this->response_received_callback ) ) {
            call_user_func( $this->response_received_callback );
        }
    }
}
