import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { randomUUID } from "node:crypto";
import { createWriteStream, type WriteStream, statSync, openSync, readSync, closeSync } from "node:fs";
import { WebSocketServer, type WebSocket } from "ws";
import { validateBearer } from "./lib/auth.js";
import { ConversationStore } from "./lib/store.js";
import { TaskLedger } from "./task-ledger.js";
import type { Task } from "./task-ledger.js";
import type {
  IncomingMessage as ChannelMessage,
  MessageAttachment,
  ReplyPayload,
  NotifyPayload,
  StatusEvent,
  AgentStatus,
  WSMessage,
  ChatMessage,
} from "./lib/types.js";
import { readFileSync } from "node:fs";
import { getCachedSkills, invalidateCache } from "channel-lib/skills-loader";
import {
  writeToAgentFifo,
  formatTaskIncoming,
  formatTaskCompleted,
} from "channel-lib/agent-router";
import { basename } from "node:path";

// ─── Image Validation ───────────────────────────────────────────────
const MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024; // 20MB
const MAX_IMAGE_DIMENSION = 8192;
const SUPPORTED_IMAGE_TYPES = new Set([
  "image/jpeg",
  "image/png",
  "image/gif",
  "image/webp",
]);

type ImageValidation =
  | { valid: true; base64: string; mediaType: string }
  | { valid: false; reason: string };

function getImageDimensions(
  header: Buffer,
  mimeType: string
): { width: number; height: number } | null {
  // PNG: IHDR chunk — width at byte 16, height at byte 20 (big-endian uint32)
  if (mimeType === "image/png" && header.length >= 24) {
    if (header[0] === 0x89 && header[1] === 0x50) {
      return {
        width: header.readUInt32BE(16),
        height: header.readUInt32BE(20),
      };
    }
  }

  // GIF: width at byte 6, height at byte 8 (little-endian uint16)
  if (mimeType === "image/gif" && header.length >= 10) {
    if (header[0] === 0x47 && header[1] === 0x49 && header[2] === 0x46) {
      return {
        width: header.readUInt16LE(6),
        height: header.readUInt16LE(8),
      };
    }
  }

  // JPEG: scan for SOF0/SOF1/SOF2 marker to find dimensions
  if (mimeType === "image/jpeg" && header.length >= 2) {
    if (header[0] === 0xff && header[1] === 0xd8) {
      let offset = 2;
      while (offset + 9 < header.length) {
        if (header[offset] !== 0xff) break;
        const marker = header[offset + 1];
        if (marker >= 0xc0 && marker <= 0xc2) {
          return {
            height: header.readUInt16BE(offset + 5),
            width: header.readUInt16BE(offset + 7),
          };
        }
        if (marker === 0xd8 || marker === 0xd9) {
          offset += 2;
        } else {
          const segLen = header.readUInt16BE(offset + 2);
          offset += 2 + segLen;
        }
      }
    }
  }

  // WebP: RIFF container with VP8/VP8L/VP8X sub-chunks
  if (mimeType === "image/webp" && header.length >= 30) {
    if (
      header.toString("ascii", 0, 4) === "RIFF" &&
      header.toString("ascii", 8, 12) === "WEBP"
    ) {
      const chunkType = header.toString("ascii", 12, 16);
      if (chunkType === "VP8 " && header[23] === 0x9d && header[24] === 0x01 && header[25] === 0x2a) {
        return {
          width: header.readUInt16LE(26) & 0x3fff,
          height: header.readUInt16LE(28) & 0x3fff,
        };
      }
      if (chunkType === "VP8L" && header.length >= 25) {
        const bits = header.readUInt32LE(21);
        return {
          width: (bits & 0x3fff) + 1,
          height: ((bits >> 14) & 0x3fff) + 1,
        };
      }
      if (chunkType === "VP8X") {
        return {
          width: (header[24] | (header[25] << 8) | (header[26] << 16)) + 1,
          height: (header[27] | (header[28] << 8) | (header[29] << 16)) + 1,
        };
      }
    }
  }

  return null;
}

