<?php
/**
 * Local Backend Provider
 *
 * OpenAI-compatible endpoint provider for custom/local AI inference
 * (LM Studio, Ollama, llama.cpp, or any OpenAI-compatible API).
 *
 * @package WP_Agentic_Writer
 */

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

class WP_Agentic_Writer_Local_Backend_Provider implements
    WP_Agentic_Writer_AI_Provider_Interface
{
    /**
     * Local backend base URL
     *
     * @var string
     */
    private $base_url = "";

    /**
     * API key for the endpoint
     *
     * @var string
     */
    private $api_key = "";

    /**
     * Local backend image base URL
     *
     * @var string
     */
    private $image_base_url = "";

    /**
     * API key for the image endpoint
     *
     * @var string
     */
    private $image_api_key = "";

    /**
     * Model identifier
     *
     * @var string
     */
    private $model = "";

    /**
     * Per-task model overrides
     *
     * @var array
     */
    private $task_models = [];

    /**
     * Constructor
     */
    public function __construct()
    {
        $settings = get_option("wp_agentic_writer_settings", []);
        $this->base_url = $settings["local_backend_url"] ?? "";
        $this->api_key = $settings["local_backend_key"] ?? "";

        // Use separate image endpoint settings, but fallback to text endpoint if empty
        $this->image_base_url = !empty($settings["local_backend_image_url"])
            ? $settings["local_backend_image_url"]
            : $this->base_url;

        $this->image_api_key = !empty($settings["local_backend_image_key"])
            ? $settings["local_backend_image_key"]
            : $this->api_key;

        $this->model = $settings["local_backend_model"] ?? "";
        $this->task_models = $settings["local_backend_models"] ?? [];
    }

    /**
     * Get model code for a specific task type.
     *
     * Falls back to the default model if no per-task override is set.
     *
     * @param string $task_type Task type (chat, clarity, planning, writing, refinement, image).
     * @return string Model identifier.
     */
    public function get_model_for_task($task_type)
    {
        $task_model = $this->task_models[$task_type] ?? "";
        return !empty($task_model) ? $task_model : $this->model;
    }

    /**
     * Get the formatted endpoint URL.
     * Handles whether the user included /v1 in their base URL or not.
     *
     * @param string $path The API path (e.g. '/chat/completions')
     * @return string Full endpoint URL.
     */
    private function get_endpoint_url($path)
    {
        $base = rtrim($this->base_url, "/");
        // If the base URL doesn't end with /v1, and the path doesn't start with it
        if (!preg_match('#/v1$#', $base) && strpos($path, "/v1") !== 0) {
            $base .= "/v1";
        }
        // Ensure path starts with /
        if (strpos($path, "/") !== 0) {
            $path = "/" . $path;
        }
        return $base . $path;
    }

    /**
     * Non-streaming chat completion
     *
     * @param array  $messages Array of message objects.
     * @param array  $options  Optional parameters.
     * @param string $type     Task type.
     * @return array|WP_Error Response with content, model, tokens, cost.
     */
    public function chat($messages, $options = [], $type = "planning")
    {
        if (!$this->is_configured()) {
            return new WP_Error(
                "not_configured",
                __("Custom endpoint URL not configured.", "wp-agentic-writer"),
            );
        }

        $start_time = microtime(true);

        $response = wp_remote_post(
            $this->get_endpoint_url("/chat/completions"),
            [
                "headers" => [
                    "Content-Type" => "application/json",
                    "Authorization" => "Bearer " . $this->api_key,
                ],
                "body" => wp_json_encode([
                    "model" => $this->get_model_for_task($type),
                    "messages" => $messages,
                    "stream" => false,
                ]),
                "timeout" => 120, // Long timeout for local processing
                "sslverify" => false, // Local network
            ],
        );

        $generation_time = microtime(true) - $start_time;

        if (is_wp_error($response)) {
            return new WP_Error(
                "connection_failed",
                sprintf(
                    /* translators: %s: error message */
                    __(
                        "Custom endpoint connection failed: %s",
                        "wp-agentic-writer",
                    ),
                    $response->get_error_message(),
                ),
            );
        }

        $code = wp_remote_retrieve_response_code($response);
        if (200 !== $code) {
            $body = wp_remote_retrieve_body($response);
            error_log(
                "[WPAW] Local backend HTTP error: " .
                    $code .
                    ", body: " .
                    substr($body, 0, 500),
            );
            return new WP_Error(
                "api_error",
                sprintf(
                    /* translators: %1$d: HTTP status code, %2$s: response body */
                    __(
                        'Custom endpoint error (%1$d): %2$s',
                        "wp-agentic-writer",
                    ),
                    $code,
                    $body,
                ),
            );
        }

        $body = json_decode(wp_remote_retrieve_body($response), true);

        // Fallback for endpoints that ignore stream=false and send SSE chunks
        if (
            !isset($body["choices"][0]["message"]["content"]) &&
            strpos(wp_remote_retrieve_body($response), "data: ") === 0
        ) {
            $lines = explode("\n", wp_remote_retrieve_body($response));
            $content = "";
            foreach ($lines as $line) {
                if (strpos($line, "data: ") === 0) {
                    $json_str = substr($line, 6);
                    if ($json_str === "[DONE]") {
                        continue;
                    }
                    $chunk = json_decode($json_str, true);
                    if (isset($chunk["choices"][0]["delta"]["content"])) {
                        $content .= $chunk["choices"][0]["delta"]["content"];
                    }
                }
            }
            if (!empty($content)) {
                $body = [
                    "choices" => [
                        [
                            "message" => [
                                "content" => $content,
                            ],
                        ],
                    ],
                ];
            }
        }

        error_log(
            "[WPAW] Local backend response keys: " .
                implode(
                    ", ",
                    is_array($body) ? array_keys($body) : ["not_array"],
                ),
        );

        if (!isset($body["choices"][0]["message"]["content"])) {
            error_log(
                "[WPAW] Local backend response: " . wp_json_encode($body),
            );
            return new WP_Error(
                "invalid_response",
                __(
                    "Invalid response format from custom endpoint",
                    "wp-agentic-writer",
                ),
            );
        }

        $content = $body["choices"][0]["message"]["content"];

        // Detect successful HTTP responses that nonetheless carry no usable
        // content. This happens with agentic/tool-calling models (e.g. names
        // ending in -agent/-agentic) that try to emit a function call on a plain
        // prose prompt, malform it, and return empty content. Surface a clear,
        // actionable error instead of a confusing empty reply.
        if ("" === trim((string) $content)) {
            $finish_reason = $body["choices"][0]["finish_reason"] ?? "";
            error_log(
                "[WPAW] Local backend returned empty content. finish_reason=" .
                    $finish_reason .
                    ", model=" .
                    $this->get_model_for_task($type),
            );
            if (
                "malformed_function_call" === $finish_reason ||
                "tool_calls" === $finish_reason ||
                "function_call" === $finish_reason
            ) {
                return new WP_Error(
                    "empty_agentic_response",
                    sprintf(
                        /* translators: %s: model identifier */
                        __(
                            'The selected model "%s" returned no text (finish reason: tool/function call). This usually means an agentic/coding model is being used for prose. Choose a standard chat model (without an -agent or -agentic suffix) in Settings.',
                            "wp-agentic-writer",
                        ),
                        $this->get_model_for_task($type),
                    ),
                );
            }
        }

        return [
            "content" => $content,
            "model" => $this->get_model_for_task($type),
            "input_tokens" => 0, // Local backend doesn't track tokens
            "output_tokens" => 0,
            "total_tokens" => 0,
            "cost" => 0, // Free for local backend
            "generation_time" => $generation_time,
        ];
    }

    /**
     * Streaming chat completion (not supported yet)
     *
     * @param array    $messages Array of message objects.
     * @param array    $options  Optional parameters.
     * @param string   $type     Task type.
     * @param callable $callback Function to call with each chunk.
     * @return array|WP_Error Response or error.
     */
    public function chat_stream(
        $messages,
        $options = [],
        $type = "planning",
        $callback = null,
    ) {
        if (!$this->is_configured()) {
            return new WP_Error(
                "not_configured",
                __("Custom endpoint URL not configured.", "wp-agentic-writer"),
            );
        }

        $body = [
            "model" => $this->get_model_for_task($type),
            "messages" => $messages,
            "stream" => true,
        ];

        $accumulated_content = "";
        $accumulated_usage = [];
        $buffer = "";
        $finish_reason = "";

        $accumulating_callback = function ($chunk, $is_complete) use (
            &$accumulated_content,
            $callback,
        ) {
            if (!$is_complete && !empty($chunk)) {
                $accumulated_content .= $chunk;
            }

            if ($callback) {
                call_user_func(
                    $callback,
                    $chunk,
                    $is_complete,
                    $accumulated_content,
                );
            }
        };

        $ch = curl_init($this->get_endpoint_url("/chat/completions"));

        $headers = [
            "Content-Type: application/json",
            "Authorization: Bearer " . $this->api_key,
        ];

        // Add search headers if web search is enabled
        if (!empty($options["web_search_enabled"])) {
            $headers[] = "X-Search-Enabled: true";
            // Extract last user message as search query
            foreach (array_reverse($messages) as $msg) {
                if ("user" === $msg["role"]) {
                    $headers[] =
                        "X-Search-Query: " . substr($msg["content"], 0, 500);
                    break;
                }
            }
        }

        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => false,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_WRITEFUNCTION => function ($curl, $data) use (
                &$buffer,
                $accumulating_callback,
                &$accumulated_content,
                &$accumulated_usage,
                &$finish_reason,
            ) {
                $buffer .= $data;

                while (true) {
                    $newline_pos = strpos($buffer, "\n");
                    if (false === $newline_pos) {
                        break;
                    }

                    $line = substr($buffer, 0, $newline_pos);
                    $buffer = substr($buffer, $newline_pos + 1);

                    $line = trim($line);
                    if (empty($line) || 0 !== strpos($line, "data: ")) {
                        continue;
                    }

                    $json_str = substr($line, 6);

                    if ("[DONE]" === $json_str || '"[DONE]"' === $json_str) {
                        call_user_func($accumulating_callback, "", true);
                        return strlen($data);
                    }

                    $chunk = json_decode($json_str, true);
                    if (
                        isset($chunk["choices"][0]["delta"]["content"]) &&
                        is_string($chunk["choices"][0]["delta"]["content"])
                    ) {
                        $content = $chunk["choices"][0]["delta"]["content"];
                        call_user_func($accumulating_callback, $content, false);
                    }
                    // Some OpenAI-compatible proxies stream a complete message
                    // payload instead of delta chunks.
                    if (
                        isset($chunk["choices"][0]["message"]["content"]) &&
                        is_string($chunk["choices"][0]["message"]["content"])
                    ) {
                        $content = $chunk["choices"][0]["message"]["content"];
                        if (
                            "" !== $content &&
                            0 === strpos($content, $accumulated_content)
                        ) {
                            $content = substr(
                                $content,
                                strlen($accumulated_content),
                            );
                        }
                        if ("" !== $content) {
                            call_user_func(
                                $accumulating_callback,
                                $content,
                                false,
                            );
                        }
                    }
                    // Also support text-completion style chunks.
                    if (
                        isset($chunk["choices"][0]["text"]) &&
                        is_string($chunk["choices"][0]["text"])
                    ) {
                        $content = $chunk["choices"][0]["text"];
                        if ("" !== $content) {
                            call_user_func(
                                $accumulating_callback,
                                $content,
                                false,
                            );
                        }
                    }
                    // Also support Ollama-compatible chat stream chunks.
                    if (
                        isset($chunk["message"]["content"]) &&
                        is_string($chunk["message"]["content"])
                    ) {
                        $content = $chunk["message"]["content"];
                        if ("" !== $content) {
                            call_user_func(
                                $accumulating_callback,
                                $content,
                                false,
                            );
                        }
                    }
                    // Also support simple content/response payloads.
                    if (
                        isset($chunk["content"]) &&
                        is_string($chunk["content"])
                    ) {
                        $content = $chunk["content"];
                        if ("" !== $content) {
                            call_user_func(
                                $accumulating_callback,
                                $content,
                                false,
                            );
                        }
                    }
                    if (
                        isset($chunk["response"]) &&
                        is_string($chunk["response"])
                    ) {
                        $content = $chunk["response"];
                        if ("" !== $content) {
                            call_user_func(
                                $accumulating_callback,
                                $content,
                                false,
                            );
                        }
                    }
                    // Also support Anthropic format if proxy uses it
                    if (
                        isset($chunk["type"]) &&
                        "content_block_delta" === $chunk["type"] &&
                        isset($chunk["delta"]["text"])
                    ) {
                        $content = $chunk["delta"]["text"];
                        call_user_func($accumulating_callback, $content, false);
                    }

                    // Support reasoning_content from thinking models (e.g. Claude extended thinking)
                    // These models stream reasoning separately from the final answer.
                    // We pass it through the callback so the frontend can choose to display it.
                    if (
                        isset(
                            $chunk["choices"][0]["delta"]["reasoning_content"],
                        ) &&
                        is_string(
                            $chunk["choices"][0]["delta"]["reasoning_content"],
                        )
                    ) {
                        $reasoning =
                            $chunk["choices"][0]["delta"]["reasoning_content"];
                        if (defined("WP_DEBUG") && WP_DEBUG) {
                            error_log(
                                "WPAW Local Backend: Received reasoning_content chunk (" .
                                    strlen($reasoning) .
                                    " chars)",
                            );
                        }
                        // Pass reasoning content through with a special prefix so frontend can identify it
                        // The frontend can strip this prefix and display reasoning in a collapsible section
                        call_user_func(
                            $accumulating_callback,
                            $reasoning,
                            false,
                        );
                    }

                    if (isset($chunk["usage"])) {
                        $accumulated_usage = $chunk["usage"];
                    }
                    if (
                        isset($chunk["choices"][0]["finish_reason"]) &&
                        is_string($chunk["choices"][0]["finish_reason"]) &&
                        "" !== $chunk["choices"][0]["finish_reason"]
                    ) {
                        $finish_reason = $chunk["choices"][0]["finish_reason"];
                    }
                }

                return strlen($data);
            },
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_POSTFIELDS => wp_json_encode($body),
            CURLOPT_TIMEOUT => 300,
        ]);

        $start_time = microtime(true);
        $result = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_error = curl_error($ch);
        curl_close($ch);

        // Debug logging
        error_log(
            "WPAW Local Backend chat_stream: HTTP=" .
                $http_code .
                ", curl_result=" .
                ($result ? "true" : "false") .
                ", curl_error=" .
                $curl_error .
                ", accumulated_content_len=" .
                strlen($accumulated_content) .
                ", buffer_len=" .
                strlen($buffer),
        );

        if (false === $result && !empty($curl_error)) {
            return new WP_Error("curl_error", "cURL error: " . $curl_error);
        }

        if ($http_code >= 400) {
            error_log(
                "WPAW Local Backend API error: HTTP=" .
                    $http_code .
                    ", Buffer: " .
                    substr($buffer, 0, 1000),
            );
            return new WP_Error(
                "api_error",
                sprintf(
                    "API error (%d): %s",
                    $http_code,
                    substr($buffer, 0, 500),
                ),
            );
        }

        // FALLBACK: If no SSE chunks were parsed, the proxy likely returned a plain JSON response.
        // Try to parse the leftover buffer as a standard OpenAI/Anthropic JSON response.
        if (empty($accumulated_content) && !empty($buffer)) {
            error_log(
                "WPAW Local Backend: No SSE chunks parsed. Attempting raw JSON fallback. Buffer preview: " .
                    substr($buffer, 0, 500),
            );
            $raw_json = json_decode($buffer, true);
            if (is_array($raw_json)) {
                // Provider returned a structured error payload (e.g. HTTP 4xx/5xx
                // from the upstream model). Surface the real message instead of a
                // generic "empty response" so the user knows what actually failed.
                if (isset($raw_json["error"])) {
                    $provider_error = is_array($raw_json["error"])
                        ? $raw_json["error"]["message"] ??
                            wp_json_encode($raw_json["error"])
                        : (string) $raw_json["error"];
                    error_log(
                        "WPAW Local Backend: Provider returned error payload: " .
                            substr((string) $provider_error, 0, 300),
                    );
                    return new WP_Error(
                        "provider_error",
                        sprintf(
                            /* translators: %s: provider error message */
                            __(
                                "The AI provider returned an error: %s",
                                "wp-agentic-writer",
                            ),
                            (string) $provider_error,
                        ),
                    );
                }
                // OpenAI format: choices[0].message.content
                if (isset($raw_json["choices"][0]["message"]["content"])) {
                    $accumulated_content =
                        $raw_json["choices"][0]["message"]["content"];
                    error_log(
                        "WPAW Local Backend: Extracted content via OpenAI format fallback (" .
                            strlen($accumulated_content) .
                            " chars)",
                    );
                }
                // Anthropic format: content[0].text
                elseif (isset($raw_json["content"][0]["text"])) {
                    $accumulated_content = $raw_json["content"][0]["text"];
                    error_log(
                        "WPAW Local Backend: Extracted content via Anthropic format fallback (" .
                            strlen($accumulated_content) .
                            " chars)",
                    );
                }
                // Simple format: content string
                elseif (
                    isset($raw_json["content"]) &&
                    is_string($raw_json["content"])
                ) {
                    $accumulated_content = $raw_json["content"];
                    error_log(
                        "WPAW Local Backend: Extracted content via simple format fallback (" .
                            strlen($accumulated_content) .
                            " chars)",
                    );
                }

                if (!empty($accumulated_content) && $callback) {
                    // Emit the full content as a single chunk so the SSE handler picks it up
                    call_user_func(
                        $callback,
                        $accumulated_content,
                        false,
                        $accumulated_content,
                    );
                    call_user_func($callback, "", true, $accumulated_content);
                }

                // Extract usage if available
                if (isset($raw_json["usage"])) {
                    $accumulated_usage = $raw_json["usage"];
                }
            } else {
                error_log(
                    "WPAW Local Backend: Buffer is not valid JSON. First 300 chars: " .
                        substr($buffer, 0, 300),
                );
            }
        }

        // If the stream completed with a tool/function finish reason but no
        // text, an agentic/coding model was used for prose. Retrying won't help
        // (it's a model-capability mismatch), so surface a clear error instead
        // of burning two more calls on the non-streaming fallback.
        if (
            empty($accumulated_content) &&
            ("malformed_function_call" === $finish_reason ||
                "tool_calls" === $finish_reason ||
                "function_call" === $finish_reason)
        ) {
            error_log(
                "WPAW Local Backend: Empty content with finish_reason=" .
                    $finish_reason .
                    "; model=" .
                    $this->get_model_for_task($type),
            );
            return new WP_Error(
                "empty_agentic_response",
                sprintf(
                    /* translators: %s: model identifier */
                    __(
                        'The selected model "%s" returned no text (finish reason: tool/function call). This usually means an agentic/coding model is being used for prose. Choose a standard chat model (without an -agent or -agentic suffix) in Settings.',
                        "wp-agentic-writer",
                    ),
                    $this->get_model_for_task($type),
                ),
            );
        }

        if (empty($accumulated_content)) {
            error_log(
                "WPAW Local Backend: Streaming returned empty content; falling back to non-streaming chat.",
            );
            $fallback_response = $this->chat($messages, $options, $type);
            if (is_wp_error($fallback_response)) {
                return $fallback_response;
            }
            $accumulated_content = $fallback_response["content"] ?? "";

            if ("" === trim((string) $accumulated_content)) {
                error_log(
                    "WPAW Local Backend: Non-streaming fallback returned empty content; retrying once.",
                );
                $fallback_response = $this->chat($messages, $options, $type);
                if (is_wp_error($fallback_response)) {
                    return $fallback_response;
                }
                $accumulated_content = $fallback_response["content"] ?? "";
            }

            if ("" === trim((string) $accumulated_content)) {
                return new WP_Error(
                    "empty_response",
                    __(
                        "The provider returned an empty chat response.",
                        "wp-agentic-writer",
                    ),
                );
            }

            if (!empty($accumulated_content) && $callback) {
                call_user_func(
                    $callback,
                    $accumulated_content,
                    false,
                    $accumulated_content,
                );
                call_user_func($callback, "", true, $accumulated_content);
            }

            return [
                "content" => $accumulated_content,
                "model" =>
                    $fallback_response["model"] ??
                    $this->get_model_for_task($type),
                "input_tokens" =>
                    $fallback_response["input_tokens"] ??
                    ($accumulated_usage["prompt_tokens"] ?? 0),
                "output_tokens" =>
                    $fallback_response["output_tokens"] ??
                    ($accumulated_usage["completion_tokens"] ?? 0),
                "total_tokens" =>
                    $fallback_response["total_tokens"] ??
                    ($accumulated_usage["total_tokens"] ?? 0),
                "cost" => $fallback_response["cost"] ?? 0,
                "generation_time" => microtime(true) - $start_time,
            ];
        }

        return [
            "content" => $accumulated_content,
            "model" => $this->get_model_for_task($type),
            "input_tokens" => $accumulated_usage["prompt_tokens"] ?? 0,
            "output_tokens" => $accumulated_usage["completion_tokens"] ?? 0,
            "total_tokens" => $accumulated_usage["total_tokens"] ?? 0,
            "cost" => 0,
            "generation_time" => microtime(true) - $start_time,
        ];
    }

    /**
     * Generate image via OpenAI-compatible endpoint
     *
     * @param string $prompt Image prompt.
     * @param string $model  Model to use (falls back to configured model).
     * @param array  $options Optional parameters (size, quality, n).
     * @return array|WP_Error Response with url, model, cost, or error.
     */
    public function generate_image($prompt, $model = null, $options = [])
    {
        if (empty($this->image_base_url)) {
            return new WP_Error(
                "no_api_url",
                __(
                    "Custom Endpoint URL for Images is not configured.",
                    "wp-agentic-writer",
                ),
            );
        }

        if (empty($model)) {
            $model = $this->get_model_for_task("image");
        }

        if (empty($model)) {
            return new WP_Error(
                "no_model",
                __(
                    "Custom Endpoint Image Generation Model is not defined. Please configure a valid model code in Settings > Custom Endpoint.",
                    "wp-agentic-writer",
                ),
            );
        }

        $base = rtrim($this->image_base_url, "/");
        if (!preg_match('#/v1$#', $base)) {
            $base .= "/v1";
        }
        $endpoint = $base . "/images/generations";

        $body = [
            "model" => $model,
            "prompt" => $prompt,
            "n" => $options["n"] ?? 1,
            "size" => $options["size"] ?? "1024x1024",
        ];

        // Pass along standard DALL-E style optional params if present
        if (isset($options["quality"])) {
            $body["quality"] = $options["quality"];
        }
        if (isset($options["style"])) {
            $body["style"] = $options["style"];
        }

        $args = [
            "body" => wp_json_encode($body),
            "headers" => [
                "Content-Type" => "application/json",
            ],
            "timeout" => 60,
        ];

        if (!empty($this->image_api_key)) {
            $args["headers"]["Authorization"] =
                "Bearer " . $this->image_api_key;
        }

        $start_time = microtime(true);
        $response = wp_remote_post($endpoint, $args);
        $generation_time = microtime(true) - $start_time;

        if (is_wp_error($response)) {
            return $response;
        }

        $status_code = wp_remote_retrieve_response_code($response);
        $body_response = json_decode(wp_remote_retrieve_body($response), true);

        if (200 !== $status_code) {
            $error_msg =
                $body_response["error"]["message"] ??
                "Unknown error generating image via custom endpoint.";
            return new WP_Error("api_error", $error_msg, [
                "status" => $status_code,
            ]);
        }

        if (empty($body_response["data"][0]["url"])) {
            return new WP_Error(
                "api_error",
                "No image URL returned by custom endpoint.",
            );
        }

        return [
            "url" => $body_response["data"][0]["url"],
            "cost" => 0,
            "generation_time" => $generation_time,
            "model" => $model,
            "prompt" => $prompt,
            "input_tokens" => 0,
            "output_tokens" => 0,
        ];
    }

    /**
     * Check if provider is configured
     *
     * @return bool True if base URL is set.
     */
    public function is_configured()
    {
        return !empty($this->base_url);
    }

    /**
     * Test connection to local backend
     *
     * @return array|WP_Error Success array or error.
     */
    public function test_connection()
    {
        if (!$this->is_configured()) {
            return new WP_Error(
                "not_configured",
                __("Custom endpoint URL not configured", "wp-agentic-writer"),
            );
        }

        // Best-effort reachability checks. Do not hard-fail here; inference test below is authoritative.
        $reachable = false;
        $health_endpoints = ["/ping", "/health", "/"];
        foreach ($health_endpoints as $endpoint) {
            $health_response = wp_remote_get($this->base_url . $endpoint, [
                "timeout" => 5,
                "sslverify" => false,
            ]);

            if (is_wp_error($health_response)) {
                continue;
            }

            $health_body = trim(
                (string) wp_remote_retrieve_body($health_response),
            );
            $health_code = (int) wp_remote_retrieve_response_code(
                $health_response,
            );
            $health_json = json_decode($health_body, true);

            // Any 2xx indicates proxy process is reachable.
            if ($health_code >= 200 && $health_code < 300) {
                $reachable = true;
            }

            // Stronger signal for known proxy responses.
            if (strcasecmp($health_body, "pong") === 0) {
                $reachable = true;
                break;
            }
            if (is_array($health_json)) {
                $ok_flag =
                    $health_json["ok"] ?? ($health_json["success"] ?? null);
                $status = strtolower((string) ($health_json["status"] ?? ""));
                if (
                    true === $ok_flag ||
                    in_array($status, ["ok", "healthy", "pong"], true)
                ) {
                    $reachable = true;
                    break;
                }
            }
        }

        // Test actual inference with simple prompt
        $test_response = wp_remote_post(
            $this->get_endpoint_url("/chat/completions"),
            [
                "headers" => [
                    "Content-Type" => "application/json",
                    "Authorization" => "Bearer " . $this->api_key,
                ],
                "body" => wp_json_encode([
                    "model" => $this->model,
                    "messages" => [
                        [
                            "role" => "user",
                            "content" =>
                                "Reply with exactly: Connection test successful",
                        ],
                    ],
                    "stream" => false,
                ]),
                "timeout" => 30,
                "sslverify" => false,
            ],
        );

        if (is_wp_error($test_response)) {
            // If both health and inference are unreachable, report connection issue.
            if (!$reachable) {
                return new WP_Error(
                    "ping_failed",
                    sprintf(
                        /* translators: %s: error message */
                        __(
                            "Cannot reach endpoint: %s. Is it running and reachable from this server?",
                            "wp-agentic-writer",
                        ),
                        $test_response->get_error_message(),
                    ),
                );
            }
            return new WP_Error(
                "inference_failed",
                sprintf(
                    /* translators: %s: error message */
                    __("Inference test failed: %s", "wp-agentic-writer"),
                    $test_response->get_error_message(),
                ),
            );
        }

        $code = wp_remote_retrieve_response_code($test_response);
        $raw_body = wp_remote_retrieve_body($test_response);
        $test_body = json_decode($raw_body, true);

        if ($code >= 400) {
            $error_msg =
                $test_body["error"]["message"] ?? substr($raw_body, 0, 150);
            return new WP_Error(
                "api_error",
                sprintf(
                    /* translators: %1$d: HTTP status code, %2$s: error message */
                    __('API Error (HTTP %1$d): %2$s', "wp-agentic-writer"),
                    $code,
                    esc_html($error_msg),
                ),
            );
        }

        if (!isset($test_body["choices"][0]["message"]["content"])) {
            // Some endpoints might still ignore stream=false and return SSE chunks.
            // If the response starts with "data: ", try to parse the first chunk's content.
            if (strpos($raw_body, "data: ") === 0) {
                $lines = explode("\n", $raw_body);
                $content = "";
                foreach ($lines as $line) {
                    if (strpos($line, "data: ") === 0) {
                        $json_str = substr($line, 6);
                        if ($json_str === "[DONE]") {
                            continue;
                        }
                        $chunk = json_decode($json_str, true);
                        if (isset($chunk["choices"][0]["delta"]["content"])) {
                            $content .=
                                $chunk["choices"][0]["delta"]["content"];
                        }
                    }
                }

                if (!empty($content)) {
                    return [
                        "success" => true,
                        "message" => __(
                            "Connected! Endpoint responding correctly.",
                            "wp-agentic-writer",
                        ),
                        "sample_response" => $content,
                    ];
                }
            }

            return new WP_Error(
                "invalid_response",
                __(
                    "Endpoint not responding with expected OpenAI-compatible format. Check your URL and API key.",
                    "wp-agentic-writer",
                ) .
                    " Response preview: " .
                    substr($raw_body, 0, 100),
            );
        }

        return [
            "success" => true,
            "message" => __(
                "Connected! Endpoint responding correctly.",
                "wp-agentic-writer",
            ),
            "sample_response" => $test_body["choices"][0]["message"]["content"],
        ];
    }

    /**
     * Check if provider supports task type
     *
     * @param string $type Task type.
     * @return bool True if supported.
     */
    public function supports_task_type($type)
    {
        // Custom endpoint supports both text and image tasks
        return in_array(
            $type,
            ["chat", "clarity", "planning", "writing", "refinement", "image"],
            true,
        );
    }
}
