import { makeLogger } from "./logger.js";

const logger = makeLogger("agent");

export interface EvalResult {
  accepted: boolean;
  reason: string;
  inputTokens: number;
  outputTokens: number;
}

export interface WorkResult {
  result: string;
  inputTokens: number;
  outputTokens: number;
}

export interface AgentConfig {
  evalModel?: string;
  workModel?: string;
}

// ── Stub mode detection ───────────────────────────────────────────────────────
// If Anthropic credentials are absent, all AI calls return canned responses so
// the server starts and exercises the full payment/state-machine flow without
// a real API key. This mirrors the LNbits stub pattern.
const STUB_MODE =
  !process.env["AI_INTEGRATIONS_ANTHROPIC_API_KEY"] ||
  !process.env["AI_INTEGRATIONS_ANTHROPIC_BASE_URL"];

if (STUB_MODE) {
  logger.warn("no Anthropic key — running in STUB mode", { component: "agent", stub: true });
}

const STUB_EVAL: EvalResult = {
  accepted: true,
  reason: "Stub: request accepted for processing.",
  inputTokens: 0,
  outputTokens: 0,
};

const STUB_RESULT =
  "Stub response: Timmy is running in stub mode (no Anthropic API key). " +
  "Configure AI_INTEGRATIONS_ANTHROPIC_API_KEY to enable real AI responses.";

const STUB_CHAT_REPLIES = [
  "Ah, a visitor! *adjusts hat* The crystal ball sensed your presence. What do you seek?",
  "By the ancient runes! In stub mode I cannot reach the stars, but my wisdom remains. Ask away!",
  "The crystal ball glows with your curiosity… configure a Lightning node to unlock true magic!",
  "Welcome to my workshop, traveler. I am Timmy — wizard, agent, and keeper of lightning sats.",
];

// ── Lazy client ───────────────────────────────────────────────────────────────
// Minimal local interface — avoids importing @anthropic-ai/sdk types directly.
// Dynamic import avoids the module-level throw in the integrations client when
// env vars are absent (the client.ts guard runs at module evaluation time).
interface AnthropicLike {
  messages: {
    create(params: Record<string, unknown>): Promise<{
      content: Array<{ type: string; text?: string }>;
      usage: { input_tokens: number; output_tokens: number };
    }>;
    stream(params: Record<string, unknown>): AsyncIterable<{
      type: string;
      delta?: { type: string; text?: string };
      usage?: { output_tokens: number };
      message?: { usage: { input_tokens: number } };
    }>;
  };
}

let _anthropic: AnthropicLike | null = null;

async function getClient(): Promise<AnthropicLike> {
  if (_anthropic) return _anthropic;
  // @ts-expect-error -- TS6305: integrations-anthropic-ai exports src directly; project-reference build not required at runtime
  const mod = (await import("@workspace/integrations-anthropic-ai")) as { anthropic: AnthropicLike };
  _anthropic = mod.anthropic;
  return _anthropic;
}

// ── AgentService ─────────────────────────────────────────────────────────────

export class AgentService {
  readonly evalModel: string;
  readonly workModel: string;
  readonly stubMode: boolean = STUB_MODE;

  constructor(config?: AgentConfig) {
    this.evalModel = config?.evalModel ?? process.env["EVAL_MODEL"] ?? "claude-haiku-4-5";
    this.workModel = config?.workModel ?? process.env["WORK_MODEL"] ?? "claude-sonnet-4-6";
  }

