<?php
/**
 * MEMANTO Client
 *
 * PHP client for MEMANTO API v2 — external semantic memory service.
 * Provides persistent, cross-session memory for the WP Agentic Writer plugin.
 *
 * @package WP_Agentic_Writer
 * @since   0.3.0
 */

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

/**
 * Class WP_Agentic_Writer_Memanto_Client
 *
 * Communicates with a MEMANTO instance (powered by Moorcheh SDK).
 * All calls are wrapped in error handling — failures never disrupt the plugin.
 */
class WP_Agentic_Writer_Memanto_Client
{
    /**
     * Singleton instance.
     *
     * @var WP_Agentic_Writer_Memanto_Client
     */
    private static $instance = null;

    /**
     * MEMANTO base URL (e.g. https://abc123.context.wpagentic.dev).
     *
     * @var string
     */
    private $base_url = "";

    /**
     * Cached health status.
     *
     * @var array|null { healthy: bool, checked_at: int } or null if not checked yet.
     */
    private $health_cache = null;

    /**
     * Circuit breaker transient key.
     *
     * @var string
     */
    private const CIRCUIT_TRANSIENT = "wpaw_memanto_circuit";

    /**
     * Circuit breaker timestamp transient key.
     *
     * @var string
     */
    private const CIRCUIT_SINCE_TRANSIENT = "wpaw_memanto_circuit_since";

    /**
     * Circuit breaker timeout in seconds (try again after this).
     *
     * @var int
     */
    private const CIRCUIT_TIMEOUT = 60;

    /**
     * Circuit breaker max duration in seconds (stay open max this long).
     *
     * @var int
     */
    private const CIRCUIT_MAX_DURATION = 300;