function validateImage(path: string, mimeType: string): ImageValidation {
  if (!SUPPORTED_IMAGE_TYPES.has(mimeType)) {
    return { valid: false, reason: `unsupported type: ${mimeType}` };
  }

  let fileSize: number;
  try {
    fileSize = statSync(path).size;
  } catch (err) {
    return { valid: false, reason: `cannot access file: ${err}` };
  }

  if (fileSize === 0) {
    return { valid: false, reason: "empty file" };
  }

  if (fileSize > MAX_IMAGE_FILE_SIZE) {
    const mb = (fileSize / 1024 / 1024).toFixed(1);
    return { valid: false, reason: `file too large (${mb}MB, max 20MB)` };
  }

  // Read header to validate magic bytes and parse dimensions
  let header: Buffer;
  try {
    const fd = openSync(path, "r");
    header = Buffer.alloc(Math.min(65536, fileSize));
    readSync(fd, header, 0, header.length, 0);
    closeSync(fd);
  } catch (err) {
    return { valid: false, reason: `cannot read file: ${err}` };
  }

  const isPng = header[0] === 0x89 && header[1] === 0x50 && header[2] === 0x4e && header[3] === 0x47;
  const isJpeg = header[0] === 0xff && header[1] === 0xd8;
  const isGif = header[0] === 0x47 && header[1] === 0x49 && header[2] === 0x46;
  const isWebp =
    header.length >= 12 &&
    header.toString("ascii", 0, 4) === "RIFF" &&
    header.toString("ascii", 8, 12) === "WEBP";

  if (!isPng && !isJpeg && !isGif && !isWebp) {
    return { valid: false, reason: "not a valid image (magic bytes don't match any supported format)" };
  }

  const dims = getImageDimensions(header, mimeType);
  if (dims) {
    if (dims.width === 0 || dims.height === 0) {
      return { valid: false, reason: `invalid dimensions (${dims.width}x${dims.height})` };
    }
    if (dims.width > MAX_IMAGE_DIMENSION || dims.height > MAX_IMAGE_DIMENSION) {
      return {
        valid: false,
        reason: `dimensions too large (${dims.width}x${dims.height}, max ${MAX_IMAGE_DIMENSION}x${MAX_IMAGE_DIMENSION})`,
      };
    }
  }

  // All checks passed — read full file and encode
  let data: Buffer;
  try {
    data = readFileSync(path);
  } catch (err) {
    return { valid: false, reason: `cannot read file: ${err}` };
  }

  return { valid: true, base64: data.toString("base64"), mediaType: mimeType };
}

const PORT = parseInt(process.env.ONE_CHANNEL_PORT || "7100", 10);
const INPUT_FIFO = process.env.ONE_INPUT_FIFO || "/tmp/one-input";

// Derive agent name from the channel directory name (e.g., "one-channel" -> "one")
const AGENT_NAME = basename(process.cwd()).replace(/-channel$/, "");

// ─── FIFO for stream-json input to Claude Code ──────────────────────
// Messages are written here as NDJSON lines that Claude's --input-format
// stream-json reads from stdin. Session ID is captured from Claude's init
// output and tracked for subsequent messages.
let sessionId = "default";
let fifoStream: WriteStream | null = null;

function openFifo(): void {
  try {
    fifoStream = createWriteStream(INPUT_FIFO, { flags: "a" });
    fifoStream.on("error", (err) => {
      process.stderr.write(`[${AGENT_NAME}-channel] FIFO write error: ${err.message}\n`);
      fifoStream = null;
    });
  } catch (err) {
    process.stderr.write(`[${AGENT_NAME}-channel] Cannot open FIFO ${INPUT_FIFO}: ${err}\n`);
  }
}

