// ============================================================================
// H33 Agent SDK — Main client
// ============================================================================

import type {
  AgentReceipt,
  RegisterAgentRequest,
  StartSessionRequest,
  AttestActionRequest,
  AttestToolRequest,
  MemoryCheckpointRequest,
  DelegateRequest,
  PolicyEvaluateRequest,
  ExposureCheckRequest,
  ApprovalRequest,
  ReplayRequest,
  ReplayForkRequest,
  LineageQueryRequest,
  ProofResponse,
  ReplayResponse,
  LineageResponse,
  H33Error,
} from './types.js';

/** Options for constructing the H33AgentClient */
export interface H33ClientOptions {
  /** API key. Falls back to process.env.H33_API_KEY */
  apiKey?: string;
  /** Base URL. Defaults to https://api.h33.ai */
  baseUrl?: string;
  /** Request timeout in milliseconds. Defaults to 30000 */
  timeoutMs?: number;
}

/**
 * H33 Agent Client — cryptographic attestation for AI agents, tools, and
 * autonomous systems.
 *
 * Every mutating call returns an `AgentReceipt` that is independently
 * verifiable at the public proof endpoint (no API key required).
 *
 * @example
 * ```ts
 * const h33 = new H33AgentClient({ apiKey: process.env.H33_API_KEY });
 * const agent = await h33.register({ ... });
 * ```
 */
export class H33AgentClient {
  private readonly baseUrl: string;
  private readonly apiKey: string;
  private readonly timeoutMs: number;

  constructor(options: H33ClientOptions = {}) {
    this.apiKey = options.apiKey || process.env.H33_API_KEY || '';
    this.baseUrl = (options.baseUrl || 'https://api.h33.ai').replace(/\/+$/, '');
    this.timeoutMs = options.timeoutMs ?? 30_000;
  }

  // ── Agent Identity ───────────────────────────────────────────────────────

  /**
   * Register a new agent identity. Returns a receipt containing the
   * assigned `agent_id`.
   */
  async register(req: RegisterAgentRequest): Promise<AgentReceipt> {
    return this.post<AgentReceipt>('/api/v1/agents/register', req);
  }

  // ── Sessions ─────────────────────────────────────────────────────────────

  /**
   * Start an attested session. All subsequent actions reference this
   * session and form a hash-chained DAG.
   */
  async startSession(req: StartSessionRequest): Promise<AgentReceipt> {
    return this.post<AgentReceipt>('/api/v1/agents/sessions/start', req);
  }

  /**
   * End an active session. Seals the session chain and produces a final
   * receipt.
   */
  async endSession(sessionId: string, summary?: string): Promise<AgentReceipt> {
    return this.post<AgentReceipt>('/api/v1/agents/sessions/end', {
      session_id: sessionId,
      summary,
    });
  }

  // ── Actions ──────────────────────────────────────────────────────────────

  /**
   * Attest an arbitrary action. The `input_hash` should be a SHA-256 of the
   * action's input payload; never send raw content to the API.
   */
  async attestAction(req: AttestActionRequest): Promise<AgentReceipt> {
    return this.post<AgentReceipt>('/api/v1/agents/actions/attest', req);
  }

  /**
   * Attest a tool call. Use `hashToolRequest()` / `hashToolResponse()` from
   * `@h33/agent` to produce the hashes.
   */
  async attestTool(req: AttestToolRequest): Promise<AgentReceipt> {
    return this.post<AgentReceipt>('/api/v1/agents/tools/attest', req);
  }

  /**
   * Checkpoint the current memory / context state. Use `hashMemoryState()`
   * or `hashContextWindow()` from `@h33/agent` to produce the hashes.
   */
  async checkpointMemory(req: MemoryCheckpointRequest): Promise<AgentReceipt> {
    return this.post<AgentReceipt>('/api/v1/agents/memory/checkpoint', req);
  }

  // ── Governance ───────────────────────────────────────────────────────────

  /**
   * Evaluate a policy before performing an action. Returns the receipt AND
   * the policy decision embedded in the response.
   */
  async evaluatePolicy(req: PolicyEvaluateRequest): Promise<AgentReceipt> {
    return this.post<AgentReceipt>('/api/v1/agents/policy/evaluate', req);
  }