    /**
     * Get singleton instance.
     *
     * @return WP_Agentic_Writer_Memanto_Client
     */
    public static function get_instance()
    {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Create a one-off client for a specific base URL.
     *
     * @param string $base_url MEMANTO base URL.
     * @return WP_Agentic_Writer_Memanto_Client
     */
    public static function for_base_url($base_url)
    {
        return new self($base_url);
    }

    /**
     * Constructor.
     *
     * @param string|null $base_url Optional MEMANTO base URL override.
     */
    private function __construct($base_url = null)
    {
        if (null !== $base_url) {
            $this->base_url = untrailingslashit($base_url);
            return;
        }

        $settings = get_option("wp_agentic_writer_settings", []);
        $this->base_url = untrailingslashit($settings["memanto_url"] ?? "");
    }

    // =========================================================================
    // Circuit Breaker
    // =========================================================================

    /**
     * Check if circuit breaker allows requests.
     *
     * @return bool True if circuit is closed (requests allowed), false if open (skip).
     */
    private function check_circuit_breaker()
    {
        $state = get_transient(self::CIRCUIT_TRANSIENT);

        if ($state !== "open") {
            return true;
        }

        // Circuit is open — check if timeout has passed.
        $since = get_transient(self::CIRCUIT_SINCE_TRANSIENT);

        if ($since && time() - $since < self::CIRCUIT_TIMEOUT) {
            wpaw_debug_log("MEMANTO circuit breaker is open, skipping request");
            return false;
        }

        // Timeout passed — try again (half-open state).
        delete_transient(self::CIRCUIT_TRANSIENT);
        delete_transient(self::CIRCUIT_SINCE_TRANSIENT);
        wpaw_debug_log("MEMANTO circuit breaker resetting, attempting request");
        return true;
    }

    /**
     * Trip the circuit breaker (open state).
     *
     * Once open, requests are skipped for CIRCUIT_TIMEOUT seconds,
     * then it retries. Max duration is CIRCUIT_MAX_DURATION.
     */
    private function trip_circuit_breaker()
    {
        set_transient(
            self::CIRCUIT_TRANSIENT,
            "open",
            self::CIRCUIT_MAX_DURATION,
        );
        set_transient(
            self::CIRCUIT_SINCE_TRANSIENT,
            time(),
            self::CIRCUIT_MAX_DURATION,
        );
        wpaw_debug_log("MEMANTO circuit breaker tripped");
    }

    // =========================================================================
    // Configuration & Health
    // =========================================================================

    /**
     * Whether MEMANTO is configured (URL + Moorcheh key set).
     *
     * @return bool
     */
    public function is_configured()
    {
        return !empty($this->base_url) && !empty($this->get_moorcheh_key());
    }

    /**
     * Whether MEMANTO is enabled in settings.
     *
     * @return bool
     */
    public function is_enabled()
    {
        $settings = get_option("wp_agentic_writer_settings", []);
        return !empty($settings["memanto_enabled"]);
    }

    /**
     * Whether MEMANTO is enabled, configured, and reachable.
     *
     * @return bool
     */
    public function is_active()
    {
        return $this->is_enabled() &&
            $this->is_configured() &&
            $this->check_circuit_breaker() &&
            $this->is_healthy();
    }

    /**
     * Check MEMANTO health endpoint. Result cached for 5 minutes.
     *
     * @return bool True if healthy.
     */
    public function is_healthy()
    {
        if (!$this->is_configured()) {
            return false;
        }

        // Use cached result if fresh (5 minutes).
        if (null !== $this->health_cache) {
            if (time() - $this->health_cache["checked_at"] < 300) {
                return $this->health_cache["healthy"];
            }
        }

        // Also check transient for cross-request caching.
        $cached = get_transient("wpaw_memanto_health");
        if (
            false !== $cached &&
            isset($cached["checked_at"]) &&
            time() - $cached["checked_at"] < 300
        ) {
            $this->health_cache = $cached;
            return $cached["healthy"];
        }

        $response = $this->get("/health");

        if (is_wp_error($response)) {
            $this->health_cache = ["healthy" => false, "checked_at" => time()];
            set_transient("wpaw_memanto_health", $this->health_cache, 300);
            return false;
        }

        $healthy =
            !empty($response["status"]) && "healthy" === $response["status"];
        $this->health_cache = ["healthy" => $healthy, "checked_at" => time()];
        set_transient("wpaw_memanto_health", $this->health_cache, 300);
        return $healthy;
    }

    /**
     * Force-refresh the health check (used by Test Connection button).
     *
     * @return array { healthy: bool, details: array|null }
     */
    public function check_health_fresh()
    {
        if (!$this->is_configured()) {
            return ["healthy" => false, "details" => null];
        }

        delete_transient("wpaw_memanto_health");
        $this->health_cache = null;

        $response = $this->get("/health");

        if (is_wp_error($response)) {
            return [
                "healthy" => false,
                "details" => ["error" => $response->get_error_message()],
            ];
        }

        $healthy =
            !empty($response["status"]) && "healthy" === $response["status"];
        $this->health_cache = ["healthy" => $healthy, "checked_at" => time()];
        set_transient("wpaw_memanto_health", $this->health_cache, 300);

        return ["healthy" => $healthy, "details" => $response];
    }

    // =========================================================================
    // Agent Management
    // =========================================================================

    /**
     * Ensure an agent exists. Creates if not found.
     *
     * @param string $agent_id Agent identifier (e.g. "wp-user-1" or "wp-post-42").
     * @return bool True on success.
     */
    public function ensure_agent($agent_id)
    {
        if (!$this->is_configured()) {
            return false;
        }

        // Check if agent already exists.
        $agent = $this->get("/api/v2/agents/{$agent_id}");
        if (!is_wp_error($agent) && !empty($agent["agent_id"])) {
            return true;
        }

        // Create agent.
        $result = $this->post("/api/v2/agents", [
            "agent_id" => $agent_id,
            "pattern" => "support",
            "description" => "WP Agentic Writer agent",
        ]);

        return !is_wp_error($result);
    }

    // =========================================================================
    // Session Management
    // =========================================================================

    /**
     * Activate a session for an agent. Returns cached token if still valid.
     *
     * @param string $agent_id Agent identifier.
     * @return string|false Session token or false on failure.
     */
    public function activate_session($agent_id)
    {
        if (!$this->is_configured()) {
            return false;
        }

        $transient_key = "wpaw_memanto_token_" . md5($agent_id);
        $cached_token = get_transient($transient_key);

        if (!empty($cached_token)) {
            return $cached_token;
        }

        $response = $this->post(
            "/api/v2/agents/{$agent_id}/activate",
            [],
            $agent_id,
        );

        if (is_wp_error($response) || empty($response["session_token"])) {
            wpaw_debug_log("MEMANTO activate_session failed", [
                "agent_id" => $agent_id,
            ]);
            return false;
        }

        $token = $response["session_token"];
        $expires_at = strtotime($response["expires_at"] ?? "+6 hours");
        $ttl = max(60, $expires_at - time() - 300); // Expire 5 min before actual.

        set_transient($transient_key, $token, $ttl);
        return $token;
    }

    /**
     * Deactivate a session for an agent.
     *
     * Clears the cached token and sends the deactivate request
     * without injecting a session token (avoids re-activation loop).
     *
     * @param string $agent_id Agent identifier.
     * @return bool True on success.
     */
    public function deactivate_session($agent_id)
    {
        $transient_key = "wpaw_memanto_token_" . md5($agent_id);
        delete_transient($transient_key);

        if (!$this->is_configured()) {
            return false;
        }

        $url = $this->base_url . "/api/v2/agents/{$agent_id}/deactivate";
        $headers = $this->get_headers();

        $response = wp_remote_post($url, [
            "headers" => $headers,
            "body" => wp_json_encode([]),
            "timeout" => 10,
        ]);

        if (is_wp_error($response)) {
            wpaw_debug_log("MEMANTO deactivate_session failed", [
                "agent_id" => $agent_id,
            ]);
            return false;
        }

        return true;
    }

    // =========================================================================
    // Memory Operations
    // =========================================================================

    /**
     * Store a memory.
     *
     * @param string $agent_id Agent identifier.
     * @param string $content  Memory content (max 10000 chars).
     * @param string $type     Memory type (fact, preference, goal, decision, artifact, learning, event, instruction, relationship, context, observation, commitment, error).
     * @param array  $tags     Optional tags.
     * @param string $title    Optional title (max 100 chars).
     * @return bool True on success.
     */
    public function remember(
        $agent_id,
        $content,
        $type = "context",
        $tags = [],
        $title = "",
    ) {
        if (!$this->is_active()) {
            return false;
        }

        $body = [
            "content" => mb_substr($content, 0, 10000),
            "type" => $type,
            "tags" => $tags,
            "source" => "wp-agentic-writer",
        ];

        if (!empty($title)) {
            $body["title"] = mb_substr($title, 0, 100);
        }

        $response = $this->post(
            "/api/v2/agents/{$agent_id}/remember",
            $body,
            $agent_id,
        );

        if (is_wp_error($response)) {
            wpaw_debug_log("MEMANTO remember failed", [
                "agent_id" => $agent_id,
                "type" => $type,
                "error" => $response->get_error_message(),
            ]);
            return false;
        }

        return true;
    }

    /**
     * Store multiple memories in batch (max 100).
     *
     * @param string $agent_id Agent identifier.
     * @param array  $memories Array of memory items. Each item: { content, type, tags, title }.
     * @return bool True on success.
     */
    public function batch_remember($agent_id, $memories)
    {
        if (!$this->is_active() || empty($memories)) {
            return false;
        }

        $batch = [];
        foreach (array_slice($memories, 0, 100) as $item) {
            $entry = [
                "content" => mb_substr($item["content"] ?? "", 0, 10000),
                "type" => $item["type"] ?? "context",
                "source" => "wp-agentic-writer",
            ];
            if (!empty($item["title"])) {
                $entry["title"] = mb_substr($item["title"], 0, 100);
            }
            if (!empty($item["tags"])) {
                $entry["tags"] = $item["tags"];
            }
            $batch[] = $entry;
        }

        $response = $this->post(
            "/api/v2/agents/{$agent_id}/batch-remember",
            ["memories" => $batch],
            $agent_id,
        );

        if (is_wp_error($response)) {
            wpaw_debug_log("MEMANTO batch_remember failed", [
                "agent_id" => $agent_id,
                "count" => count($batch),
                "error" => $response->get_error_message(),
            ]);
            return false;
        }

        return true;
    }

    /**
     * Semantic search / recall memories.
     *
     * @param string $agent_id      Agent identifier.
     * @param string $query         Search query.
     * @param array  $type_filter   Optional memory type filter.
     * @param int    $limit         Max results (default 10).
     * @param float  $min_similarity Minimum similarity score 0-1.
     * @return array Recalled memories (empty on failure).
     */
    public function recall(
        $agent_id,
        $query,
        $type_filter = [],
        $limit = 10,
        $min_similarity = 0.3,
    ) {
        if (!$this->is_active()) {
            return [];
        }

        $body = [
            "query" => $query,
            "limit" => $limit,
            "min_similarity" => $min_similarity,
        ];

        if (!empty($type_filter)) {
            $body["type"] = $type_filter;
        }

        $response = $this->post(
            "/api/v2/agents/{$agent_id}/recall",
            $body,
            $agent_id,
        );

        if (is_wp_error($response)) {
            wpaw_debug_log("MEMANTO recall failed", [
                "agent_id" => $agent_id,
                "query" => substr($query, 0, 100),
                "error" => $response->get_error_message(),
            ]);
            return [];
        }

        return is_array($response) ? $response : [];
    }

    /**
     * Recall most recent memories.
     *
     * @param string $agent_id    Agent identifier.
     * @param int    $limit       Max results.
     * @param array  $type_filter Optional memory type filter.
     * @return array Recent memories (empty on failure).
     */
    public function recall_recent($agent_id, $limit = 10, $type_filter = [])
    {
        if (!$this->is_active()) {
            return [];
        }

        $body = ["limit" => $limit];
        if (!empty($type_filter)) {
            $body["type"] = $type_filter;
        }

        $response = $this->post(
            "/api/v2/agents/{$agent_id}/recall/recent",
            $body,
            $agent_id,
        );

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

        return is_array($response) ? $response : [];
    }

    // =========================================================================
    // Agent ID Builders
    // =========================================================================

    /**
     * Build user-level agent ID.
     *
     * @param int $user_id WordPress user ID.
     * @return string Agent ID like "wp-user-1".
     */
    public function get_user_agent_id($user_id)
    {
        return "wp-user-" . absint($user_id);
    }

    /**
     * Build post-level agent ID.
     *
     * @param int $post_id WordPress post ID.
     * @return string Agent ID like "wp-post-42".
     */
    public function get_post_agent_id($post_id)
    {
        return "wp-post-" . absint($post_id);
    }

    // =========================================================================
    // HTTP Helpers (private)
    // =========================================================================

    /**
     * Get the Moorcheh API key from settings.
     *
     * @return string
     */
    private function get_moorcheh_key()
    {
        $settings = get_option("wp_agentic_writer_settings", []);
        return $settings["memanto_moorcheh_key"] ?? "";
    }

    /**
     * GET request to MEMANTO API.
     *
     * @param string $path API path (e.g. "/health", "/api/v2/agents/{id}").
     * @return array|WP_Error Decoded response or error.
     */
    private function get($path)
    {
        $url = $this->base_url . $path;

        $response = wp_remote_get($url, [
            "headers" => $this->get_headers(),
            "timeout" => 10,
        ]);

        $result = $this->parse_response($response);
        if (is_wp_error($result)) {
            $this->trip_circuit_breaker();
        }
        return $result;
    }

    /**
     * POST request to MEMANTO API.
     *
     * @param string $path     API path.
     * @param array  $body     Request body.
     * @param string $agent_id Optional agent ID for session token injection.
     * @return array|WP_Error Decoded response or error.
     */
    private function post($path, $body = [], $agent_id = "")
    {
        $url = $this->base_url . $path;
        $headers = $this->get_headers();

        // Inject session token if we have an agent_id.
        if (!empty($agent_id)) {
            $token = $this->activate_session($agent_id);
            if ($token) {
                $headers["X-Session-Token"] = $token;
            }
        }

        $response = wp_remote_post($url, [
            "headers" => $headers,
            "body" => wp_json_encode($body),
            "timeout" => 10,
        ]);

        // Handle expired token (401) — re-activate and retry once.
        if (!is_wp_error($response)) {
            $code = wp_remote_retrieve_response_code($response);
            if (401 === $code && !empty($agent_id)) {
                // Clear cached token and retry.
                delete_transient("wpaw_memanto_token_" . md5($agent_id));
                $token = $this->activate_session($agent_id);
                if ($token) {
                    $headers["X-Session-Token"] = $token;
                    $response = wp_remote_post($url, [
                        "headers" => $headers,
                        "body" => wp_json_encode($body),
                        "timeout" => 10,
                    ]);
                }
            }
        }

        $result = $this->parse_response($response);
        if (is_wp_error($result)) {
            $this->trip_circuit_breaker();
        }
        return $result;
    }

    /**
     * Build common headers for MEMANTO API requests.
     *
     * @return array Headers.
     */
    private function get_headers()
    {
        return [
            "Content-Type" => "application/json",
            "Accept" => "application/json",
            "X-API-Key" => $this->get_moorcheh_key(),
        ];
    }

    /**
     * Parse HTTP response from MEMANTO API.
     *
     * @param array|WP_Error $response wp_remote response.
     * @return array|WP_Error Decoded body or error.
     */
    private function parse_response($response)
    {
        if (is_wp_error($response)) {
            return new WP_Error(
                "memanto_connection_error",
                $response->get_error_message(),
            );
        }

        $code = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);

        if (401 === $code) {
            // Let caller handle re-auth.
            return new WP_Error(
                "memanto_unauthorized",
                "Session token expired",
            );
        }

        if ($code >= 400) {
            wpaw_debug_log("MEMANTO API error ({$code})", $body);
            return new WP_Error(
                "memanto_api_error",
                sprintf(
                    "MEMANTO API error (%d): %s",
                    $code,
                    substr($body, 0, 200),
                ),
            );
        }

        $decoded = json_decode($body, true);
        return is_array($decoded) ? $decoded : [];
    }
}
