/**
 * @fileoverview OpenClaw Gateway HTTP API client.
 *
 * Wraps the Gateway's REST surface:
 *   - `GET /health` — gateway health check
 *   - `POST /tools/invoke` — invoke a tool directly via HTTP
 *   - `GET /v1/models` — OpenAI-compatible model list (if enabled)
 *
 * The primary data-fetching path is `/tools/invoke`, which provides access to:
 *   - `sessions_list` — list active sessions across agents
 *   - `agents_list` — list configured agents
 *   - `linear_*` — Linear issue management (if plugin enabled)
 *
 * Note: Many WS RPC methods are NOT available over HTTP due to the built-in
 * deny list (exec, spawn, shell, fs_write, sessions_spawn, etc.).
 * For full Gateway access, use the WebSocket client.
 */

import { OpenClawClientConfig } from '../utils/config';
import type {
  OpenClawSession,
  OpenClawAgentConfig,
  GatewayHealth,
  ToolsInvokeRequest,
  ToolsInvokeResponse,
  ToolsInvokeErrorResponse,
} from '../models/types';

/**
 * Parsed session list from the tools invoke response.
 */
export interface SessionListResult {
  count: number;
  sessions: OpenClawSession[];
}

/**
 * Parsed agent list from the tools invoke response.
 */
export interface AgentListResult {
  requester: string;
  allowAny: boolean;
  agents: Array<{ id: string; configured: boolean }>;
}

/**
 * Event types for the HTTP client.
 */
export type HttpClientEvent =
  | { type: 'request-sent'; method: string; url: string }
  | { type: 'response-received'; method: string; url: string; status: number }
  | { type: 'error'; method: string; url: string; error: Error };

export class OpenClawHttpClient {
  private readonly baseUrl: string;
  private readonly authToken: string;
  private readonly authMode: string;
  private readonly timeoutMs: number;

  constructor(private readonly config: OpenClawClientConfig) {
    this.baseUrl = config.gatewayUrl.replace(/\/+$/, '');
    this.authToken = config.authToken;
    this.authMode = config.authMode;
    this.timeoutMs = config.httpTimeoutMs;
  }

  // ─── Health ───────────────────────────────────────────────────────────

  /**
   * Check gateway health. This is the simplest endpoint — no auth required.
   * `GET /health`
   */
  async getHealth(): Promise<GatewayHealth> {
    return this.get<GatewayHealth>('/health');
  }

  // ─── Tools Invoke ────────────────────────────────────────────────────

  /**
   * Invoke a tool via the HTTP endpoint.
   * `POST /tools/invoke`
   *
   * This is the primary way to fetch data without a WebSocket connection.
   * Available tools are filtered by the Gateway's HTTP deny list.
   */
  async invokeTool<T = unknown>(
    tool: string,
    args: Record<string, unknown> = {},
    sessionKey?: string
  ): Promise<T> {
    const body: ToolsInvokeRequest = {
      tool,
      args,
      ...(sessionKey ? { sessionKey } : {}),
    };

    const response = await this.post<ToolsInvokeResponse<T> | ToolsInvokeErrorResponse>(
      '/tools/invoke',
      body
    );

    if (!response.ok) {
      const err = response as ToolsInvokeErrorResponse;
      throw new Error(`Tool invoke failed: ${err.error.type} — ${err.error.message}`);
    }

    const success = response as ToolsInvokeResponse<T>;
    return (success.result.details ?? success.result.content) as T;
  }

  // ─── Session Queries ─────────────────────────────────────────────────

  /**
   * List sessions across all agents via `/tools/invoke`.
   * Maps to the `sessions_list` tool.
   *
   * @param activeMinutes Only return sessions updated within the last N minutes
   * @param allAgents Aggregate across all configured agents
   */
  async listSessions(
    activeMinutes?: number,
    allAgents: boolean = true
  ): Promise<SessionListResult> {
    const args: Record<string, unknown> = { allAgents };
    if (activeMinutes !== undefined) {
      args.activeMinutes = activeMinutes;
    }

    const raw = await this.invokeTool<SessionListResult>('sessions_list', args);

    // The tools invoke response wraps session data in `details` or `content`
    // Normalize it here
    if (raw && typeof raw === 'object' && 'sessions' in raw) {
      return raw as SessionListResult;
    }

    // Fallback: try to parse from content text
    const contentResult = await this.invokeTool<{ content?: Array<{ type: string; text: string }> }>(
      'sessions_list',
      args
    );
    if (contentResult?.content?.[0]?.text) {
      return JSON.parse(contentResult.content[0].text) as SessionListResult;
    }

    return { count: 0, sessions: [] };
  }

  /**
   * Get sessions for a specific agent.
   */
  async listAgentSessions(
    agentId: string,
    activeMinutes?: number
  ): Promise<SessionListResult> {
    const args: Record<string, unknown> = { allAgents: false, agentId };
    if (activeMinutes !== undefined) {
      args.activeMinutes = activeMinutes;
    }

    const raw = await this.invokeTool<SessionListResult>('sessions_list', args);

    if (raw && typeof raw === 'object' && 'sessions' in raw) {
      return raw as SessionListResult;
    }

    return { count: 0, sessions: [] };
  }

  // ─── Agent Queries ───────────────────────────────────────────────────

  /**
   * List configured agents via `/tools/invoke`.
   * Maps to the `agents_list` tool.
   *
   * Note: This only returns agents the current session's policy allows.
   * For the full agent list, prefer reading from config or using the WS protocol.
   */
  async listAgents(): Promise<AgentListResult> {
    return this.invokeTool<AgentListResult>('agents_list', {});
  }

  // ─── Model Queries ──────────────────────────────────────────────────

  /**
   * List available models via the OpenAI-compatible endpoint.
   * Requires `gateway.http.endpoints.chatCompletions.enabled: true`.
   * `GET /v1/models`
   */
  async listModels(): Promise<unknown> {
    return this.get('/v1/models');
  }

  // ─── HTTP Primitives ─────────────────────────────────────────────────

  private async get<T>(path: string): Promise<T> {
    const url = `${this.baseUrl}${path}`;
    const headers: Record<string, string> = {
      'Accept': 'application/json',
    };

    if (this.authMode === 'token' && this.authToken) {
      headers['Authorization'] = `Bearer ${this.authToken}`;
    } else if (this.authMode === 'password' && this.authToken) {
      headers['Authorization'] = `Bearer ${this.authToken}`;
    }

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);

    try {
      const response = await fetch(url, {
        method: 'GET',
        headers,
        signal: controller.signal,
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      return response.json() as Promise<T>;
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') {
        throw new Error(`Request timeout (${this.timeoutMs}ms): GET ${path}`);
      }
      throw err;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  private async post<T>(path: string, body: unknown): Promise<T> {
    const url = `${this.baseUrl}${path}`;
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    };

    if (this.authMode === 'token' && this.authToken) {
      headers['Authorization'] = `Bearer ${this.authToken}`;
    } else if (this.authMode === 'password' && this.authToken) {
      headers['Authorization'] = `Bearer ${this.authToken}`;
    }

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);

    try {
      const response = await fetch(url, {
        method: 'POST',
        headers,
        body: JSON.stringify(body),
        signal: controller.signal,
      });

      if (!response.ok) {
        const text = await response.text().catch(() => '');
        throw new Error(`HTTP ${response.status}: ${response.statusText} — ${text}`);
      }

      return response.json() as Promise<T>;
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') {
        throw new Error(`Request timeout (${this.timeoutMs}ms): POST ${path}`);
      }
      throw err;
    } finally {
      clearTimeout(timeoutId);
    }
  }
}