// API fichiers (P7) pour l'éditeur Monaco : lecture/écriture du contenu d'un fichier, strictement
// bornées à un worktree ENREGISTRÉ (WorktreeManager.assertPathInWorktree : anti `..`, anti symlink
// sortant). Authentifiée par le hook preValidation global. Un terminal web est déjà du RCE par
// conception, mais on borne quand même (défense en profondeur + moindre étonnement).
import type { FastifyInstance } from 'fastify';
import { readFile, writeFile, stat, mkdir } from 'node:fs/promises';
import { dirname, extname } from 'node:path';
import type { FileContentResponse, WriteFileRequest, WriteFileResponse } from '@arboretum/shared';
import type { WorktreeManager } from '../core/worktree-manager.js';
import type { Db } from '../db/index.js';
import { recordAudit } from '../core/audit-log.js';
import { sendManagerError } from './repos.js';

const FILE_MAX_BYTES = 2 * 1024 * 1024;

/** Langage Monaco déduit de l'extension (sous-ensemble courant ; Monaco a un fallback 'plaintext'). */
const LANG_BY_EXT: Record<string, string> = {
  '.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
  '.json': 'json', '.vue': 'vue', '.css': 'css', '.scss': 'scss', '.less': 'less', '.html': 'html', '.md': 'markdown',
  '.py': 'python', '.rb': 'ruby', '.go': 'go', '.rs': 'rust', '.java': 'java', '.c': 'c', '.h': 'c', '.cpp': 'cpp', '.cc': 'cpp',
  '.cs': 'csharp', '.php': 'php', '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.yml': 'yaml', '.yaml': 'yaml',
  '.toml': 'toml', '.xml': 'xml', '.sql': 'sql', '.swift': 'swift', '.kt': 'kotlin', '.dart': 'dart', '.lua': 'lua',
};

function languageFor(path: string): string | undefined {
  return LANG_BY_EXT[extname(path).toLowerCase()];
}

/** Détecte un fichier binaire par présence d'un octet NUL dans le buffer. */
function looksBinary(buf: Buffer): boolean {
  const n = Math.min(buf.length, 8192);
  for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
  return false;
}

export function registerFileRoutes(app: FastifyInstance, wt: WorktreeManager, db: Db): void {
  // Lecture du contenu d'un fichier (UTF-8) d'un worktree.
  app.get('/api/v1/repos/:id/files/content', async (req, reply) => {
    const { id } = req.params as { id: string };
    const q = req.query as { wt?: string; path?: string };
    if (typeof q.wt !== 'string' || q.wt === '' || typeof q.path !== 'string' || q.path === '') {
      return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'wt and path are required' } });
    }
    try {
      const abs = await wt.assertPathInWorktree(id, q.wt, q.path);
      const st = await stat(abs).catch(() => null);
      if (!st || !st.isFile()) return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No such file' } });
      if (st.size > FILE_MAX_BYTES) return reply.status(413).send({ error: { code: 'FILE_TOO_LARGE', message: `File exceeds ${FILE_MAX_BYTES} bytes` } });
      const buf = await readFile(abs);
      if (looksBinary(buf)) return reply.status(415).send({ error: { code: 'BINARY_FILE', message: 'Binary files are not editable' } });
      const lang = languageFor(q.path);
      const res: FileContentResponse = {
        path: q.path,
        content: buf.toString('utf-8'),
        encoding: 'utf-8',
        size: st.size,
        mtime: st.mtimeMs,
        ...(lang ? { language: lang } : {}),
      };
      return reply.send(res);
    } catch (err) {
      return sendManagerError(reply, err);
    }
  });

  // Écriture (création/modification) du contenu d'un fichier d'un worktree.
  app.put('/api/v1/repos/:id/files/content', async (req, reply) => {
    const { id } = req.params as { id: string };
    const body = req.body as Partial<WriteFileRequest> | null;
    if (!body || typeof body.wt !== 'string' || typeof body.path !== 'string' || typeof body.content !== 'string') {
      return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'wt, path and content are required' } });
    }
    if (Buffer.byteLength(body.content, 'utf-8') > FILE_MAX_BYTES) {
      return reply.status(413).send({ error: { code: 'FILE_TOO_LARGE', message: `Content exceeds ${FILE_MAX_BYTES} bytes` } });
    }
    try {
      const abs = await wt.assertPathInWorktree(id, body.wt, body.path);
      // Garde-fou conflit (P8) : si le client a chargé le fichier (baseMtime) et qu'il a changé
      // depuis (ex. Claude a écrit en parallèle), on refuse pour ne pas écraser silencieusement.
      if (typeof body.baseMtime === 'number') {
        const cur = await stat(abs).catch(() => null);
        if (cur && cur.isFile() && cur.mtimeMs !== body.baseMtime) {
          return reply.status(409).send({ error: { code: 'STALE_FILE', message: 'File changed on disk since it was loaded' } });
        }
      }
      await mkdir(dirname(abs), { recursive: true }); // dirname reste sous le worktree (abs validé)
      await writeFile(abs, body.content, 'utf-8');
      const after = await stat(abs);
      recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'file.write', resourceId: id, details: { path: body.path } });
      const res: WriteFileResponse = { ok: true, size: Buffer.byteLength(body.content, 'utf-8'), mtime: after.mtimeMs };
      return reply.send(res);
    } catch (err) {
      return sendManagerError(reply, err);
    }
  });
}
