import express, { type Application, type Request, type Response } from 'express';
import { mkdirSync, readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
import { join, extname } from 'path';
import { Repository, localTaskRepoName } from '../db/repository.js';
import { logger } from '../logger.js';
import { parseTaskId } from './validation.js';
import { ensurePathWithin, isPathEscapeError, serializeLocalFileEntry, checkTaskOwnership, canViewTask, setUntrustedFileResponseHeaders } from './local-api-helpers.js';

export interface LocalFilesApiOptions {
  /** Whether the auth subsystem is wired. When false (no-auth single-user
   *  deployment) there is no req.user, so the sole local operator owns every
   *  task and is allowed to open their own generated HTML with trusted=1.
   *  Defaults to true (owner identity comes from req.user). */
  authActive?: boolean;
}

export function mountLocalFilesApi(
  app: Application,
  repo: Repository,
  opts: LocalFilesApiOptions = {},
): void {
  const authActive = opts.authActive ?? true;

  app.get('/api/local/tasks/:taskId/files', 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)) return;
      if (!task?.workspacePath) {
        res.status(404).json({ error: 'Workspace not found' });
        return;
      }

      const section = String(req.query.section ?? 'input');
      if (!['workspace', 'input', 'output', 'logs'].includes(section)) {
        res.status(400).json({ error: 'section must be workspace, input, output, or logs' });
        return;
      }

      const relativeDir = String(req.query.path ?? '').replace(/^\/+/, '').replace(/\/+$/, '');
      const rootDir = section === 'workspace' ? task.workspacePath : join(task.workspacePath, section);
      mkdirSync(rootDir, { recursive: true });
      const dirPath = ensurePathWithin(rootDir, relativeDir);
      const entries = readdirSync(dirPath, { withFileTypes: true }).map((entry) => {
        const stat = statSync(join(dirPath, entry.name));
        return serializeLocalFileEntry(relativeDir, entry.name, entry.isDirectory(), stat.size, stat.mtime);
      });
      res.json({ basePath: section, path: relativeDir, entries });
    } catch (err) {
      if (isPathEscapeError(err)) {
        res.status(400).json({ error: 'Path escapes workspace' });
        return;
      }
      logger.error(`Local files list API error: ${err}`);
      res.status(500).json({ error: 'Failed to list files' });
    }
  });

  app.get('/api/local/tasks/:taskId/files/content', 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)) return;
      if (!task?.workspacePath) {
        res.status(404).json({ error: 'Workspace not found' });
        return;
      }

      const section = String(req.query.section ?? 'input');
      if (!['workspace', 'input', 'output', 'logs'].includes(section)) {
        res.status(400).json({ error: 'section must be workspace, input, output, or logs' });
        return;
      }
      const relativePath = String(req.query.path ?? '').replace(/^\/+/, '');
      if (!relativePath) {
        res.status(400).json({ error: 'path is required' });
        return;
      }
      const rootDir = section === 'workspace' ? task.workspacePath : join(task.workspacePath, section);
      const filePath = ensurePathWithin(rootDir, relativePath);
      const stat = statSync(filePath);
      if (!stat.isFile()) {
        res.status(400).json({ error: 'path must point to a file' });
        return;
      }
      setUntrustedFileResponseHeaders(res);
      res.setHeader('Content-Type', 'text/plain; charset=utf-8');
      res.send(readFileSync(filePath, 'utf-8'));
    } catch (err) {
      if (isPathEscapeError(err)) {
        res.status(400).json({ error: 'Path escapes workspace' });
        return;
      }
      logger.error(`Local file content API error: ${err}`);
      res.status(500).json({ error: 'Failed to read file' });
    }
  });

  app.get('/api/local/tasks/:taskId/files/raw', 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)) return;
      if (!task?.workspacePath) {
        res.status(404).json({ error: 'Workspace not found' });
        return;
      }

      const section = String(req.query.section ?? 'input');
      if (!['workspace', 'input', 'output', 'logs'].includes(section)) {
        res.status(400).json({ error: 'section must be workspace, input, output, or logs' });
        return;
      }
      const relativePath = String(req.query.path ?? '').replace(/^\/+/, '');
      if (!relativePath) {
        res.status(400).json({ error: 'path is required' });
        return;
      }
      const rootDir = section === 'workspace' ? task.workspacePath : join(task.workspacePath, section);
      const filePath = ensurePathWithin(rootDir, relativePath);
      const stat = statSync(filePath);
      if (!stat.isFile()) {
        res.status(400).json({ error: 'path must point to a file' });
        return;
      }
      // trusted=1 drops the CSP sandbox so the owner's own generated HTML can
      // run on the app origin. STRICTLY owner-only — self-XSS at worst:
      //  - org/public visibility lets other users VIEW the task, but serving
      //    someone else's HTML unsandboxed here would be stored XSS against
      //    the viewer;
      //  - admins are excluded too: another user's HTML running in an ADMIN
      //    session would be a user→admin privilege-escalation lure.
      // No-auth single-user mode has no req.user; the sole operator owns every
      // task, so they are the owner for this purpose (self-XSS only — there is
      // no second principal to attack).
      const trustedAllowed = authActive
        ? !!viewer && task.ownerId != null && viewer.id === task.ownerId
        : true;
      const trustedHtml = req.query.trusted === '1' && /\.html?$/i.test(filePath) && trustedAllowed;
      if (!trustedHtml) {
        setUntrustedFileResponseHeaders(res);
      }
      res.type(extname(filePath) || 'application/octet-stream');
      res.send(readFileSync(filePath));
    } catch (err) {
      if (isPathEscapeError(err)) {
        res.status(400).json({ error: 'Path escapes workspace' });
        return;
      }
      logger.error(`Local file raw API error: ${err}`);
      res.status(500).json({ error: 'Failed to read raw file' });
    }
  });

  app.put('/api/local/tasks/:taskId/files/content', 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;
      if (!task?.workspacePath) {
        res.status(404).json({ error: 'Workspace not found' });
        return;
      }
      const latestJob = await repo.getLatestJobForIssue(localTaskRepoName(taskId), taskId);
      if (latestJob && ['running', 'dispatching'].includes(latestJob.status)) {
        res.status(409).json({ error: 'Cannot edit files while job is running' });
        return;
      }
      const section = String(req.body?.section ?? '');
      if (section !== 'output') {
        res.status(400).json({ error: 'Only output files can be edited' });
        return;
      }
      const relativePath = String(req.body?.path ?? '').replace(/^\/+/, '');
      if (!relativePath) {
        res.status(400).json({ error: 'path is required' });
        return;
      }
      const content = req.body?.content;
      if (typeof content !== 'string') {
        res.status(400).json({ error: 'content is required' });
        return;
      }
      // PUT (inline edit) is output-only; section is narrowed to 'output' above.
      const rootDir = join(task.workspacePath, section);
      const filePath = ensurePathWithin(rootDir, relativePath);
      writeFileSync(filePath, content, 'utf-8');
      res.json({ ok: true });
    } catch (err) {
      if (isPathEscapeError(err)) {
        res.status(400).json({ error: 'Path escapes workspace' });
        return;
      }
      logger.error(`Local file update API error: ${err}`);
      res.status(500).json({ error: 'Failed to update file' });
    }
  });
}