  /**
   * Check for data exposure before accessing a classified resource.
   */
  async checkExposure(req: ExposureCheckRequest): Promise<AgentReceipt> {
    return this.post<AgentReceipt>('/api/v1/agents/exposure/check', req);
  }

  /**
   * Request human-in-the-loop approval for a sensitive action.
   */
  async requestApproval(req: ApprovalRequest): Promise<AgentReceipt> {
    return this.post<AgentReceipt>('/api/v1/agents/approval/request', req);
  }

  /**
   * Delegate capabilities to a sub-agent. Creates a scoped child session
   * with the specified permissions.
   */
  async delegate(req: DelegateRequest): Promise<AgentReceipt> {
    return this.post<AgentReceipt>('/api/v1/agents/delegate', req);
  }

  // ── Verification (PUBLIC -- no auth needed) ──────────────────────────────

  /**
   * Retrieve a cryptographic proof for any receipt by node ID. This is a
   * PUBLIC endpoint -- no API key is sent. Anyone with a node ID can
   * independently verify it.
   */
  async getProof(nodeId: string): Promise<ProofResponse> {
    return this.get<ProofResponse>(`/api/v1/agents/proof/${encodeURIComponent(nodeId)}`, false);
  }

  // ── Replay ───────────────────────────────────────────────────────────────

  /**
   * Replay a session's action chain. Returns an ordered list of receipts
   * with chain integrity verification.
   */
  async replay(req: ReplayRequest): Promise<ReplayResponse> {
    return this.post<ReplayResponse>('/api/v1/agents/replay', req);
  }

  /**
   * Fork a session at a specific node. Creates a new session branching
   * from the specified point in the original chain.
   */
  async replayFork(req: ReplayForkRequest): Promise<ReplayResponse> {
    return this.post<ReplayResponse>('/api/v1/agents/replay/fork', req);
  }

  /**
   * Query the lineage DAG for a given node. Returns ancestors, descendants,
   * or both depending on the direction parameter.
   */
  async queryLineage(req: LineageQueryRequest): Promise<LineageResponse> {
    return this.post<LineageResponse>('/api/v1/agents/lineage/query', req);
  }

  // ── Internal HTTP ────────────────────────────────────────────────────────

  /** POST with JSON body and auth header */
  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.apiKey) {
      headers['Authorization'] = `Bearer ${this.apiKey}`;
    }

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

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

      if (!res.ok) {
        await this.handleError(res);
      }

      return (await res.json()) as T;
    } catch (err: unknown) {
      if (err instanceof Error && err.name === 'AbortError') {
        throw new Error(`H33 API request timed out after ${this.timeoutMs}ms: POST ${path}`);
      }
      throw err;
    } finally {
      clearTimeout(timer);
    }
  }

  /** GET with optional auth header */
  private async get<T>(path: string, sendAuth: boolean = true): Promise<T> {
    const url = `${this.baseUrl}${path}`;
    const headers: Record<string, string> = {
      'Accept': 'application/json',
    };
    if (sendAuth && this.apiKey) {
      headers['Authorization'] = `Bearer ${this.apiKey}`;
    }

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

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

      if (!res.ok) {
        await this.handleError(res);
      }

      return (await res.json()) as T;
    } catch (err: unknown) {
      if (err instanceof Error && err.name === 'AbortError') {
        throw new Error(`H33 API request timed out after ${this.timeoutMs}ms: GET ${path}`);
      }
      throw err;
    } finally {
      clearTimeout(timer);
    }
  }

  /** Parse error response and throw a structured error */
  private async handleError(res: Response): Promise<never> {
    let body: H33Error | null = null;
    try {
      body = (await res.json()) as H33Error;
    } catch {
      // response was not JSON
    }

    const message = body?.message || `HTTP ${res.status} ${res.statusText}`;
    const code = body?.code || 'UNKNOWN';
    const requestId = body?.request_id;

    const err = new Error(
      `H33 API error [${code}]: ${message}${requestId ? ` (request_id: ${requestId})` : ''}`
    );
    (err as any).status = res.status;
    (err as any).code = code;
    (err as any).requestId = requestId;
    throw err;
  }
}
