import crypto from "node:crypto";
import type { Hono } from "hono";
import { z } from "zod";
import * as agentStore from "../agent-store.js";
import type { AgentExecutor } from "../agent-executor.js";
import type { AgentConfig, AgentConfigExport } from "../agent-types.js";
import { parseBody } from "../validation.js";
import { notFound } from "../errors.js";
import { RateLimiter, getClientIp } from "../rate-limiter.js";

// 10 requests per 60 seconds, keyed by agentId + source IP
const webhookRateLimiter = new RateLimiter(10, 60_000);
webhookRateLimiter.startCleanup();

// ─── Zod Schemas ──────────────────────────────────────────────────

const mcpServerSchema = z.object({
  type: z.enum(["stdio", "sse", "http"]),
  command: z.string().optional(),
  args: z.array(z.string()).optional(),
  url: z.string().optional(),
  env: z.record(z.string(), z.string()).optional(),
});

const containerSchema = z.object({
  image: z.string().optional(),
  ports: z.array(z.number()).optional(),
  volumes: z.array(z.string()).optional(),
  initScript: z.string().optional(),
});

const agentTriggersSchema = z.object({
  schedule: z.object({
    enabled: z.boolean(),
    expression: z.string().max(200),
    recurring: z.boolean(),
  }).optional(),
  webhook: z.object({
    enabled: z.boolean(),
    secret: z.string().default(''),
  }).optional(),
}).optional();

const agentCreateSchema = z.object({
  name: z.string().max(200).optional().default(""),
  description: z.string().max(2000).optional().default(""),
  icon: z.string().max(100).optional(),
  version: z.literal(1).optional().default(1),
  backendType: z.enum(["claude", "codex"]).optional().default("claude"),
  model: z.string().max(200).optional().default(""),
  permissionMode: z.string().max(100).optional().default("bypassPermissions"),
  cwd: z.string().max(1000).optional().default(""),
  envSlug: z.string().max(200).optional(),
  env: z.record(z.string(), z.string()).optional(),
  allowedTools: z.array(z.string()).optional(),
  codexInternetAccess: z.boolean().optional(),
  prompt: z.string().max(200_000).optional().default(""),
  mcpServers: z.record(z.string(), mcpServerSchema).optional(),
  skills: z.array(z.string()).optional(),
  container: containerSchema.optional(),
  branch: z.string().max(500).optional(),
  createBranch: z.boolean().optional(),
  useWorktree: z.boolean().optional(),
  triggers: agentTriggersSchema,
  enabled: z.boolean().optional().default(true),
});

const agentUpdateSchema = z.object({
  name: z.string().max(200).optional(),
  description: z.string().max(2000).optional(),
  icon: z.string().max(100).optional(),
  version: z.literal(1).optional(),
  backendType: z.enum(["claude", "codex"]).optional(),
  model: z.string().max(200).optional(),
  permissionMode: z.string().max(100).optional(),
  cwd: z.string().max(1000).optional(),
  envSlug: z.string().max(200).optional(),
  env: z.record(z.string(), z.string()).optional(),
  allowedTools: z.array(z.string()).optional(),
  codexInternetAccess: z.boolean().optional(),
  prompt: z.string().max(200_000).optional(),
  mcpServers: z.record(z.string(), mcpServerSchema).optional(),
  skills: z.array(z.string()).optional(),
  container: containerSchema.optional(),
  branch: z.string().max(500).optional(),
  createBranch: z.boolean().optional(),
  useWorktree: z.boolean().optional(),
  triggers: agentTriggersSchema,
  enabled: z.boolean().optional(),
});

const agentRunSchema = z.object({
  input: z.string().max(100_000).optional(),
}).optional().default({});

/** Fields the user can set when creating/updating an agent */
const EDITABLE_FIELDS = [
  "name", "description", "icon", "version",
  "backendType", "model", "permissionMode", "cwd",
  "envSlug", "env", "allowedTools", "codexInternetAccess",
  "prompt", "mcpServers", "skills",
  "container", "branch", "createBranch", "useWorktree",
  "triggers", "enabled",
] as const;

function pickEditable(body: Record<string, unknown>): Partial<AgentConfig> {
  const result: Record<string, unknown> = {};
  for (const key of EDITABLE_FIELDS) {
    if (key in body) result[key] = body[key];
  }
  return result as Partial<AgentConfig>;
}

