import express, { type Application, type Request, type Response } from 'express';
import { mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { localTaskRepoName } from '../db/repository.js';
import { logger } from '../logger.js';
import { resolveJobScheduling } from '../scheduling.js';
import { parseTaskId, validateCreateTaskBody } from './validation.js';
import { getLocalWorkspacePath, checkTaskOwnership, canViewTask, resolveSpaceAccess, reserveAttachmentName } from './local-api-helpers.js';
import { buildTitleFallback } from '../title-generation.js';
import { resolveTaskWorkspaceDir, ensureWorkspaceDirs } from '../spaces/workspace-resolver.js';
import { spaceRunsDir } from '../spaces/runtime-paths.js';
import { type LocalTasksDeps, makeDynamicJson } from './local-tasks-shared.js';

/**
 * Core task lifecycle CRUD: list / create / detail / patch / delete.
 */
export function registerLocalTaskCrudRoutes(app: Application, deps: LocalTasksDeps): void {
  const { repo, worktreeDir, sessRepo, userFolderRoot, noAuthOwner, opts } = deps;
  const dynamicJson = makeDynamicJson(opts.getMaxUploadMb);

  app.get('/api/local/tasks', async (req: Request, res: Response) => {
    try {
      const viewer = req.user as Express.User | undefined;
      const tasks = await repo.listLocalTasks(viewer ? { viewer } : {});
      res.json({ tasks });
    } catch (err) {
      logger.error(`Local tasks list API error: ${err}`);
      res.status(500).json({ error: 'Failed to fetch local tasks' });
    }
  });

  app.post('/api/local/tasks', dynamicJson, async (req: Request, res: Response) => {
    try {
      const validation = validateCreateTaskBody(req.body);
      if (!validation.valid) {
        res.status(400).json({ error: validation.error });
        return;
      }
      const body = validation.data;

      // Visibility extraction + validation
      const rawVisibility = req.body?.visibility ?? 'private';
      if (!['private', 'org', 'public'].includes(rawVisibility)) {
        res.status(400).json({ error: 'invalid visibility' });
        return;
      }
      const visibility = rawVisibility as 'private' | 'org' | 'public';
      const rawScopeOrgId = req.body?.visibilityScopeOrgId;
      const visibilityScopeOrgId: string | null =
        typeof rawScopeOrgId === 'string' && rawScopeOrgId.length > 0 ? rawScopeOrgId : null;
      if (visibility === 'org') {
        const orgIds = (req.user as Express.User | undefined)?.orgIds ?? [];
        if (!visibilityScopeOrgId || !orgIds.includes(visibilityScopeOrgId)) {
          res.status(400).json({ error: 'visibility_scope_org_id must be one of your orgs' });
          return;
        }
      }

      // Optional browser session profile binding. Owner-scoped check
      // (sessRepo.getProfileById enforces owner_id = req.user.id) prevents
      // user A from binding user B's profile to their task.
      let browserSessionProfileId: number | null = null;
      const rawProfileId = req.body?.browserSessionProfileId;
      if (rawProfileId !== undefined && rawProfileId !== null && rawProfileId !== '') {
        const n = Number(rawProfileId);
        if (!Number.isInteger(n) || n <= 0) {
          res.status(400).json({ error: 'browserSessionProfileId must be a positive integer' });
          return;
        }
        if (sessRepo) {
          const userId = (req.user as Express.User | undefined)?.id;
          if (!userId) {
            res.status(400).json({ error: 'browserSessionProfileId requires an authenticated user' });
            return;
          }
          const owned = sessRepo.getProfileById(n, userId);
          if (!owned) {
            res.status(400).json({ error: 'browser session profile not found or not owned by you' });
            return;
          }
        }
        browserSessionProfileId = n;
      }

      const userTitle = (body.title ?? '').trim();
      const rawPiece = (body.piece ?? 'auto').trim();
      const attachmentNames = (body.attachments ?? []).map((a: { name?: string }) => a.name).filter(Boolean) as string[];

      // Title is NOT generated by an LLM at creation time anymore — that fired a
      // second concurrent LLM request per task and churned gateway backend
      // slots. Instead we set a cheap synchronous fallback now, and the agent
      // upgrades it during the run by deriving from the Mission Brief goal
      // (see Repository.updateMissionBriefSync). On-demand AI regeneration is
      // available via POST /api/local/tasks/:id/regenerate-title.
      const autoSelectedPiece = (rawPiece === 'auto' && opts.selectPiece)
        ? await opts.selectPiece(body.body.trim(), attachmentNames, (req.user as Express.User | undefined)?.id)
            .catch((e: unknown) => { logger.warn(`Piece classification failed: ${e}`); return 'chat'; })
        : rawPiece;

      const taskTitle = userTitle || buildTitleFallback(body.body.trim());
      const titleSource: 'auto' | 'user' = userTitle ? 'user' : 'auto';
      const piece = autoSelectedPiece;
      const profile = body.profile ?? 'auto';
      const outputFormat = body.outputFormat ?? 'markdown';
      const askPolicy = body.askPolicy ?? 'low';
      const priority = body.priority ?? 'medium';
      const scheduling = resolveJobScheduling({
        role: profile,
        pieceName: piece,
        instruction: body.body.trim(),
      });

      // Per-task options (e.g. { mcpDisabled, skillsDisabled })
      const rawOptions = req.body?.options;
      const taskOptions: Record<string, unknown> =
        rawOptions && typeof rawOptions === 'object' && !Array.isArray(rawOptions)
          ? rawOptions as Record<string, unknown>
          : {};

      // spaceId は閲覧できるスペースのみ受理する（見えない/存在しない ID は
      // 個人スペース既定に倒す）。これで他スペースのワークスペースへの紛れ込みを防ぐ。
      let resolvedSpaceId: string | null = null;
      if (body.spaceId) {
        const reqSpace = await repo.getSpace(body.spaceId, { viewer: req.user as Express.User | undefined });
        if (reqSpace) {
          resolvedSpaceId = reqSpace.id;
        } else {
          logger.warn(`[local-tasks] create: spaceId=${body.spaceId} not visible to user; falling back to personal space`);
        }
      }

      // Tasks created inside a space are ALWAYS private: access is granted
      // exclusively by space membership (the shared-space OR-branch in
      // buildVisibilityWhere lets every member see all rows of their space).
      // Forcing private here prevents an org/public selection from leaking the
      // chat to non-members. Per-chat visibility is intentionally not selectable.
      const effectiveVisibility = resolvedSpaceId ? 'private' : visibility;
      const effectiveScopeOrgId = effectiveVisibility === 'org' ? visibilityScopeOrgId : null;

      const task = await repo.createLocalTask({
        title: taskTitle,
        titleSource,
        body: body.body.trim(),
        pieceName: piece,
        profile,
        outputFormat,
        askPolicy,
        priority,
        workspaceMode: body.workspaceMode,
        spaceId: resolvedSpaceId,
        ownerId: req.user?.id ?? noAuthOwner,
        visibility: effectiveVisibility,
        visibilityScopeOrgId: effectiveScopeOrgId,
        browserSessionProfileId,
        options: taskOptions,
      });

      // 実効スペースを解決し、mode に応じてワークスペースパスを決める。
      // 既定は persistent（個人/案件スペースの共有ツリー）。ephemeral は使い捨て。
      // resolveTaskFolderContext は副作用で個人スペース生成も担保する。
      const mode = body.workspaceMode ?? 'persistent';
      await repo.resolveTaskFolderContext(task.id, userFolderRoot);
      const space = task.spaceId
        ? await repo.getSpace(task.spaceId)
        : await repo.ensurePersonalSpace(task.ownerId ?? 'local');
      const effectiveWorktreeDir = worktreeDir ?? './data/worktrees';
      const workspacePath = space
        ? resolveTaskWorkspaceDir(mode, space, effectiveWorktreeDir, task.id)
        : getLocalWorkspacePath(worktreeDir, task.id); // フォールバック（スペース解決不能時のみ）
      ensureWorkspaceDirs(workspacePath);
      // 計画5 (Phase B): persistent スペースタスクは実行ログを成果物ツリーから分離し、
      // タスク単位の {worktreeDir}/space/{id}/runs/{taskId} に書く。ephemeral は
      // runtime_dir=null のままで、worker/runPiece が ephemeral/{taskId}/logs に
      // フォールバックする（後方互換）。
      let runtimeDir: string | undefined;
      if (mode === 'persistent' && space) {
        runtimeDir = spaceRunsDir(effectiveWorktreeDir, space.id, task.id);
        mkdirSync(runtimeDir, { recursive: true });
      }
      await repo.updateLocalTask(task.id, { workspacePath, ...(runtimeDir ? { runtimeDir } : {}) });

      // Save attachments to input/. The resulting (sanitized, dedupe-renamed)
      // filenames are stored on the initial request comment so the UI can render
      // download links, and reused below to tell the agent which files landed in
      // input/. 同名の既存ファイルは上書きせず `名前 (2).ext` に避ける
      // （コメント追加パス POST :id/comments と同じ挙動に揃える）。
      const inputDir = join(workspacePath, 'input');
      mkdirSync(inputDir, { recursive: true });
      const reservedNames = new Set<string>();
      const savedFileNames: string[] = [];
      for (const att of body.attachments ?? []) {
        if (!att.name || !att.contentBase64) continue;
        const safeName = att.name.replace(/[\\/]/g, '_');
        const finalName = reserveAttachmentName(inputDir, safeName, reservedNames);
        writeFileSync(join(inputDir, finalName), Buffer.from(att.contentBase64, 'base64'));
        savedFileNames.push(finalName);
      }

      await repo.addLocalTaskComment(task.id, 'user', body.body.trim(), 'request', savedFileNames);

      const metadataBlock = [
        '---',
        `ui_profile: ${scheduling.role}`,
        `ui_output_format: ${outputFormat}`,
        `ui_ask_policy: ${askPolicy}`,
        `ui_priority: ${priority}`,
        '---',
      ].join('\n');
      const attachmentBlock = savedFileNames.length > 0
        ? `添付ファイル（input/ に保存済み）: ${savedFileNames.join(', ')}\n\n`
        : '';
      const instruction = `${taskTitle}\n\n${body.body.trim()}\n\n${attachmentBlock}${metadataBlock}`.trim();
      // Merge task options into job payload so the worker can read them at runtime.
      const hasOptions = Object.keys(taskOptions).length > 0;
      const job = await repo.createJob({
        repo: localTaskRepoName(task.id),
        issueNumber: task.id,
        instruction,
        pieceName: piece,
        role: scheduling.role,
        ownerId: task.ownerId,
        visibility: task.visibility,
        visibilityScopeOrgId: task.visibilityScopeOrgId,
        browserSessionProfileId: task.browserSessionProfileId ?? null,
        spaceId: task.spaceId ?? null,
        payload: hasOptions ? JSON.stringify({ options: taskOptions }) : undefined,
      });
      await repo.addAuditLog(job.id, 'job_queued_local_create', 'local-ui', { taskId: task.id });

      if (rawPiece === 'auto') {
        await repo.addAuditLog(job.id, 'piece_auto_selected', 'piece-classifier', {
          selectedPiece: piece,
        });
      }

      const created = await repo.getLocalTask(task.id);
      res.status(201).json({ task: created, jobId: job.id });
    } catch (err) {
      logger.error(`Create local task API error: ${err}`);
      res.status(500).json({ error: 'Failed to create local task' });
    }
  });

  app.get('/api/local/tasks/:taskId', 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;
      res.json({ task });
    } catch (err) {
      logger.error(`Local task detail API error: ${err}`);
      res.status(500).json({ error: 'Failed to fetch local task' });
    }
  });

  app.patch('/api/local/tasks/:taskId', 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 task = await repo.getLocalTask(taskId, { viewer: req.user as Express.User | undefined });
      if (!checkTaskOwnership(req, res, task)) return;

      const updates: { title?: string; titleSource?: 'user'; visibility?: 'private' | 'org' | 'public'; visibilityScopeOrgId?: string | null } = {};
      if (req.body.title !== undefined) {
        if (typeof req.body.title !== 'string') {
          res.status(400).json({ error: 'title must be a string' }); return;
        }
        const trimmed = req.body.title.trim();
        if (!trimmed) { res.status(400).json({ error: 'title must not be empty' }); return; }
        if (trimmed.length > 200) { res.status(400).json({ error: 'title must be 200 characters or less' }); return; }
        // Manual edit pins the title: the agent never auto-overwrites a user title.
        updates.title = trimmed;
        updates.titleSource = 'user';
      }
      if (req.body.visibility !== undefined) {
        const v = req.body.visibility;
        if (!['private', 'org', 'public'].includes(v)) {
          res.status(400).json({ error: 'invalid visibility' }); return;
        }
        updates.visibility = v;
      }
      if (req.body.visibilityScopeOrgId !== undefined) {
        updates.visibilityScopeOrgId = req.body.visibilityScopeOrgId ?? null;
      }
      if (updates.visibility === 'org') {
        const orgIds = (req.user as Express.User | undefined)?.orgIds ?? [];
        const scopeId = updates.visibilityScopeOrgId ?? task!.visibilityScopeOrgId ?? null;
        if (!scopeId || !orgIds.includes(scopeId)) {
          res.status(400).json({ error: 'visibility_scope_org_id must be one of your orgs' }); return;
        }
        updates.visibilityScopeOrgId = scopeId;
      }
      if (updates.visibility && updates.visibility !== 'org') {
        updates.visibilityScopeOrgId = null;
      }
      await repo.updateLocalTask(taskId, updates);
      const refreshed = await repo.getLocalTask(taskId, { viewer: req.user as Express.User | undefined });
      if ((updates.visibility !== undefined || updates.visibilityScopeOrgId !== undefined) && refreshed) {
        await repo.updateJobsVisibilityForTask(taskId, {
          visibility: refreshed.visibility ?? 'private',
          visibilityScopeOrgId: refreshed.visibilityScopeOrgId ?? null,
        });
      }
      res.json({ task: refreshed });
    } catch (err) {
      logger.error(`Patch local task API error: ${err}`);
      res.status(500).json({ error: 'Failed to update task' });
    }
  });

  app.delete('/api/local/tasks/:taskId', 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 task = await repo.getLocalTask(taskId, { viewer: req.user as Express.User | undefined });
      if (!checkTaskOwnership(req, res, task)) return;
      await repo.deleteLocalTask(taskId, worktreeDir);
      res.json({ ok: true });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes('has an active job')) {
        res.status(409).json({ error: 'Cannot delete task with running jobs' });
        return;
      }
      logger.error(`Delete local task API error: ${err}`);
      res.status(500).json({ error: 'Failed to delete local task' });
    }
  });
}
