import { type Application, type Request, type Response } from 'express';
import { logger } from '../logger.js';
import { parseTaskId } from './validation.js';
import { jobEventBus, type JobStreamEvent } from './job-events.js';
import { type LocalTasksDeps } from './local-tasks-shared.js';

/**
 * delegate イベントを SSE クライアントへ送る JSON ペイロードに整形する。
 * delegate_text は delegate_text_delta（フロント側の蓄積キー）へ名前替えする。
 * 対象外イベントは null。純関数（テスト容易性のため分離）。
 */
export function formatDelegateSseFrame(
  event: JobStreamEvent,
): Record<string, unknown> | null {
  switch (event.type) {
    case 'delegate_text':
      return { type: 'delegate_text_delta', delegateRunId: event.delegateRunId, text: event.text };
    case 'delegate_lifecycle':
      return {
        type: 'delegate_lifecycle',
        delegateRunId: event.delegateRunId,
        parentRunId: event.parentRunId ?? null,
        depth: event.depth ?? 0,
        description: event.description ?? '',
        status: event.status ?? 'running',
        originJobId: event.originJobId ?? null,
      };
    case 'delegate_tool':
      return {
        type: 'delegate_tool',
        delegateRunId: event.delegateRunId,
        toolName: event.toolName ?? '',
        toolInput: event.toolInput ?? '',
      };
    default:
      return null;
  }
}

/**
 * Server-Sent Events stream of real-time job events (text deltas, tool-call
 * deltas, prompt progress) for a task's currently-running job.
 */
export function registerLocalTaskStreamRoutes(app: Application, deps: LocalTasksDeps): void {
  const { repo } = deps;

  // ── SSE stream: real-time job events ──────────────────────────────────────
  app.get('/api/local/tasks/:taskId/stream', async (req: Request, res: Response) => {
    const taskId = parseTaskId(req.params.taskId);
    if (taskId === null) { res.status(400).json({ error: 'invalid taskId' }); return; }

    try {
      const viewer = req.user as Express.User | undefined;
      const task = await repo.getLocalTask(taskId, viewer ? { viewer } : {});
      if (!task) { res.status(404).json({ error: 'task not found' }); return; }

      const runningJob = task.latestJob;
      if (!runningJob || (runningJob.status !== 'running' && runningJob.status !== 'dispatching')) {
        res.status(204).end();
        return;
      }
      const jobId = runningJob.id;

      res.setHeader('Content-Type', 'text/event-stream');
      res.setHeader('Cache-Control', 'no-store');
      res.setHeader('Connection', 'keep-alive');
      res.setHeader('X-Accel-Buffering', 'no');
      res.flushHeaders();

      // Text delta batching (50ms flush)
      let textBuf = '';
      let flushTimer: ReturnType<typeof setTimeout> | null = null;
      const TEXT_FLUSH_MS = 50;

      // Tool-call argument delta batching, keyed by callId (50ms flush).
      const toolBuf = new Map<string, { name: string; chunk: string }>();
      let toolFlushTimer: ReturnType<typeof setTimeout> | null = null;

      // delegate live: per-run text batching (50ms flush)。
      const delegateTextBuf = new Map<string, string>();
      let delegateFlushTimer: ReturnType<typeof setTimeout> | null = null;
      const flushDelegateText = () => {
        for (const [delegateRunId, text] of delegateTextBuf) {
          if (res.writableEnded) break;
          res.write(`data: ${JSON.stringify({ type: 'delegate_text_delta', delegateRunId, text })}\n\n`);
        }
        delegateTextBuf.clear();
        delegateFlushTimer = null;
      };

      const flushText = () => {
        if (textBuf) {
          const data = JSON.stringify({ type: 'text_delta', text: textBuf });
          res.write(`data: ${data}\n\n`);
          textBuf = '';
        }
        flushTimer = null;
      };

      const flushToolDeltas = () => {
        for (const [callId, { name, chunk }] of toolBuf) {
          if (res.writableEnded) break;
          res.write(`data: ${JSON.stringify({ type: 'tool_use_delta', callId, name, chunk })}\n\n`);
        }
        toolBuf.clear();
        toolFlushTimer = null;
      };

      const handler = (event: JobStreamEvent) => {
        if (res.writableEnded) return;
        if (event.type === 'text') {
          textBuf += event.text ?? '';
          if (!flushTimer) flushTimer = setTimeout(flushText, TEXT_FLUSH_MS);
          return;
        }
        if (event.type === 'tool_use_delta') {
          const callId = event.callId ?? '';
          // chunk is a full snapshot of args-so-far; keep the LATEST per
          // callId (replace, not append) so each flush sends the newest
          // complete prefix. Coalesces many snapshots into one per 50ms.
          toolBuf.set(callId, {
            name: event.name ?? toolBuf.get(callId)?.name ?? '',
            chunk: event.chunk ?? '',
          });
          if (!toolFlushTimer) toolFlushTimer = setTimeout(flushToolDeltas, TEXT_FLUSH_MS);
          return;
        }
        if (event.type === 'delegate_text') {
          const id = event.delegateRunId ?? '';
          delegateTextBuf.set(id, (delegateTextBuf.get(id) ?? '') + (event.text ?? ''));
          if (!delegateFlushTimer) delegateFlushTimer = setTimeout(flushDelegateText, TEXT_FLUSH_MS);
          return;
        }
        if (event.type === 'delegate_lifecycle' || event.type === 'delegate_tool') {
          // この run の保留テキストを先に出してから lifecycle/tool を送る（順序保証）。
          if (delegateTextBuf.size) flushDelegateText();
          const frame = formatDelegateSseFrame(event);
          if (frame) res.write(`data: ${JSON.stringify(frame)}\n\n`);
          return;
        }
        // Flush pending text + tool deltas before non-streaming events
        if (textBuf) flushText();
        if (toolBuf.size) flushToolDeltas();
        if (delegateTextBuf.size) flushDelegateText();
        if (event.type === 'prompt_progress') {
          const effective = (event.processed ?? 0) - (event.cache ?? 0);
          const effectiveTotal = (event.total ?? 0) - (event.cache ?? 0);
          const percent = effectiveTotal > 0 ? Math.round(effective / effectiveTotal * 100) : 0;
          res.write(`data: ${JSON.stringify({ type: 'prompt_progress', percent, processed: event.processed, total: event.total, cache: event.cache, timeMs: event.timeMs })}\n\n`);
        } else if (event.type === 'done') {
          res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
          cleanup();
          res.end();
        } else {
          res.write(`data: ${JSON.stringify(event)}\n\n`);
        }
      };

      // Heartbeat to keep connection alive
      const heartbeat = setInterval(() => {
        if (!res.writableEnded) res.write(': heartbeat\n\n');
      }, 15_000);

      const cleanup = () => {
        jobEventBus.offJob(jobId, handler);
        clearInterval(heartbeat);
        if (flushTimer) { clearTimeout(flushTimer); flushText(); }
        if (toolFlushTimer) { clearTimeout(toolFlushTimer); flushToolDeltas(); }
        if (delegateFlushTimer) { clearTimeout(delegateFlushTimer); flushDelegateText(); }
      };

      jobEventBus.onJob(jobId, handler);
      req.on('close', cleanup);
    } catch (err) {
      logger.error(`Local task stream API error: ${err}`);
      if (!res.headersSent) res.status(500).json({ error: 'stream failed' });
    }
  });
}