/** Strip internal tracking fields to produce a portable export */
function toExport(agent: AgentConfig): AgentConfigExport {
  const {
    id: _id,
    createdAt: _ca,
    updatedAt: _ua,
    totalRuns: _tr,
    consecutiveFailures: _cf,
    lastRunAt: _lr,
    lastSessionId: _ls,
    enabled: _en,
    ...exportable
  } = agent;
  return exportable;
}

export function registerAgentRoutes(
  api: Hono,
  agentExecutor?: AgentExecutor,
): void {
  // ── CRUD ────────────────────────────────────────────────────────────────

  api.get("/agents", (c) => {
    const agents = agentStore.listAgents();
    const enriched = agents.map((a) => ({
      ...a,
      nextRunAt: agentExecutor?.getNextRunTime(a.id)?.getTime() ?? null,
    }));
    return c.json(enriched);
  });

  api.get("/agents/:id", (c) => {
    const agent = agentStore.getAgent(c.req.param("id"));
    if (!agent) throw notFound("Agent not found");
    return c.json({
      ...agent,
      nextRunAt: agentExecutor?.getNextRunTime(agent.id)?.getTime() ?? null,
    });
  });

  api.post("/agents", async (c) => {
    const parsed = await parseBody(c, agentCreateSchema);
    if (!parsed.success) return parsed.response;
    const body = parsed.data;
    try {
      const agent = agentStore.createAgent({
        version: body.version,
        name: body.name,
        description: body.description,
        icon: body.icon,
        backendType: body.backendType,
        model: body.model,
        permissionMode: body.permissionMode,
        cwd: body.cwd,
        envSlug: body.envSlug,
        env: body.env,
        allowedTools: body.allowedTools,
        codexInternetAccess: body.codexInternetAccess,
        prompt: body.prompt,
        mcpServers: body.mcpServers,
        skills: body.skills,
        container: body.container,
        branch: body.branch,
        createBranch: body.createBranch,
        useWorktree: body.useWorktree,
        triggers: body.triggers,
        enabled: body.enabled,
      });
      if (agent.enabled && agent.triggers?.schedule?.enabled) {
        agentExecutor?.scheduleAgent(agent);
      }
      return c.json(agent, 201);
    } catch (e: unknown) {
      return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
    }
  });

  api.put("/agents/:id", async (c) => {
    const id = c.req.param("id");
    const parsed = await parseBody(c, agentUpdateSchema);
    if (!parsed.success) return parsed.response;
    try {
      const allowed = pickEditable(parsed.data as Record<string, unknown>);
      const agent = agentStore.updateAgent(id, allowed);
      if (!agent) return c.json({ error: "Agent not found" }, 404);
      // Stop old timer (id may differ after a rename)
      if (agent.id !== id) agentExecutor?.stopAgent(id);
      // Reschedule if enabled
      if (agent.enabled && agent.triggers?.schedule?.enabled) {
        agentExecutor?.scheduleAgent(agent);
      } else {
        agentExecutor?.stopAgent(agent.id);
      }
      return c.json(agent);
    } catch (e: unknown) {
      return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
    }
  });

  api.delete("/agents/:id", (c) => {
    const id = c.req.param("id");
    agentExecutor?.stopAgent(id);
    const deleted = agentStore.deleteAgent(id);
    if (!deleted) return c.json({ error: "Agent not found" }, 404);
    return c.json({ ok: true });
  });

  // ── Toggle ──────────────────────────────────────────────────────────────

  api.post("/agents/:id/toggle", (c) => {
    const id = c.req.param("id");
    const agent = agentStore.getAgent(id);
    if (!agent) return c.json({ error: "Agent not found" }, 404);
    const updated = agentStore.updateAgent(id, { enabled: !agent.enabled });
    if (updated?.enabled && updated.triggers?.schedule?.enabled) {
      agentExecutor?.scheduleAgent(updated);
    } else if (updated) {
      agentExecutor?.stopAgent(updated.id);
    }
    return c.json(updated);
  });

  // ── Run (manual trigger) ───────────────────────────────────────────────

  api.post("/agents/:id/run", async (c) => {
    const id = c.req.param("id");
    const agent = agentStore.getAgent(id);
    if (!agent) return c.json({ error: "Agent not found" }, 404);
    const parsed = await parseBody(c, agentRunSchema);
    if (!parsed.success) return parsed.response;
    const input = parsed.data.input;
    agentExecutor?.executeAgentManually(id, input);
    return c.json({ ok: true, message: "Agent triggered" });
  });

  // ── Executions ─────────────────────────────────────────────────────────

  api.get("/agents/:id/executions", (c) => {
    const id = c.req.param("id");
    return c.json(agentExecutor?.getExecutions(id) ?? []);
  });

  // ── Import / Export ────────────────────────────────────────────────────

  api.post("/agents/import", async (c) => {
    const parsed = await parseBody(c, agentCreateSchema);
    if (!parsed.success) return parsed.response;
    const body = parsed.data;
    try {
      // Accept an exported agent JSON and create a new agent from it
      const agent = agentStore.createAgent({
        version: body.version,
        name: body.name,
        description: body.description,
        icon: body.icon,
        backendType: body.backendType,
        model: body.model,
        permissionMode: body.permissionMode,
        cwd: body.cwd,
        envSlug: body.envSlug,
        env: body.env,
        allowedTools: body.allowedTools,
        codexInternetAccess: body.codexInternetAccess,
        prompt: body.prompt,
        mcpServers: body.mcpServers,
        skills: body.skills,
        container: body.container,
        branch: body.branch,
        createBranch: body.createBranch,
        useWorktree: body.useWorktree,
        triggers: body.triggers,
        enabled: false, // Imported agents start disabled for safety
      });
      return c.json(agent, 201);
    } catch (e: unknown) {
      return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
    }
  });

  api.get("/agents/:id/export", (c) => {
    const agent = agentStore.getAgent(c.req.param("id"));
    if (!agent) return c.json({ error: "Agent not found" }, 404);
    return c.json(toExport(agent));
  });

  // ── Webhook Test ──────────────────────────────────────────────────────

  api.post("/agents/:id/webhook/test", (c) => {
    const id = c.req.param("id");
    const agent = agentStore.getAgent(id);
    if (!agent) return c.json({ error: "Agent not found" }, 404);

    if (!agent.triggers?.webhook?.enabled) {
      return c.json({ error: "Webhook not enabled for this agent" }, 400);
    }

    if (!agentExecutor) {
      return c.json({ error: "Agent executor not available" }, 503);
    }

    agentExecutor.executeAgentManually(id);
    return c.json({ ok: true, message: "Test webhook triggered" });
  });

  // ── Webhook Secret ─────────────────────────────────────────────────────

  api.post("/agents/:id/regenerate-secret", (c) => {
    const id = c.req.param("id");
    const agent = agentStore.regenerateWebhookSecret(id);
    if (!agent) return c.json({ error: "Agent not found" }, 404);
    return c.json(agent);
  });

  // ── Webhook Trigger ────────────────────────────────────────────────────

  api.post("/agents/:id/webhook/:secret", async (c) => {
    const id = c.req.param("id");
    const secret = c.req.param("secret");

    // Rate limit by agent ID + source IP to prevent brute-force attacks
    const rateLimitKey = `${id}:${getClientIp(c)}`;
    const rateLimitResult = webhookRateLimiter.check(rateLimitKey);
    if (!rateLimitResult.allowed) {
      const retryAfter = Math.ceil((rateLimitResult.resetAt - Date.now()) / 1000);
      c.header("Retry-After", String(Math.max(1, retryAfter)));
      return c.json({ error: "Too many requests. Please try again later." }, 429);
    }

    const agent = agentStore.getAgent(id);
    if (!agent) return c.json({ error: "Agent not found" }, 404);

    // Validate webhook is enabled and secret matches
    if (!agent.triggers?.webhook?.enabled) {
      return c.json({ error: "Webhook not enabled for this agent" }, 403);
    }
    // Use constant-time comparison to prevent timing attacks
    const expected = Buffer.from(agent.triggers.webhook.secret);
    const received = Buffer.from(secret);
    if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) {
      return c.json({ error: "Invalid webhook secret" }, 401);
    }

    // Extract input from body — accept JSON { input: "..." } or plain text
    let input: string | undefined;
    const contentType = c.req.header("content-type") || "";
    if (contentType.includes("application/json")) {
      const body = await c.req.json().catch(() => ({}));
      input = typeof body.input === "string" ? body.input : undefined;
    } else {
      const text = await c.req.text().catch(() => "");
      if (text.trim()) input = text.trim();
    }

    agentExecutor?.executeAgentManually(id, input);
    return c.json({ ok: true, message: "Agent triggered via webhook" });
  });
}