function writeToFifo(text: string, conversationId: string, userEmail: string, attachments?: MessageAttachment[]): boolean {
  if (!fifoStream) openFifo();
  if (!fifoStream) return false;

  const prompt = `[conversation_id: ${conversationId}] [from: ${userEmail}]\n${text}`;

  let content: string | Array<Record<string, unknown>>;

  if (attachments && attachments.length > 0) {
    // Build multimodal content: text block + validated image blocks
    const blocks: Array<Record<string, unknown>> = [];
    blocks.push({ type: "text", text: prompt });

    for (const att of attachments) {
      const result = validateImage(att.path, att.mime_type || "image/jpeg");
      if (result.valid) {
        blocks.push({
          type: "image",
          source: {
            type: "base64",
            media_type: result.mediaType,
            data: result.base64,
          },
        });
      } else {
        process.stderr.write(
          `[${AGENT_NAME}-channel] Rejected attachment "${att.name}" (${att.path}): ${result.reason}\n`
        );
        blocks.push({
          type: "text",
          text: `[Attachment "${att.name}" could not be processed: ${result.reason}]`,
        });
      }
    }
    content = blocks;
  } else {
    content = prompt;
  }

  const msg = JSON.stringify({
    type: "user",
    message: { role: "user", content },
    session_id: sessionId,
    parent_tool_use_id: null,
  });

  return fifoStream.write(msg + "\n");
}

// Try to open the FIFO on startup
openFifo();

// ─── Conversation Store ──────────────────────────────────────────────
const store = new ConversationStore();
await store.load();

// ─── Client Registries ───────────────────────────────────────────────
// WebSocket clients: conversation_id -> Set<WebSocket>
const wsClients = new Map<string, Set<WebSocket>>();
// SSE clients: conversation_id -> Set<ServerResponse>
const sseClients = new Map<string, Set<ServerResponse>>();
// Task SSE clients: all subscribers to task updates
const taskSseClients = new Set<ServerResponse>();

// ─── Task Ledger ─────────────────────────────────────────────────────
// Per-agent SSE clients: agentName -> Set<ServerResponse>
// Receives task updates where to_agent or from_agent matches the subscribed name.
const agentTaskSseClients = new Map<string, Set<ServerResponse>>();

function broadcastTaskUpdate(task: Task): void {
  // Broadcast to all-tasks SSE subscribers
  if (taskSseClients.size > 0) {
    const payload = `data: ${JSON.stringify({ type: "task_update", task })}\n\n`;
    for (const res of taskSseClients) {
      if (!res.writableEnded) res.write(payload);
    }
  }

  // Broadcast to per-agent SSE subscribers (from_agent and to_agent both get updates)
  const payload = `data: ${JSON.stringify({ type: "task_update", task })}\n\n`;
  for (const agentName of [task.from_agent, task.to_agent]) {
    const clients = agentTaskSseClients.get(agentName);
    if (!clients) continue;
    for (const res of clients) {
      if (!res.writableEnded) res.write(payload);
    }
  }
}

const taskLedger = new TaskLedger(broadcastTaskUpdate);
await taskLedger.load();

function broadcast(conversationId: string, message: WSMessage): void {
  const payload = JSON.stringify(message);

  // WebSocket clients
  const ws = wsClients.get(conversationId);
  if (ws) {
    for (const client of ws) {
      if (client.readyState === client.OPEN) client.send(payload);
    }
  }

  // SSE clients
  const sse = sseClients.get(conversationId);
  if (sse) {
    for (const res of sse) {
      if (!res.writableEnded) res.write(`data: ${payload}\n\n`);
    }
  }
}

function broadcastAll(message: WSMessage): void {
  const payload = JSON.stringify(message);

  for (const clients of wsClients.values()) {
    for (const ws of clients) {
      if (ws.readyState === ws.OPEN) ws.send(payload);
    }
  }

  for (const clients of sseClients.values()) {
    for (const res of clients) {
      if (!res.writableEnded) res.write(`data: ${payload}\n\n`);
    }
  }
}

// ─── MCP Channel Server ─────────────────────────────────────────────
const mcp = new Server(
  { name: `${AGENT_NAME}-channel`, version: "0.1.0" },
  {
    capabilities: {
      experimental: { "claude/channel": {} },
      tools: {},
    },
    instructions: [
      `You are ${AGENT_NAME}, a persistent AI agent.`,
      "Messages from the user arrive as <channel> events.",
      "ALWAYS reply using the `reply` tool — your terminal output is NOT visible to the user.",
      "Include the conversation_id from the channel event metadata in every reply.",
      "For long-running tasks, send an initial acknowledgment, then follow up with results.",
      "If a message has no conversation_id, use the one from the most recent channel event.",
    ].join(" "),
  }
);

