/**
 * Chat orchestrator.
 *
 * Exports:
 *   sendChat(req)          — non-streaming, no tools (used by tests, fallback paths)
 *   streamChat(req, cb)    — streaming + tool calling via OpenRouter (Pattern C)
 *
 * CHAT_PROVIDER controls which path:
 *   'openrouter' (default for Pattern C) — full tools + streaming
 *   'claude-code' — simple Q&A via OAuth subprocess, NO tools
 *   'auto' — claude-code first; on rate-limit/error fall back to OpenRouter (no tools)
 */
import { claudeCodeProvider } from "./claude-code";
import { sendOnce, streamWithTools } from "./openrouter";
import { createEventStream } from "./agui";
import type { ToolHandlerContext } from "./tools";

export type Provider = "claude-code" | "openrouter";

const MODE = (process.env.CHAT_PROVIDER || "openrouter") as Provider | "auto";

/* ─── Non-streaming (legacy/fallback) ───────────────────────────────────── */

export interface SendChatReq {
  system: string;
  messages: Array<{ role: "user" | "assistant" | "system"; content: string }>;
  maxTokens?: number;
}

export interface SendChatResp {
  provider: Provider;
  model: string;
  content: string;
  tokensIn?: number;
  tokensOut?: number;
  costUsd?: number;
  durationMs: number;
}

export async function sendChat(req: SendChatReq): Promise<SendChatResp> {
  const t0 = Date.now();

  async function viaOpenRouter(): Promise<SendChatResp> {
    const r = await sendOnce({
      system: req.system,
      messages: req.messages,
      maxTokens: req.maxTokens,
    });
    return {
      provider: "openrouter",
      model: r.model,
      content: r.content,
      tokensIn: r.tokensIn,
      tokensOut: r.tokensOut,
      costUsd: 0,
      durationMs: Date.now() - t0,
    };
  }

  if (MODE === "openrouter") return viaOpenRouter();

  if (MODE === "claude-code") {
    if (!claudeCodeProvider.isAvailable()) {
      throw new Error("claude-code mode but CLAUDE_CODE_OAUTH_TOKEN not set");
    }
    const r = await claudeCodeProvider.send({
      system: req.system,
      messages: req.messages,
      maxTokens: req.maxTokens,
    });
    return { ...r, durationMs: Date.now() - t0 };
  }

  // auto
  if (claudeCodeProvider.isAvailable()) {
    try {
      const r = await claudeCodeProvider.send({
        system: req.system,
        messages: req.messages,
        maxTokens: req.maxTokens,
      });
      return { ...r, durationMs: Date.now() - t0 };
    } catch (e) {
      const isRate = (e as Error & { isRateLimit?: boolean }).isRateLimit;
      if (isRate || /401|403|oauth|token/i.test((e as Error).message)) {
        return viaOpenRouter();
      }
      throw e;
    }
  }
  return viaOpenRouter();
}

/* ─── Streaming + tool calling (Pattern C) ──────────────────────────────── */

export interface StreamChatReq {
  system: string;
  history: Array<{ role: "user" | "assistant"; content: string }>;
  userTurn: string;
  ctx: ToolHandlerContext;
}

export interface StreamChatResult {
  stream: ReadableStream<Uint8Array>;
  /** Resolves AFTER the stream completes — usable in a deferred persist step. */
  done: Promise<{
    content: string;
    model: string;
    tokensIn: number;
    tokensOut: number;
    toolCalls: Array<{ name: string; args: Record<string, unknown>; result: unknown }>;
    artifacts: import("./agui").Artifact[];
  }>;
}

/**
 * Returns immediately with a ReadableStream the caller can pipe to Response.
 * The `done` promise resolves when the full conversation (including all tool
 * rounds) is finished — so the caller can then persist the assistant message
 * to the database.
 */
export function streamChat(req: StreamChatReq): StreamChatResult {
  const { stream, emit, close } = createEventStream();

  const done = (async () => {
    try {
      const result = await streamWithTools(
        {
          system: req.system,
          history: req.history,
          userTurn: req.userTurn,
          ctx: req.ctx,
        },
        { emit },
      );
      emit({
        type: "done",
        provider: "openrouter",
        model: result.model,
        usage: {
          tokens_in: result.tokensIn,
          tokens_out: result.tokensOut,
          tool_calls: result.toolCalls.length,
        },
      });
      close();
      return result;
    } catch (e) {
      emit({ type: "error", message: e instanceof Error ? e.message : String(e) });
      close();
      throw e;
    }
  })();

  return { stream, done };
}