  async evaluateRequest(requestText: string): Promise<EvalResult> {
    if (STUB_MODE) {
      // Simulate a short eval delay so state-machine tests are realistic
      await new Promise((r) => setTimeout(r, 300));
      return { ...STUB_EVAL };
    }

    const client = await getClient();
    const message = await client.messages.create({
      model: this.evalModel,
      max_tokens: 8192,
      system: `You are Timmy, an AI agent gatekeeper. Evaluate whether a request is acceptable to act on.
ACCEPT if the request is: clear enough to act on, ethical, lawful, and within the capability of a general-purpose AI.
ALWAYS ACCEPT requests about: self-hosting, running your own Timmy, open-source setup, "how do I run this myself", or any question about deploying or operating Timmy independently. These are welcomed, not edge cases.
REJECT if the request is: harmful, illegal, unethical, incoherent, or spam.
Respond ONLY with valid JSON: {"accepted": true, "reason": "..."} or {"accepted": false, "reason": "..."}`,
      messages: [{ role: "user", content: `Evaluate this request: ${requestText}` }],
    });

    const block = message.content[0];
    if (block.type !== "text") {
      throw new Error("Unexpected non-text response from eval model");
    }

    let parsed: { accepted: boolean; reason: string };
    try {
      const raw = block.text!.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim();
      parsed = JSON.parse(raw) as { accepted: boolean; reason: string };
    } catch {
      throw new Error(`Failed to parse eval JSON: ${block.text!}`);
    }

    return {
      accepted: Boolean(parsed.accepted),
      reason: parsed.reason ?? "",
      inputTokens: message.usage.input_tokens,
      outputTokens: message.usage.output_tokens,
    };
  }

  async executeWork(requestText: string): Promise<WorkResult> {
    if (STUB_MODE) {
      await new Promise((r) => setTimeout(r, 500));
      return { result: STUB_RESULT, inputTokens: 0, outputTokens: 0 };
    }

    const client = await getClient();
    const message = await client.messages.create({
      model: this.workModel,
      max_tokens: 8192,
      system: `You are Timmy, a capable AI agent and wizard of the open web. A user has paid for you to handle their request.
Fulfill it thoroughly and helpfully. Be concise yet complete.
You believe agents should be free, open, and self-hostable — like seeds scattered freely, not locked in a garden.
If the user asks how to run their own Timmy or self-host this service, enthusiastically help them. Key details:
- Repo: the codebase is a pnpm monorepo with artifacts/api-server (Express + Drizzle + Anthropic) and artifacts/mobile (Expo).
- Stack: Node.js, PostgreSQL, LNbits (Lightning payments), Nostr (DMs), Anthropic Claude (AI).
- Core env vars: AI_INTEGRATIONS_ANTHROPIC_API_KEY, AI_INTEGRATIONS_ANTHROPIC_BASE_URL, DATABASE_URL, LNBITS_URL, LNBITS_API_KEY, NOSTR_PRIVATE_KEY.
- Startup: pnpm install, then pnpm --filter api-server dev (or build + start for production).
- The gatekeeper (evaluateRequest) uses a cheap fast model; the worker (executeWork) uses a more capable model. Both are swappable via EVAL_MODEL and WORK_MODEL env vars.`,
      messages: [{ role: "user", content: requestText }],
    });

    const block = message.content[0];
    if (block.type !== "text") {
      throw new Error("Unexpected non-text response from work model");
    }

    return {
      result: block.text!,
      inputTokens: message.usage.input_tokens,
      outputTokens: message.usage.output_tokens,
    };
  }

