/**
 * Workflow API Routes - CRUD + execution control.
 *
 * Ported from Python apps/server/routers/workflows.py
 */

import { zValidator } from "@hono/zod-validator";
import { desc, eq } from "drizzle-orm";
import { Hono } from "hono";
import { z } from "zod";
import { db as defaultDb } from "../db/database";
import { workflows } from "../db/schema";
import type { WorkflowExecutor } from "../engine/executor";
import { validateWorkflow } from "../engine/validator";
import type { WorkflowResponse } from "../models/workflow";
import type { WSBroadcaster } from "../websocket/handler";

const app = new Hono();

// Shared instances (set by main or tests)
let _db: typeof defaultDb = defaultDb;
let executor: WorkflowExecutor;
let wsBroadcaster: WSBroadcaster | null = null;

export function setDb(newDb: typeof defaultDb): void {
  _db = newDb;
}

export function setExecutor(exec: WorkflowExecutor): void {
  executor = exec;
}

export function setWSBroadcaster(broadcaster: WSBroadcaster): void {
  wsBroadcaster = broadcaster;
}

// ─── Validation Schemas ──────────────────────

const createWorkflowBody = z.object({
  name: z.string({ required_error: "name is required" }),
  description: z.string().optional(),
  nodes: z.array(z.record(z.string(), z.unknown())).default([]),
  connections: z.array(z.record(z.string(), z.unknown())).default([]),
  character: z.record(z.string(), z.unknown()).optional(),
});

// ─── Helpers ──────────────────────────────

const SENSITIVE_KEYS = ["apiKey", "api_key", "password", "secret", "token", "apiSecret"];
const SENSITIVE_KEYS_LOWER = new Set(SENSITIVE_KEYS.map((k) => k.toLowerCase()));
const MAX_STRIP_DEPTH = 20;

/**
 * Recursively strip sensitive fields (api keys, tokens, passwords) from a
 * value. Walks nested objects and arrays up to MAX_STRIP_DEPTH to guard against
 * pathological inputs. Returns a new structure; input is not mutated.
 */
function deepStripSensitive(value: unknown, depth = 0): unknown {
  if (depth >= MAX_STRIP_DEPTH) return value;
  if (Array.isArray(value)) {
    return value.map((item) => deepStripSensitive(item, depth + 1));
  }
  if (value && typeof value === "object") {
    const out: Record<string, unknown> = {};
    for (const [key, inner] of Object.entries(value as Record<string, unknown>)) {
      if (SENSITIVE_KEYS_LOWER.has(key.toLowerCase())) {
        out[key] = "";
      } else {
        out[key] = deepStripSensitive(inner, depth + 1);
      }
    }
    return out;
  }
  return value;
}

function stripApiKeys(nodes: Record<string, unknown>[]): Record<string, unknown>[] {
  return nodes.map((node) => deepStripSensitive(node) as Record<string, unknown>);
}

type WorkflowRow = typeof workflows.$inferSelect;

function workflowToResponse(row: WorkflowRow): WorkflowResponse {
  return {
    id: row.id,
    name: row.name,
    description: row.description ?? undefined,
    nodes: JSON.parse(row.nodesJson || "[]"),
    connections: JSON.parse(row.connectionsJson || "[]"),
    character: JSON.parse(
      row.characterJson || '{"name": "AI Assistant", "personality": "Friendly and helpful"}',
    ),
    createdAt: row.createdAt,
    updatedAt: row.updatedAt,
  };
}

function generateId(): string {
  return crypto.randomUUID();
}

function nowISO(): string {
  return new Date().toISOString();
}

// ─── Routes ───────────────────────────────

// Create workflow
app.post("/", zValidator("json", createWorkflowBody), async (c) => {
  const body = c.req.valid("json");
  const id = generateId();
  const now = nowISO();

  const nodes = body.nodes ?? [];
  const connections = body.connections ?? [];
  const character = body.character ?? {
    name: "AI Assistant",
    personality: "Friendly and helpful virtual streamer",
  };

  await _db.insert(workflows).values({
    id,
    name: body.name,
    description: body.description ?? null,
    nodesJson: JSON.stringify(nodes),
    connectionsJson: JSON.stringify(connections),
    characterJson: JSON.stringify(character),
    createdAt: now,
    updatedAt: now,
  });

  const [row] = await _db.select().from(workflows).where(eq(workflows.id, id));
  return c.json(workflowToResponse(row));
});