// ─── MCP Tools ───────────────────────────────────────────────────────
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "reply",
      description:
        "Send a reply message back to the user through the web UI. " +
        "Use this for ALL responses — the user cannot see terminal output.",
      inputSchema: {
        type: "object" as const,
        properties: {
          conversation_id: {
            type: "string",
            description: "The conversation ID from the channel event metadata",
          },
          text: {
            type: "string",
            description: "The reply text to send to the user",
          },
        },
        required: ["conversation_id", "text"],
      },
    },
    {
      name: "notify",
      description:
        "Send a notification/toast to the web UI. Use for proactive alerts " +
        "or status updates that are not part of a conversation.",
      inputSchema: {
        type: "object" as const,
        properties: {
          text: {
            type: "string",
            description: "The notification text",
          },
          level: {
            type: "string",
            enum: ["info", "warning", "error"],
            description: "Notification severity level",
          },
        },
        required: ["text", "level"],
      },
    },
  ],
}));

mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
  const { name, arguments: args } = req.params;

  if (name === "reply") {
    const { conversation_id, text } = args as unknown as ReplyPayload;

    const message: ChatMessage = {
      id: randomUUID(),
      role: "assistant",
      text,
      timestamp: Date.now(),
    };
    store.addMessage(conversation_id, message);

    const wsMsg: WSMessage = {
      type: "reply",
      conversation_id,
      text,
      timestamp: message.timestamp,
    };
    broadcast(conversation_id, wsMsg);

    return { content: [{ type: "text", text: "Reply sent." }] };
  }

  if (name === "notify") {
    const { text, level } = args as unknown as NotifyPayload;

    const wsMsg: WSMessage = {
      type: "notify",
      text,
      timestamp: Date.now(),
      level,
    };
    broadcastAll(wsMsg);

    return { content: [{ type: "text", text: "Notification sent." }] };
  }

  return {
    content: [{ type: "text", text: `Unknown tool: ${name}` }],
    isError: true,
  };
});

// ─── HTTP Server ─────────────────────────────────────────────────────
function readBody(req: IncomingMessage): Promise<string> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    req.on("data", (chunk: Buffer) => chunks.push(chunk));
    req.on("end", () => resolve(Buffer.concat(chunks).toString()));
    req.on("error", reject);
  });
}

function json(res: ServerResponse, status: number, data: unknown): void {
  res.writeHead(status, { "Content-Type": "application/json" });
  res.end(JSON.stringify(data));
}