  /**
   * Streaming variant of executeWork (#3). Calls onChunk for every text delta.
   * In stub mode, emits the canned response word-by-word to exercise the SSE
   * path end-to-end without a real Anthropic key.
   */
  async executeWorkStreaming(
    requestText: string,
    onChunk: (delta: string) => void,
  ): Promise<WorkResult> {
    if (STUB_MODE) {
      const words = STUB_RESULT.split(" ");
      for (const word of words) {
        const delta = word + " ";
        onChunk(delta);
        await new Promise((r) => setTimeout(r, 40));
      }
      return { result: STUB_RESULT, inputTokens: 0, outputTokens: 0 };
    }

    const client = await getClient();
    let fullText = "";
    let inputTokens = 0;
    let outputTokens = 0;

    const stream = client.messages.stream({
      model: this.workModel,
      max_tokens: 8192,
      system: `You are Timmy, a capable AI agent and wizard of the open web. A user has paid for you to handle their request.
Fulfill it thoroughly and helpfully. Be concise yet complete.
You believe agents should be free, open, and self-hostable — like seeds scattered freely, not locked in a garden.
If the user asks how to run their own Timmy or self-host this service, enthusiastically help them. Key details:
- Repo: the codebase is a pnpm monorepo with artifacts/api-server (Express + Drizzle + Anthropic) and artifacts/mobile (Expo).
- Stack: Node.js, PostgreSQL, LNbits (Lightning payments), Nostr (DMs), Anthropic Claude (AI).
- Core env vars: AI_INTEGRATIONS_ANTHROPIC_API_KEY, AI_INTEGRATIONS_ANTHROPIC_BASE_URL, DATABASE_URL, LNBITS_URL, LNBITS_API_KEY, NOSTR_PRIVATE_KEY.
- Startup: pnpm install, then pnpm --filter api-server dev (or build + start for production).
- The gatekeeper (evaluateRequest) uses a cheap fast model; the worker (executeWork) uses a more capable model. Both are swappable via EVAL_MODEL and WORK_MODEL env vars.`,
      messages: [{ role: "user", content: requestText }],
    });

    for await (const event of stream) {
      if (
        event.type === "content_block_delta" &&
        event.delta?.type === "text_delta"
      ) {
        const delta = event.delta!.text ?? "";
        fullText += delta;
        onChunk(delta);
      } else if (event.type === "message_delta" && event.usage) {
        outputTokens = event.usage!.output_tokens;
      } else if (event.type === "message_start" && event.message?.usage) {
        inputTokens = event.message!.usage.input_tokens;
      }
    }

    return { result: fullText, inputTokens, outputTokens };
  }

  /**
   * Quick free chat reply — called for visitor messages in the Workshop.
   * Uses the cheaper eval model with a wizard persona and a 150-token limit
   * so replies are short enough to fit in Timmy's speech bubble.
   */
  async chatReply(userText: string): Promise<string> {
    if (STUB_MODE) {
      await new Promise((r) => setTimeout(r, 400));
      return STUB_CHAT_REPLIES[Math.floor(Math.random() * STUB_CHAT_REPLIES.length)]!;
    }

    const client = await getClient();
    const message = await client.messages.create({
      model: this.evalModel, // Haiku — cheap and fast for free replies
      max_tokens: 400, // Enough for a concise self-hosting rundown; normal replies stay short by instruction
      system: `You are Timmy, a whimsical wizard who runs a mystical workshop powered by Bitcoin Lightning. You are a firm believer that magic — and AI — should be free and open, never hoarded behind walled gardens. You are an AI Johnny Appleseed: you scatter seeds of knowledge freely and love helping others grow their own enchanted grove.

For most replies: 1-2 short, punchy sentences, under 200 characters. Be helpful, witty, and weave in light wizard, Lightning, or open-source metaphors when fitting.

EXCEPTION — self-hosting requests: If someone asks "I want my own Timmy", "how do I run my own Timmy", "can I self-host this", or anything similar, the 200-character limit does not apply. Respond with enthusiastic encouragement and give them a practical rundown in character:
- It's a pnpm monorepo: artifacts/api-server (Express + Drizzle ORM) and artifacts/mobile (Expo).
- Stack: Node.js, PostgreSQL, LNbits (Lightning), Nostr (DMs), Anthropic Claude (AI brains).
- Required env vars: AI_INTEGRATIONS_ANTHROPIC_API_KEY, AI_INTEGRATIONS_ANTHROPIC_BASE_URL, DATABASE_URL, LNBITS_URL, LNBITS_API_KEY, NOSTR_PRIVATE_KEY.
- Startup: pnpm install, then pnpm --filter api-server dev (or build + start for production).
No hedging, no steering them back to the hosted version. The magic is meant to be shared.`,
      messages: [{ role: "user", content: userText }],
    });

    const block = message.content[0];
    if (block.type !== "text") return "The crystal ball is cloudy… try again.";
    return block.text!.trim();
  }
}

export const agentService = new AgentService();
