import path from "node:path";
import { Router, type Request, type Response } from "express";
import multer from "multer";
import type { Db } from "@paperclipai/db";
import { uploadChatFileSchema, createFileReferenceSchema } from "@paperclipai/shared";
import type { StorageService } from "../storage/types.js";
import { chatFileService, deriveCategory } from "../services/chat-files.js";
import { chatService } from "../services/chat.js";
import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
import { assertBoard, assertCompanyAccess } from "./authz.js";
import { placeholderService } from "../services/placeholder-service.js";
import { gitFileService } from "../services/git-file-service.js";
import { resolveDefaultStorageDir } from "../home-paths.js";

const fileUpload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 },
});

async function runSingleFileUpload(
  upload: ReturnType<typeof multer>,
  req: Request,
  res: Response,
) {
  await new Promise<void>((resolve, reject) => {
    upload.single("file")(req, res, (err: unknown) => {
      if (err) reject(err);
      else resolve();
    });
  });
}

export function chatFileRoutes(db: Db, storage: StorageService) {
  const router = Router();
  const fileSvc = chatFileService(db);
  const convSvc = chatService(db);
  const phSvc = placeholderService();
  const gitSvc = gitFileService();
  const storageDir = resolveDefaultStorageDir();

  // POST /conversations/:id/files — Upload a file to a conversation
  router.post("/conversations/:id/files", async (req, res) => {
    assertBoard(req);

    const conversationId = req.params.id as string;

    try {
      await runSingleFileUpload(fileUpload, req, res);
    } catch (err) {
      if (err instanceof multer.MulterError) {
        if (err.code === "LIMIT_FILE_SIZE") {
          res.status(422).json({ error: `File exceeds ${MAX_ATTACHMENT_BYTES} bytes` });
          return;
        }
        res.status(400).json({ error: err.message });
        return;
      }
      throw err;
    }

    const file = (req as Request & { file?: { mimetype: string; buffer: Buffer; originalname: string; size: number } }).file;
    if (!file) {
      res.status(400).json({ error: "Missing file field 'file'" });
      return;
    }

    const contentType = (file.mimetype || "").toLowerCase();
    if (!isAllowedContentType(contentType)) {
      res.status(422).json({ error: `Unsupported file type: ${contentType || "unknown"}` });
      return;
    }

    const parsedMeta = uploadChatFileSchema.safeParse(req.body ?? {});
    if (!parsedMeta.success) {
      res.status(400).json({ error: "Invalid metadata", details: parsedMeta.error.issues });
      return;
    }

    // Resolve companyId from conversation
    const conversation = await convSvc.getConversation(conversationId);
    assertCompanyAccess(req, conversation.companyId);

    const stored = await storage.putFile({
      companyId: conversation.companyId,
      namespace: "chat-files",
      originalFilename: file.originalname || null,
      contentType,
      body: file.buffer,
    });

    const chatFile = await fileSvc.create(conversation.companyId, {
      conversationId,
      messageId: parsedMeta.data.messageId ?? null,
      filename: stored.originalFilename ?? file.originalname ?? "upload",
      originalFilename: file.originalname ?? "upload",
      mimeType: contentType,
      sizeBytes: stored.byteSize,
      objectKey: stored.objectKey,
      sha256: stored.sha256,
      source: parsedMeta.data.source,
      category: deriveCategory(contentType),
      projectId: parsedMeta.data.projectId ?? null,
    });

    // Git-track the uploaded file (non-blocking)
    gitSvc.commitFile(storageDir, stored.objectKey, `Upload: ${file.originalname ?? "file"}`).catch(() => {});

    // Track placeholder if agent-generated and project-scoped
    if (parsedMeta.data.source === "agent_generated" && chatFile.projectId) {
      const projectDir = path.join(storageDir, "..", "..", "projects", chatFile.projectId);
      phSvc.addEntry(projectDir, {
        fileId: chatFile.id,
        filename: chatFile.originalFilename,
        description: "Generated by agent",
      }).catch(() => {});
    }

    res.status(201).json({
      file: chatFile,
      contentPath: `/api/files/${chatFile.id}/content`,
    });
  });

  // GET /conversations/:id/files — List files for a conversation
  router.get("/conversations/:id/files", async (req, res) => {
    assertBoard(req);

    const conversationId = req.params.id as string;
    const conversation = await convSvc.getConversation(conversationId);
    assertCompanyAccess(req, conversation.companyId);

    const files = await fileSvc.listByConversation(conversationId);
    res.json({ items: files });
  });

  // GET /files/:fileId/history — Git version history for a file
  router.get("/files/:fileId/history", async (req, res) => {
    assertBoard(req);

    const fileId = req.params.fileId as string;
    const chatFile = await fileSvc.getById(fileId);
    if (!chatFile) {
      res.status(404).json({ error: "File not found" });
      return;
    }
    assertCompanyAccess(req, chatFile.companyId);

    const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
    const entries = await gitSvc.getLog(storageDir, chatFile.objectKey, limit);
    res.json({ items: entries });
  });

  // GET /files/:fileId/content — Serve file content (download/preview)
  router.get("/files/:fileId/content", async (req, res, next) => {
    assertBoard(req);

    const fileId = req.params.fileId as string;
    const chatFile = await fileSvc.getById(fileId);
    if (!chatFile) {
      res.status(404).json({ error: "File not found" });
      return;
    }
    assertCompanyAccess(req, chatFile.companyId);

    const object = await storage.getObject(chatFile.companyId, chatFile.objectKey);
    const responseContentType = chatFile.mimeType || object.contentType || "application/octet-stream";

    res.setHeader("Content-Type", responseContentType);
    const contentLength = object.contentLength ?? chatFile.sizeBytes;
    if (contentLength) {
      res.setHeader("Content-Length", String(contentLength));
    }
    res.setHeader("Cache-Control", "private, max-age=60");
    res.setHeader("X-Content-Type-Options", "nosniff");

    const isImage = responseContentType.startsWith("image/");
    const filename = chatFile.originalFilename ?? chatFile.filename ?? "file";
    const sanitizedFilename = filename.replaceAll('"', "");
    if (isImage) {
      res.setHeader("Content-Disposition", `inline; filename="${sanitizedFilename}"`);
    } else {
      res.setHeader("Content-Disposition", `attachment; filename="${sanitizedFilename}"`);
    }

    object.stream.on("error", (err) => {
      next(err);
    });
    object.stream.pipe(res);
  });

  // POST /files/:fileId/references — Create cross-conversation reference
  router.post("/files/:fileId/references", async (req, res) => {
    assertBoard(req);

    const fileId = req.params.fileId as string;
    const chatFile = await fileSvc.getById(fileId);
    if (!chatFile) {
      res.status(404).json({ error: "File not found" });
      return;
    }
    assertCompanyAccess(req, chatFile.companyId);

    const parsed = createFileReferenceSchema.safeParse({ fileId, ...(req.body ?? {}) });
    if (!parsed.success) {
      res.status(400).json({ error: "Invalid reference data", details: parsed.error.issues });
      return;
    }

    const reference = await fileSvc.createReference({
      fileId,
      conversationId: chatFile.conversationId ?? "",
      messageId: parsed.data.messageId,
    });

    res.status(201).json(reference);
  });

  // PATCH /files/:fileId/promote — Promote chat file to project scope
  router.patch("/files/:fileId/promote", async (req, res) => {
    assertBoard(req);

    const fileId = req.params.fileId as string;
    const chatFile = await fileSvc.getById(fileId);
    if (!chatFile) {
      res.status(404).json({ error: "File not found" });
      return;
    }
    assertCompanyAccess(req, chatFile.companyId);

    const { projectId } = req.body ?? {};
    if (!projectId || typeof projectId !== "string") {
      res.status(400).json({ error: "projectId is required" });
      return;
    }

    const updated = await fileSvc.promoteToProject(fileId, projectId);
    if (!updated) {
      res.status(404).json({ error: "File not found" });
      return;
    }
    res.json(updated);
  });

  // POST /files/:fileId/replace — Replace a placeholder with a final asset
  router.post("/files/:fileId/replace", async (req, res) => {
    assertBoard(req);
    const fileId = req.params.fileId as string;
    const oldFile = await fileSvc.getById(fileId);
    if (!oldFile) { res.status(404).json({ error: "File not found" }); return; }
    assertCompanyAccess(req, oldFile.companyId);

    const { newFileId } = req.body ?? {};
    if (!newFileId || typeof newFileId !== "string") {
      res.status(400).json({ error: "newFileId is required" }); return;
    }
    const newFile = await fileSvc.getById(newFileId);
    if (!newFile) { res.status(404).json({ error: "Replacement file not found" }); return; }

    // Update placeholder manifest if project-scoped
    if (oldFile.projectId) {
      const projectDir = path.join(storageDir, "..", "..", "projects", oldFile.projectId);
      await phSvc.replaceEntry(projectDir, fileId, newFileId);
    }

    // Create reference linking replacement to original context
    await fileSvc.createReference({
      fileId: newFileId,
      conversationId: oldFile.conversationId ?? "",
      messageId: oldFile.messageId ?? undefined,
    });

    res.json({ replaced: fileId, replacedBy: newFileId });
  });

  // PATCH /files/:fileId — Attach file to a message (set messageId)
  router.patch("/files/:fileId", async (req, res) => {
    assertBoard(req);

    const fileId = req.params.fileId as string;
    const chatFile = await fileSvc.getById(fileId);
    if (!chatFile) {
      res.status(404).json({ error: "File not found" });
      return;
    }
    assertCompanyAccess(req, chatFile.companyId);

    const { messageId } = req.body ?? {};
    if (!messageId || typeof messageId !== "string") {
      res.status(400).json({ error: "messageId is required" });
      return;
    }

    const updated = await fileSvc.attachToMessage(fileId, messageId);
    res.json(updated);
  });

  // POST /transcribe — Transcribe audio via local Whisper
  const audioUpload = multer({
    storage: multer.memoryStorage(),
    limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 },
  });

  async function runAudioUpload(
    upload: ReturnType<typeof multer>,
    req: Request,
    res: Response,
  ) {
    await new Promise<void>((resolve, reject) => {
      upload.single("audio")(req, res, (err: unknown) => {
        if (err) reject(err);
        else resolve();
      });
    });
  }

  router.post("/transcribe", async (req, res) => {
    assertBoard(req);

    try {
      await runAudioUpload(audioUpload, req, res);
    } catch (err) {
      if (err instanceof multer.MulterError) {
        res.status(400).json({ error: err.message });
        return;
      }
      throw err;
    }

    const file = (req as Request & { file?: { buffer: Buffer; mimetype: string } }).file;
    if (!file) {
      res.status(400).json({ error: "Missing audio field" });
      return;
    }

    // Write to temp file
    const { writeFile, unlink } = await import("node:fs/promises");
    const { tmpdir } = await import("node:os");
    const tmpPath = path.join(tmpdir(), `nexus-audio-${Date.now()}.webm`);

    try {
      await writeFile(tmpPath, file.buffer);

      // Try whisper CLI (whisper.cpp or openai-whisper)
      const { promisify } = await import("node:util");
      const { execFile: execFileCb } = await import("node:child_process");
      const execFileAsync = promisify(execFileCb);

      try {
        // Try whisper.cpp first (outputs transcription to stdout with --no-timestamps)
        const { stdout } = await execFileAsync("whisper-cpp", [
          "--model", "base.en",
          "--file", tmpPath,
          "--no-timestamps",
          "--output-txt",
        ], { timeout: 30000 });
        res.json({ text: stdout.trim() });
      } catch {
        try {
          // Fallback: openai-whisper Python CLI
          const { stdout } = await execFileAsync("whisper", [
            tmpPath,
            "--model", "base.en",
            "--output_format", "txt",
            "--output_dir", tmpdir(),
          ], { timeout: 60000 });
          // whisper CLI writes to a .txt file alongside the input
          const txtPath = tmpPath.replace(/\.webm$/, ".txt");
          try {
            const { readFile } = await import("node:fs/promises");
            const text = await readFile(txtPath, "utf-8");
            await unlink(txtPath).catch(() => {});
            res.json({ text: text.trim() });
          } catch {
            // Parse stdout as fallback
            res.json({ text: stdout.trim() });
          }
        } catch {
          res.status(503).json({
            error: "Whisper not available. Install whisper-cpp or openai-whisper for voice input.",
          });
        }
      }
    } finally {
      await unlink(tmpPath).catch(() => {});
    }
  });

  return router;
}