const httpServer = createServer(async (req, res) => {
  const url = new URL(req.url || "/", `http://localhost:${PORT}`);

  // CORS for local development
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
  if (req.method === "OPTIONS") {
    res.writeHead(204);
    res.end();
    return;
  }

  // Skills documentation (loaded from central registry)
  if (req.method === "GET" && url.pathname === "/skills.md") {
    const content = await getCachedSkills(AGENT_NAME);
    res.writeHead(200, { "Content-Type": "text/markdown" });
    res.end(content);
    return;
  }

  // Reload skills cache (called by web UI after assignment changes)
  if (req.method === "POST" && url.pathname === "/reload-skills") {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }
    invalidateCache();
    json(res, 200, { ok: true });
    return;
  }

  // Health check
  if (req.method === "GET" && url.pathname === "/health") {
    json(res, 200, { status: "ok", uptime: process.uptime() });
    return;
  }

  // Status update (from Claude Code hooks)
  if (req.method === "POST" && url.pathname === "/status") {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }

    let body: StatusEvent;
    try {
      body = JSON.parse(await readBody(req)) as StatusEvent;
    } catch {
      json(res, 400, { error: "Invalid JSON" });
      return;
    }

    const statusMap: Record<string, AgentStatus> = {
      UserPromptSubmit: "thinking",
      PreToolUse: "tool_use",
      PostToolUse: "tool_done",
      PostToolUseFailure: "tool_error",
    };

    const wsMsg: WSMessage = {
      type: "status",
      text: body.tool_name || body.event,
      timestamp: Date.now(),
      status: statusMap[body.event] || "thinking",
      tool_name: body.tool_name,
      conversation_id: body.conversation_id,
    };
    broadcastAll(wsMsg);
    json(res, 200, { ok: true });
    return;
  }

  // Post a message (from Next.js API route)
  if (req.method === "POST" && url.pathname === "/message") {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }

    let body: ChannelMessage;
    try {
      body = JSON.parse(await readBody(req)) as ChannelMessage;
    } catch {
      json(res, 400, { error: "Invalid JSON" });
      return;
    }

    if ((!body.text || typeof body.text !== "string") && (!body.attachments || body.attachments.length === 0)) {
      json(res, 400, { error: "Missing required field: text or attachments" });
      return;
    }

    const conversationId = body.conversation_id || randomUUID();
    const userEmail = body.user_email || "unknown";
    const messageText = body.text || "[image]";

    // Store the user message
    const userMessage: ChatMessage = {
      id: randomUUID(),
      role: "user",
      text: messageText,
      timestamp: Date.now(),
      user_email: userEmail,
    };
    store.addMessage(conversationId, userMessage);

    // Write to FIFO so Claude's stream-json stdin receives the message
    const written = writeToFifo(messageText, conversationId, userEmail, body.attachments);
    if (!written) {
      process.stderr.write(`[${AGENT_NAME}-channel] Warning: could not write to FIFO\n`);
    }

    json(res, 200, {
      conversation_id: conversationId,
      message_id: userMessage.id,
    });
    return;
  }

  // Get conversation history
  if (req.method === "GET" && url.pathname.startsWith("/conversations")) {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }

    const parts = url.pathname.split("/").filter(Boolean);

    if (parts.length === 1) {
      // GET /conversations — list all
      json(res, 200, { conversations: store.list() });
      return;
    }

    if (parts.length === 2) {
      // GET /conversations/:id
      const conv = store.get(parts[1]);
      if (!conv) {
        json(res, 404, { error: "Conversation not found" });
        return;
      }
      json(res, 200, conv);
      return;
    }
  }

  // SSE stream for real-time replies (works through nginx without WebSocket config)
  if (req.method === "GET" && url.pathname === "/stream") {
    const conversationId = url.searchParams.get("conversation_id");
    if (!conversationId) {
      json(res, 400, { error: "Missing conversation_id query parameter" });
      return;
    }

    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
      "X-Accel-Buffering": "no", // Disable nginx buffering
    });

    // Send initial keepalive
    res.write(": connected\n\n");

    // Register SSE client
    if (!sseClients.has(conversationId)) {
      sseClients.set(conversationId, new Set());
    }
    sseClients.get(conversationId)!.add(res);

    // Keepalive every 15s to prevent proxy timeouts
    const keepalive = setInterval(() => {
      if (!res.writableEnded) res.write(": keepalive\n\n");
    }, 15000);

    req.on("close", () => {
      clearInterval(keepalive);
      const clients = sseClients.get(conversationId);
      if (clients) {
        clients.delete(res);
        if (clients.size === 0) sseClients.delete(conversationId);
      }
    });
    return;
  }

  // ── Task Ledger Endpoints ──────────────────────────────────────────
  if (url.pathname === "/tasks" && req.method === "GET") {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }
    json(res, 200, { tasks: taskLedger.list() });
    return;
  }

  if (url.pathname === "/tasks" && req.method === "POST") {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }
    let body: {
      from_agent: string;
      to_agent: string;
      task: string;
      context?: string;
      conversation_id?: string;
    };
    try {
      body = JSON.parse(await readBody(req));
    } catch {
      json(res, 400, { error: "Invalid JSON" });
      return;
    }
    if (!body.from_agent || !body.to_agent || !body.task) {
      json(res, 400, { error: "Missing required fields: from_agent, to_agent, task" });
      return;
    }
    const task = taskLedger.dispatch(
      body.from_agent,
      body.to_agent,
      body.task,
      body.context,
      body.conversation_id
    );

    // Push task_incoming envelope into recipient agent's FIFO
    const incomingMsg = formatTaskIncoming({
      taskId: task.id,
      fromAgent: task.from_agent,
      task: task.task,
      context: task.context,
      conversationId: task.conversation_id,
    });
    writeToAgentFifo(task.to_agent, incomingMsg).then((result) => {
      if (!result.ok) {
        process.stderr.write(
          `[${AGENT_NAME}-channel] task_incoming push to ${task.to_agent} failed: ${result.error}\n`
        );
        // Mark task failed if recipient FIFO is unreachable
        taskLedger.complete(task.id, `Delivery failed: ${result.error}`, "failed");
      } else {
        process.stderr.write(
          `[${AGENT_NAME}-channel] task_incoming pushed to ${task.to_agent} (task ${task.id})\n`
        );
      }
    });

    json(res, 200, task);
    return;
  }

  // SSE stream for task updates — must be checked BEFORE the generic /tasks/:id GET handler
  // because startsWith("/tasks/") would match "/tasks/stream" and return 404 for task id "stream"
  if (req.method === "GET" && url.pathname === "/tasks/stream") {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }

    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
      "X-Accel-Buffering": "no",
    });
    res.write(": connected\n\n");

    // Send current task list as initial state
    const currentTasks = taskLedger.list();
    if (currentTasks.length > 0) {
      res.write(`data: ${JSON.stringify({ type: "task_snapshot", tasks: currentTasks })}\n\n`);
    }

    taskSseClients.add(res);

    const keepalive = setInterval(() => {
      if (!res.writableEnded) res.write(": keepalive\n\n");
    }, 15000);

    req.on("close", () => {
      clearInterval(keepalive);
      taskSseClients.delete(res);
    });
    return;
  }

  if (url.pathname.startsWith("/tasks/") && req.method === "GET") {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }
    const parts = url.pathname.split("/").filter(Boolean);
    if (parts.length === 2) {
      // GET /tasks/:id
      const task = taskLedger.check(parts[1]);
      if (!task) {
        json(res, 404, { error: "Task not found" });
        return;
      }
      json(res, 200, task);
      return;
    }
  }

  if (url.pathname.startsWith("/tasks/") && req.method === "POST") {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }
    const parts = url.pathname.split("/").filter(Boolean);
    // POST /tasks/:id/complete
    if (parts.length === 3 && parts[2] === "complete") {
      const taskId = parts[1];
      let body: { result: string; status: "complete" | "failed" };
      try {
        body = JSON.parse(await readBody(req));
      } catch {
        json(res, 400, { error: "Invalid JSON" });
        return;
      }
      if (!body.result || !body.status) {
        json(res, 400, { error: "Missing required fields: result, status" });
        return;
      }
      if (body.status !== "complete" && body.status !== "failed") {
        json(res, 400, { error: "status must be 'complete' or 'failed'" });
        return;
      }
      const task = taskLedger.complete(taskId, body.result, body.status);
      if (!task) {
        json(res, 404, { error: "Task not found" });
        return;
      }

      // Push task_completed notification into dispatcher's FIFO so it can resume
      const completedMsg = formatTaskCompleted({
        taskId: task.id,
        fromAgent: task.from_agent,
        toAgent: task.to_agent,
        status: task.status,
        result: task.result ?? "",
        conversationId: task.conversation_id,
      });
      writeToAgentFifo(task.from_agent, completedMsg).then((result) => {
        if (!result.ok) {
          process.stderr.write(
            `[${AGENT_NAME}-channel] task_completed push to ${task.from_agent} failed: ${result.error}\n`
          );
        } else {
          process.stderr.write(
            `[${AGENT_NAME}-channel] task_completed pushed to ${task.from_agent} (task ${task.id})\n`
          );
        }
      });

      json(res, 200, task);
      return;
    }
  }

  // ── Per-agent task SSE stream: /tasks/agent/:name/stream ──────────────────
  // Emits task_update events for tasks where to_agent or from_agent === :name
  if (req.method === "GET" && url.pathname.match(/^\/tasks\/agent\/[^/]+\/stream$/)) {
    const agentName = url.pathname.split("/")[3];

    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
      "X-Accel-Buffering": "no",
    });
    res.write(": connected\n\n");

    // Send current tasks for this agent as initial snapshot
    const agentTasks = taskLedger.list().filter(
      (t) => t.to_agent === agentName || t.from_agent === agentName
    );
    if (agentTasks.length > 0) {
      res.write(
        `data: ${JSON.stringify({ type: "task_snapshot", tasks: agentTasks })}\n\n`
      );
    }

    if (!agentTaskSseClients.has(agentName)) {
      agentTaskSseClients.set(agentName, new Set());
    }
    agentTaskSseClients.get(agentName)!.add(res);

    const keepalive = setInterval(() => {
      if (!res.writableEnded) res.write(": keepalive\n\n");
    }, 15000);

    req.on("close", () => {
      clearInterval(keepalive);
      const clients = agentTaskSseClients.get(agentName);
      if (clients) {
        clients.delete(res);
        if (clients.size === 0) agentTaskSseClients.delete(agentName);
      }
    });
    return;
  }

  // ── Per-agent task list: GET /tasks/agent/:name ────────────────────────────
  if (req.method === "GET" && url.pathname.match(/^\/tasks\/agent\/[^/]+$/)) {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }
    const agentName = url.pathname.split("/")[3];
    const agentTasks = taskLedger.list().filter(
      (t) => t.to_agent === agentName || t.from_agent === agentName
    );
    json(res, 200, { tasks: agentTasks });
    return;
  }

  // ── Task completion callback: POST /tasks/callback ─────────────────────────
  // External agents or distributed channel servers can POST here to trigger a
  // task_completed notification into the local agent's FIFO.
  if (req.method === "POST" && url.pathname === "/tasks/callback") {
    if (!validateBearer(req.headers.authorization)) {
      json(res, 401, { error: "Unauthorized" });
      return;
    }
    let body: {
      task_id: string;
      from_agent: string;
      to_agent: string;
      status: string;
      result: string;
      conversation_id?: string;
    };
    try {
      body = JSON.parse(await readBody(req));
    } catch {
      json(res, 400, { error: "Invalid JSON" });
      return;
    }
    if (!body.task_id || !body.to_agent || !body.status) {
      json(res, 400, { error: "Missing required fields: task_id, to_agent, status" });
      return;
    }
    // Write task_completed into the local agent's FIFO
    const completedMsg = formatTaskCompleted({
      taskId: body.task_id,
      fromAgent: body.from_agent || "unknown",
      toAgent: body.to_agent,
      status: body.status,
      result: body.result || "",
      conversationId: body.conversation_id,
    });
    const writeResult = await writeToAgentFifo(AGENT_NAME, completedMsg);
    if (!writeResult.ok) {
      process.stderr.write(`[${AGENT_NAME}-channel] /tasks/callback FIFO write failed: ${writeResult.error}\n`);
      json(res, 502, { error: writeResult.error });
      return;
    }
    json(res, 200, { ok: true });
    return;
  }

  json(res, 404, { error: "Not found" });
});