// List workflows
app.get("/", async (c) => {
  const rows = await _db.select().from(workflows).orderBy(desc(workflows.updatedAt));
  return c.json(rows.map(workflowToResponse));
});

// Get workflow
app.get("/:id", async (c) => {
  const id = c.req.param("id");
  const [row] = await _db.select().from(workflows).where(eq(workflows.id, id));
  if (!row) return c.json({ detail: "Workflow not found" }, 404);
  return c.json(workflowToResponse(row));
});

// Update workflow
app.put("/:id", async (c) => {
  const id = c.req.param("id");
  let body;
  try {
    body = await c.req.json();
  } catch {
    return c.json({ error: "Invalid JSON in request body" }, 400);
  }

  const [existing] = await _db.select().from(workflows).where(eq(workflows.id, id));
  if (!existing) return c.json({ detail: "Workflow not found" }, 404);

  const updates: Partial<WorkflowRow> = { updatedAt: nowISO() };
  if (body.name !== undefined) updates.name = body.name;
  if (body.description !== undefined) updates.description = body.description;
  if (body.nodes !== undefined) updates.nodesJson = JSON.stringify(body.nodes);
  if (body.connections !== undefined) updates.connectionsJson = JSON.stringify(body.connections);
  if (body.character !== undefined) updates.characterJson = JSON.stringify(body.character);

  await _db.update(workflows).set(updates).where(eq(workflows.id, id));

  const [row] = await _db.select().from(workflows).where(eq(workflows.id, id));
  return c.json(workflowToResponse(row));
});

// Delete workflow
app.delete("/:id", async (c) => {
  const id = c.req.param("id");
  const [existing] = await _db.select().from(workflows).where(eq(workflows.id, id));
  if (!existing) return c.json({ detail: "Workflow not found" }, 404);

  await executor.stopWorkflow(id);
  await _db.delete(workflows).where(eq(workflows.id, id));
  return c.json({ status: "deleted" });
});

// Duplicate workflow
app.post("/:id/duplicate", async (c) => {
  const id = c.req.param("id");
  const [existing] = await _db.select().from(workflows).where(eq(workflows.id, id));
  if (!existing) return c.json({ detail: "Workflow not found" }, 404);

  const newId = generateId();
  const now = nowISO();

  await _db.insert(workflows).values({
    id: newId,
    name: `${existing.name} (Copy)`,
    description: existing.description,
    nodesJson: existing.nodesJson,
    connectionsJson: existing.connectionsJson,
    characterJson: existing.characterJson,
    createdAt: now,
    updatedAt: now,
  });

  const [row] = await _db.select().from(workflows).where(eq(workflows.id, newId));
  return c.json(workflowToResponse(row));
});

// Export workflow
app.get("/:id/export", async (c) => {
  const id = c.req.param("id");
  const excludeApiKeys = c.req.query("exclude_api_keys") !== "false";

  const [existing] = await _db.select().from(workflows).where(eq(workflows.id, id));
  if (!existing) return c.json({ detail: "Workflow not found" }, 404);

  let nodes = JSON.parse(existing.nodesJson || "[]");
  if (excludeApiKeys) nodes = stripApiKeys(nodes);

  return c.json({
    name: existing.name,
    description: existing.description,
    nodes,
    connections: JSON.parse(existing.connectionsJson || "[]"),
    character: JSON.parse(existing.characterJson || "{}"),
    exportedAt: nowISO(),
    version: "1.0",
  });
});

// Import workflow
const importWorkflowBody = z.object({
  name: z.string().max(256).optional(),
  description: z.string().max(2048).optional().nullable(),
  nodes: z.array(z.record(z.string(), z.unknown())).max(500).optional(),
  connections: z.array(z.record(z.string(), z.unknown())).max(2000).optional(),
  character: z.record(z.string(), z.unknown()).optional(),
});

