import express, { type Application, type Request, type Response } from 'express';
import { mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { localTaskRepoName, MISSION_BRIEF_FIELDS } from '../db/repository.js';
import { logger } from '../logger.js';
import { parseTaskId, validateCommentBody, validateFeedbackBody } from './validation.js';
import { checkTaskOwnership, canViewTask, resolveSpaceAccess, reserveAttachmentName } from './local-api-helpers.js';
import { type LocalTasksDeps, makeDynamicJson, resolveTaskWriteGate } from './local-tasks-shared.js';

/**
 * Conversation + per-task state: feedback / mission brief / comments (read &
 * post, including interjection and tool-request gating).
 */
export function registerLocalTaskCommentsRoutes(app: Application, deps: LocalTasksDeps): void {
  const { repo } = deps;
  const dynamicJson = makeDynamicJson(deps.opts.getMaxUploadMb);

  app.put('/api/local/tasks/:taskId/feedback', express.json(), async (req: Request, res: Response) => {
    try {
      const taskId = parseTaskId(req.params.taskId);
      if (taskId === null) {
        res.status(400).json({ error: 'Invalid task ID' });
        return;
      }
      const validation = validateFeedbackBody(req.body);
      if (!validation.valid) {
        res.status(400).json({ error: validation.error });
        return;
      }
      const viewer = req.user as Express.User | undefined;
      const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
      if (!checkTaskOwnership(req, res, task)) return;
      await repo.updateFeedback(taskId, validation.data);
      const updated = await repo.getLocalTask(taskId);
      res.json({ task: updated });
    } catch (err) {
      logger.error(`Local task feedback API error: ${err}`);
      res.status(500).json({ error: 'Failed to update feedback' });
    }
  });

  app.put('/api/local/tasks/:taskId/mission', express.json(), async (req: Request, res: Response) => {
    try {
      const taskId = parseTaskId(req.params.taskId);
      if (taskId === null) {
        res.status(400).json({ error: 'Invalid task ID' });
        return;
      }
      const viewer = req.user as Express.User | undefined;
      const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
      if (!checkTaskOwnership(req, res, task)) return;

      // Partial-replace: only string fields are written. Anything else
      // (null, undefined, non-string) is treated as "leave unchanged".
      // To clear a field, send an empty string.
      const body = (req.body ?? {}) as Record<string, unknown>;
      const patch: Record<string, string> = {};
      for (const key of MISSION_BRIEF_FIELDS) {
        const v = body[key];
        if (typeof v === 'string') patch[key] = v;
      }
      if (Object.keys(patch).length === 0) {
        res.status(400).json({ error: `No mission fields provided. Send ${MISSION_BRIEF_FIELDS.join(', ')} as strings.` });
        return;
      }
      const merged = await repo.updateMissionBrief(taskId, patch);
      res.json({ missionBrief: merged });
    } catch (err) {
      logger.error(`Local task mission API error: ${err}`);
      res.status(500).json({ error: 'Failed to update mission brief' });
    }
  });

  app.get('/api/local/tasks/:taskId/comments', async (req: Request, res: Response) => {
    try {
      const taskId = parseTaskId(req.params.taskId);
      if (taskId === null) {
        res.status(400).json({ error: 'Invalid task ID' });
        return;
      }
      const viewer = req.user as Express.User | undefined;
      const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
      if (!canViewTask(req, res, task, resolveSpaceAccess(repo, task, viewer))) return;
      const comments = await repo.listLocalTaskComments(taskId);
      res.json({ comments });
    } catch (err) {
      logger.error(`Local task comments API error: ${err}`);
      res.status(500).json({ error: 'Failed to fetch local task comments' });
    }
  });

  app.post('/api/local/tasks/:taskId/comments', dynamicJson, async (req: Request, res: Response) => {
    try {
      const taskId = parseTaskId(req.params.taskId);
      if (taskId === null) {
        res.status(400).json({ error: 'Invalid task ID' });
        return;
      }
      const commentValidation = validateCommentBody(req.body);
      if (!commentValidation.valid) {
        res.status(400).json({ error: commentValidation.error });
        return;
      }
      const { body, author, attachments } = commentValidation;
      const viewer = req.user as Express.User | undefined;
      const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
      // 共有ワークスペースのタスクには editor 以上のメンバーもコメント可能。
      // owner/admin/editor+ → 投稿可、閲覧のみ（viewer ロール）→ 明確な 403、
      // 閲覧すらできない（非メンバー）→ 404。
      const writeGate = await resolveTaskWriteGate(repo, viewer, task);
      if (writeGate === 'deny') {
        res.status(404).json({ error: 'Task not found' });
        return;
      }
      if (writeGate === 'view-only') {
        res.status(403).json({ error: '閲覧のみのため投稿できません' });
        return;
      }

      // Tool-request mechanism: a job parked for tool approval must be resumed
      // via approve/deny (POST .../tool-requests/:id/decide), NOT by a normal
      // message — that would spawn a SECOND resume job running in parallel with
      // the one the approval re-queues (duplicate execution). The UI locks the
      // composer; guard here too so it's race-proof regardless of UI polling.
      // Checked TWICE: once now (before writing attachments, so a rejected post
      // leaves no orphan files in input/) and again after attachments (below)
      // to close the TOCTOU where the job flips to tool_request mid-upload.
      const earlyJob = await repo.getLatestJobForIssue(localTaskRepoName(taskId), taskId);
      if (earlyJob && earlyJob.status === 'waiting_human'
          && (earlyJob.waitReason === 'tool_request' || earlyJob.waitReason === 'package_request')) {
        res.status(409).json({ error: '保留中の承認要求があります。先に許可または拒否してください。' });
        return;
      }

      // Compute the (sanitized) attachment filenames now — they go into the
      // comment and the instruction — but DEFER the actual file writes until
      // after the tool_request gate passes (below), so a rejected post never
      // leaves orphan files in input/.
      // 実書き込みはゲート通過まで遅延するが、確定ファイル名（同名は `名前 (2).ext`
      // に避ける）は instruction / コメントに先に載せる必要があるため、ここで予測して
      // 確定させる。既存 input/ ファイルは上書きしない。
      const inputDir = task?.workspacePath ? join(task.workspacePath, 'input') : null;
      const reservedNames = new Set<string>();
      const writePlan: Array<{ name: string; buf: Buffer }> = [];
      for (const att of attachments ?? []) {
        if (!att.name || !att.contentBase64 || !inputDir) continue;
        const safeName = att.name.replace(/[\\/]/g, '_');
        const finalName = reserveAttachmentName(inputDir, safeName, reservedNames);
        writePlan.push({ name: finalName, buf: Buffer.from(att.contentBase64, 'base64') });
      }
      const savedFileNames = writePlan.map(p => p.name);
      const writeAttachments = () => {
        if (writePlan.length > 0 && inputDir) {
          mkdirSync(inputDir, { recursive: true });
          for (const p of writePlan) {
            writeFileSync(join(inputDir, p.name), p.buf);
          }
        }
      };

      // Re-fetch the latest job AFTER saving attachments for the interjection
      // check below. The authoritative tool_request gate is enforced atomically
      // inside createJobIfNoPending (same transaction as the insert) — see the
      // blockOnToolRequestPause handling below — so there is no check-then-act
      // race with the engine parking the job.
      const prevJob = await repo.getLatestJobForIssue(localTaskRepoName(taskId), taskId);

      // A job actively running injects the comment at its next iteration — save
      // it as an interjection and don't touch the job queue.
      const isRunning = prevJob && (prevJob.status === 'running' || prevJob.status === 'dispatching' || prevJob.status === 'waiting_subtasks');

      if (isRunning) {
        // Interjection: injected into the running job at its next iteration.
        writeAttachments();
        const comment = await repo.addLocalTaskComment(taskId, author, body, 'interjection', savedFileNames);
        logger.info(`[local-tasks-api] interjection: comment ${comment.id} saved for ${prevJob!.status} job ${prevJob!.id} on task ${taskId}`);
        res.status(201).json({ comment, jobId: prevJob!.id, interjection: true });
        return;
      }

      const askCount = prevJob?.status === 'waiting_human' ? prevJob.askCount : 0;
      const resumeMovement = prevJob?.status === 'waiting_human' ? prevJob.resumeMovement : null;

      // Build instruction with attachment info (savedFileNames computed above)
      const instruction = savedFileNames.length > 0
        ? `${body}\n\n添付ファイル（input/ に保存済み）: ${savedFileNames.join(', ')}`
        : body;

      // Atomically reuse a still-pending job (e.g. a queued job from a comment
      // sent a moment earlier) or create one. Without this, rapid/concurrent
      // comments each spawned a job; the newest (queued) became latestJob and the
      // task showed "Inbox" while an older job ran in the background.
      // The tool_request gate is enforced inside the same transaction.
      const { job, created, blockedByToolRequest } = repo.createJobIfNoPending({
        repo: localTaskRepoName(taskId),
        issueNumber: taskId,
        instruction,
        pieceName: task!.pieceName,
        askCount,
        resumeMovement,
        role: prevJob?.requiredRole,
        ownerId: task!.ownerId,
        visibility: task!.visibility,
        visibilityScopeOrgId: task!.visibilityScopeOrgId,
        browserSessionProfileId: task!.browserSessionProfileId ?? null,
        spaceId: task!.spaceId ?? null,
      }, { blockOnToolRequestPause: true });
      // Blocked by a tool/package-approval pause → return BEFORE persisting the
      // comment so a rejected post leaves no orphan comment tied to no job.
      if (blockedByToolRequest) {
        res.status(409).json({ error: '保留中の承認要求があります。先に許可または拒否してください。' });
        return;
      }

      // Persist attachments + the comment only after the job decision succeeds.
      writeAttachments();
      const comment = await repo.addLocalTaskComment(taskId, author, body, 'comment', savedFileNames);
      if (created) {
        await repo.addAuditLog(job.id, 'job_queued_local_comment', author, { taskId });
      } else {
        logger.info(`[local-tasks-api] comment ${comment.id} appended to pending job ${job.id} (${job.status}) on task ${taskId}; no duplicate job created`);
      }

      res.status(201).json({ comment, jobId: job.id, reusedPending: !created });
    } catch (err) {
      logger.error(`Local task comment create API error: ${err}`);
      res.status(500).json({ error: 'Failed to post local task comment' });
    }
  });
}