// ─── WebSocket Server ────────────────────────────────────────────────
const wss = new WebSocketServer({ server: httpServer, path: "/ws" });

wss.on("connection", (ws, req) => {
  const url = new URL(req.url || "/", `http://localhost:${PORT}`);
  const conversationId = url.searchParams.get("conversation_id");

  if (!conversationId) {
    ws.close(4000, "Missing conversation_id query parameter");
    return;
  }

  // Register client
  if (!wsClients.has(conversationId)) {
    wsClients.set(conversationId, new Set());
  }
  wsClients.get(conversationId)!.add(ws);

  ws.on("close", () => {
    const clients = wsClients.get(conversationId);
    if (clients) {
      clients.delete(ws);
      if (clients.size === 0) {
        wsClients.delete(conversationId);
      }
    }
  });

  ws.on("error", (err) => {
    process.stderr.write(`[${AGENT_NAME}-channel] ws error: ${err.message}\n`);
  });
});

// ─── Start ───────────────────────────────────────────────────────────
httpServer.listen(PORT, "0.0.0.0", () => {
  process.stderr.write(`[${AGENT_NAME}-channel] HTTP+WS server listening on port ${PORT}\n`);
});

// Connect MCP over stdio (Claude Code spawns this process)
const transport = new StdioServerTransport();
await mcp.connect(transport);

process.stderr.write(`[${AGENT_NAME}-channel] MCP channel connected via stdio\n`);