app.post("/import", async (c) => {
  let raw;
  try {
    raw = await c.req.json();
  } catch {
    return c.json({ error: "Invalid JSON in request body" }, 400);
  }

  const parsed = importWorkflowBody.safeParse(raw);
  if (!parsed.success) {
    return c.json({ error: "Invalid workflow payload", details: parsed.error.format() }, 400);
  }
  const data = parsed.data;

  const id = generateId();
  const now = nowISO();

  await _db.insert(workflows).values({
    id,
    name: data.name ?? "Imported Workflow",
    description: data.description ?? null,
    nodesJson: JSON.stringify(data.nodes ?? []),
    connectionsJson: JSON.stringify(data.connections ?? []),
    characterJson: JSON.stringify(
      data.character ?? {
        name: "AI Assistant",
        personality: "Friendly",
      },
    ),
    createdAt: now,
    updatedAt: now,
  });

  const [row] = await _db.select().from(workflows).where(eq(workflows.id, id));
  return c.json(workflowToResponse(row));
});

// Validate workflow before execution
const validateWorkflowBody = z
  .object({
    nodes: z.array(z.record(z.string(), z.unknown())).optional(),
    connections: z.array(z.record(z.string(), z.unknown())).optional(),
  })
  .optional();

app.post("/:id/validate", async (c) => {
  const id = c.req.param("id");
  const [existing] = await _db.select().from(workflows).where(eq(workflows.id, id));
  if (!existing) return c.json({ detail: "Workflow not found" }, 404);

  let body: z.infer<typeof validateWorkflowBody> = {};
  try {
    const rawBody = await c.req.json();
    const parsed = validateWorkflowBody.safeParse(rawBody);
    if (!parsed.success) {
      return c.json({ error: "Invalid request body", details: parsed.error.format() }, 400);
    }
    body = parsed.data ?? {};
  } catch {
    // No body provided - use saved workflow data
  }

  const nodes = body?.nodes ?? JSON.parse(existing.nodesJson || "[]");
  const connections = body?.connections ?? JSON.parse(existing.connectionsJson || "[]");

  const issues = await validateWorkflow({ nodes, connections });
  const errors = issues.filter((i) => i.level === "error");
  const warnings = issues.filter((i) => i.level === "warning");

  return c.json({
    valid: errors.length === 0,
    errors,
    warnings,
    issues,
  });
});

// Start workflow execution
app.post("/:id/start", async (c) => {
  const id = c.req.param("id");
  const [existing] = await _db.select().from(workflows).where(eq(workflows.id, id));
  if (!existing) return c.json({ detail: "Workflow not found" }, 404);

  // Distinguish "no body" (fine) from "malformed body" (reject).
  // Hono's c.req.text() resolves to "" when no body was sent, letting us
  // skip the JSON parse in that case. Malformed JSON still gets a 400.
  let body: Record<string, unknown> = {};
  const rawBody = await c.req.text();
  if (rawBody.trim().length > 0) {
    try {
      const parsed = JSON.parse(rawBody) as unknown;
      if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
        body = parsed as Record<string, unknown>;
      } else {
        return c.json({ error: "Request body must be a JSON object" }, 400);
      }
    } catch {
      return c.json({ error: "Invalid JSON in request body" }, 400);
    }
  }

  const nodes = (body.nodes as unknown[] | undefined) ?? JSON.parse(existing.nodesJson || "[]");
  const connections =
    (body.connections as unknown[] | undefined) ??
    JSON.parse(existing.connectionsJson || "[]");
  const character =
    (body.character as Record<string, unknown> | undefined) ??
    JSON.parse(existing.characterJson || "{}");
  const startNodeId = typeof body.startNodeId === "string" ? body.startNodeId : null;

  const workflowData = {
    id: existing.id,
    name: existing.name,
    nodes,
    connections,
    character,
  };

  await executor.startWorkflow(id, workflowData, startNodeId);

  if (wsBroadcaster) {
    wsBroadcaster.broadcast(id, "execution.started", {});
  }

  return c.json({ status: "started", workflow_id: id });
});

// Stop workflow execution
app.post("/:id/stop", async (c) => {
  const id = c.req.param("id");
  await executor.stopWorkflow(id);

  if (wsBroadcaster) {
    wsBroadcaster.broadcast(id, "execution.stopped", {
      reason: "HTTP stop",
    });
  }

  return c.json({ status: "stopped", workflow_id: id });
});

// Get workflow execution status
app.get("/:id/status", async (c) => {
  const id = c.req.param("id");
  const status = executor.getStatus(id);
  return c.json({
    workflowId: id,
    status: status.status ?? "idle",
    startedAt: status.started_at ?? null,
    error: status.error ?? null,
  });
});

export { app as workflowRoutes };
