import { getErrorMessage } from "../../utils/error-message.js";
import type { AuthenticatedRequest } from "../../../types/index.js";
import express, {
  type NextFunction,
  type Request,
  type Response,
} from "express";
import { randomUUID } from "crypto";
import multer from "multer";
import sharp from "sharp";
import {
  createConcurrencyLimiter,
  exceedsNormalizedImageSize,
  imageExtensionForFormat,
} from "./terminal-image-utils.js";
import { resolveTerminalImageStorageSettings } from "./terminal-image-storage-settings.js";
import {
  selectImageStorageMode,
  probeLocalImageVisibility,
  storeImageLocally,
  storeImageViaSftp,
  TerminalImageStorageError,
  type ImageSftpClient,
} from "./terminal-image-storage.js";
import { authLogger, databaseLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import { sessionManager } from "../../hosts/terminal/session-manager.js";
import {
  createCurrentCommandHistoryRepository,
  createCurrentHostResolutionRepository,
  createCurrentSettingsRepository,
} from "../repositories/factory.js";

const router = express.Router();

function isNonEmptyString(val: unknown): val is string {
  return typeof val === "string" && val.trim().length > 0;
}

const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();

// Browser image handoff for local terminal-agent workflows.
const imageUpload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 50 * 1024 * 1024,
    fields: 4,
    fieldSize: 64 * 1024,
    files: 1,
    parts: 5,
    headerPairs: 200,
  },
});
const imageUploadMiddleware = imageUpload.single("image");
const imageProcessingLimiter = createConcurrencyLimiter(4, 4);
const imageMultipartAdmissionLimiter = createConcurrencyLimiter(4, 4);
let imageUploadSequence = 0;

async function handleImageUploadMiddleware(
  req: Request,
  res: Response,
  next: NextFunction,
): Promise<void> {
  let releaseAdmission: (() => void) | undefined;
  try {
    releaseAdmission = await imageMultipartAdmissionLimiter.acquire();
  } catch {
    res.status(503).json({
      error: "Image upload capacity is temporarily unavailable",
      code: "IMAGE_UPLOAD_CAPACITY_EXCEEDED",
    });
    return;
  }

  imageUploadMiddleware(req, res, (error: unknown) => {
    try {
      if (!error) {
        next();
        return;
      }
      if (error instanceof multer.MulterError) {
        databaseLogger.warn("Image upload multipart request rejected", {
          operation: "terminal_image_upload_multipart_rejected",
          code: error.code,
          field: error.field,
          contentType: req.headers["content-type"]?.split(";", 1)[0],
        });
        res.status(400).json({
          error: "Image upload request rejected",
          code: error.code,
          field: error.field,
        });
        return;
      }
      databaseLogger.warn("Image upload multipart request malformed", {
        operation: "terminal_image_upload_multipart_invalid",
        contentType: req.headers["content-type"]?.split(";", 1)[0],
      });
      res.status(400).json({
        error: "Malformed image upload request",
        code: "IMAGE_MULTIPART_INVALID",
      });
    } finally {
      releaseAdmission?.();
    }
  });
}
function findTerminalSession(userId: string, instanceId: string) {
  return sessionManager
    .getUserSessions(userId)
    .find(
      (session) =>
        (session.attachedTabInstanceId ?? session.tabInstanceId) ===
          instanceId && session.isConnected,
    );
}

router.post(
  "/image-upload",
  authenticateJWT,
  requireDataAccess,
  handleImageUploadMiddleware,
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    const instanceId = req.body?.instanceId;
    if (!req.file) {
      return res.status(400).json({
        error: "Image required",
        code: "IMAGE_FILE_MISSING",
      });
    }
    if (!isNonEmptyString(userId)) {
      return res.status(400).json({
        error: "Missing terminal session",
        code: "IMAGE_SESSION_MISSING",
      });
    }

    const requestId = randomUUID();
    const sequence = ++imageUploadSequence;
    const source =
      req.body?.source === "file" || req.body?.source === "clipboard"
        ? req.body.source
        : undefined;
    const clientUploadTimestamp =
      typeof req.body?.clientUploadTimestamp === "string" &&
      !Number.isNaN(Date.parse(req.body.clientUploadTimestamp))
        ? req.body.clientUploadTimestamp
        : undefined;
    const serverReceivedAt = new Date().toISOString();
    databaseLogger.info("Terminal image upload received", {
      operation: "terminal_image_upload_received",
      requestId,
      sequence,
      source,
      clientUploadTimestamp,
      serverReceivedAt,
      bytes: req.file.size,
    });

    const storageSettings = await resolveTerminalImageStorageSettings(
      createCurrentSettingsRepository(),
    );
    const session = isNonEmptyString(instanceId)
      ? findTerminalSession(userId, instanceId)
      : undefined;
    let localHostVisible = false;
    if (storageSettings.localMappingConfigured && session?.sshConn) {
      localHostVisible = await probeLocalImageVisibility(
        session.sshConn,
        storageSettings,
      ).catch(() => false);
    }
    const storageMode = selectImageStorageMode(storageSettings, {
      remoteSftpAvailable: !!session?.sshConn,
      localHostVisible,
    });

    if (storageMode === "unavailable") {
      return res.status(503).json({
        error: "Image storage is unavailable",
        code: "IMAGE_STORAGE_UNAVAILABLE",
      });
    }

    if (storageMode === "local" && !storageSettings.localMappingConfigured) {
      return res.status(503).json({
        error: "Local image storage is not configured",
        code: "IMAGE_LOCAL_STORAGE_NOT_CONFIGURED",
      });
    }

    if (storageMode === "remote-sftp") {
      if (!isNonEmptyString(instanceId)) {
        return res.status(400).json({
          error: "Missing terminal session",
          code: "IMAGE_SESSION_MISSING",
        });
      }
      if (!session || !session.sshConn) {
        return res.status(409).json({
          error: "Terminal is not connected",
          code: "IMAGE_TERMINAL_NOT_CONNECTED",
        });
      }
    }

    let normalizedImage: Buffer;
    let releaseImageProcessingSlot: (() => void) | undefined;
    try {
      releaseImageProcessingSlot = await imageProcessingLimiter.acquire();
    } catch {
      return res.status(503).json({
        error: "Image upload capacity is temporarily exhausted",
        code: "IMAGE_UPLOAD_CAPACITY_EXCEEDED",
      });
    }
    try {
      const source = sharp(req.file.buffer, {
        failOn: "error",
        limitInputPixels: 40_000_000,
      });
      const { format } = await source.metadata();
      if (!imageExtensionForFormat(format)) {
        return res.status(400).json({
          error: "Unsupported image format",
          code: "IMAGE_FORMAT_UNSUPPORTED",
        });
      }
      normalizedImage = await source.rotate().png().toBuffer();
      if (exceedsNormalizedImageSize(normalizedImage.length)) {
        return res.status(413).json({
          error: "Normalized image is too large",
          code: "IMAGE_NORMALIZED_SIZE_LIMIT",
        });
      }
    } catch (error) {
      databaseLogger.warn("Image upload failed image decoding", {
        operation: "terminal_image_upload_decode",
        mimeType: req.file.mimetype,
        bytes: req.file.size,
        reason: getErrorMessage(error, "unknown"),
      });
      return res.status(400).json({
        error: "Invalid image data",
        code: "IMAGE_DECODE_FAILED",
      });
    } finally {
      releaseImageProcessingSlot?.();
    }

    let remoteSftp: ImageSftpClient | undefined;
    try {
      const stored =
        storageMode === "remote-sftp"
          ? await (async () => {
              remoteSftp = await new Promise<ImageSftpClient>(
                (resolve, reject) => {
                  let settled = false;
                  const timer = setTimeout(() => {
                    settled = true;
                    reject(new Error("SFTP channel acquisition timed out"));
                  }, 3_000);
                  session!.sshConn!.sftp((err, sftp) => {
                    if (settled) {
                      sftp?.end?.();
                      return;
                    }
                    settled = true;
                    clearTimeout(timer);
                    if (err) return reject(err);
                    resolve(sftp);
                  });
                },
              );
              return storeImageViaSftp(remoteSftp, normalizedImage, {
                ttlMs: storageSettings.ttlMs,
                maxCount: storageSettings.maxCount,
                maxBytes: storageSettings.maxBytes,
              });
            })()
          : await storeImageLocally(normalizedImage, storageSettings);

      res.json(stored);
    } catch (error) {
      if (error instanceof TerminalImageStorageError) {
        const status =
          error.code === "IMAGE_STORAGE_LIMIT_REACHED"
            ? 507
            : error.code === "IMAGE_REMOTE_WRITE_FAILED"
              ? 502
              : error.code === "IMAGE_REMOTE_QUOTA_UNAVAILABLE"
                ? 503
                : error.code === "IMAGE_LOCAL_INSPECTION_FAILED"
                  ? 503
                  : 500;
        databaseLogger.warn("Image upload storage write failed", {
          operation:
            error.code === "IMAGE_REMOTE_WRITE_FAILED"
              ? "terminal_image_upload_sftp_failed"
              : "terminal_image_upload_local_failed",
          code: error.code,
          userId,
          instanceId,
          reason: getErrorMessage(error.cause ?? error, "unknown"),
        });
        return res.status(status).json({
          error: error.message,
          code: error.code,
        });
      }
      databaseLogger.warn("Image upload failed to acquire remote channel", {
        operation: "terminal_image_upload_sftp_failed",
        code: "IMAGE_REMOTE_WRITE_FAILED",
        userId,
        instanceId,
        reason: getErrorMessage(error, "unknown"),
      });
      return res.status(502).json({
        error: "Failed to write image to the remote host",
        code: "IMAGE_REMOTE_WRITE_FAILED",
      });
    } finally {
      remoteSftp?.end?.();
    }
  },
);

/**
 * @openapi
 * /terminal/command_history:
 *   post:
 *     summary: Save command to history
 *     description: Saves a command to the command history for a specific host.
 *     tags:
 *       - Terminal
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               hostId:
 *                 type: integer
 *               command:
 *                 type: string
 *     responses:
 *       201:
 *         description: Command saved successfully.
 *       400:
 *         description: Missing required parameters.
 *       500:
 *         description: Failed to save command.
 */
router.post(
  "/command_history",
  authenticateJWT,
  requireDataAccess,
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    const { hostId, command } = req.body;

    if (!isNonEmptyString(userId) || !hostId || !isNonEmptyString(command)) {
      authLogger.warn("Invalid command history save request", {
        operation: "command_history_save",
        userId,
        hasHostId: !!hostId,
        hasCommand: !!command,
      });
      return res.status(400).json({ error: "Missing required parameters" });
    }

    const sensitivePatterns = [
      /passw(or)?d/i,
      /\bsecret\b/i,
      /\btoken\b/i,
      /\bapi.?key\b/i,
      /PASS(WORD)?=/i,
      /AWS_SECRET/i,
      /mysql\b.*-p/i,
      /sudo\s+-S\b/,
      /htpasswd/i,
      /sshpass/i,
      /curl\b.*-u\s/i,
      /export\b.*(?:PASSWORD|SECRET|TOKEN|KEY)=/i,
    ];

    const trimmedCommand = command.trim();
    if (sensitivePatterns.some((p: RegExp) => p.test(trimmedCommand))) {
      return res.status(201).json({
        id: 0,
        userId,
        hostId: parseInt(hostId, 10),
        command: trimmedCommand,
        executedAt: new Date().toISOString(),
      });
    }

    const globalEnabled = await createCurrentSettingsRepository().getBoolean(
      "command_history_enabled",
      true,
    );
    if (!globalEnabled) {
      return res.status(201).json({
        id: 0,
        userId,
        hostId: parseInt(hostId, 10),
        command: trimmedCommand,
        executedAt: new Date().toISOString(),
      });
    }

    const hostRecord =
      await createCurrentHostResolutionRepository().findHostById(
        parseInt(hostId, 10),
        userId,
      );
    if (hostRecord?.enableCommandHistory === false) {
      return res.status(201).json({
        id: 0,
        userId,
        hostId: parseInt(hostId, 10),
        command: trimmedCommand,
        executedAt: new Date().toISOString(),
      });
    }

    try {
      const result = await createCurrentCommandHistoryRepository().create(
        userId,
        parseInt(hostId, 10),
        trimmedCommand,
      );

      res.status(201).json(result);
    } catch (err) {
      authLogger.error("Failed to save command to history", err);
      res.status(500).json({
        error: getErrorMessage(err, "Failed to save command"),
      });
    }
  },
);

/**
 * @openapi
 * /terminal/command_history/{hostId}:
 *   get:
 *     summary: Get command history
 *     description: Retrieves the command history for a specific host.
 *     tags:
 *       - Terminal
 *     parameters:
 *       - in: path
 *         name: hostId
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: A list of commands.
 *       400:
 *         description: Invalid request parameters.
 *       500:
 *         description: Failed to fetch history.
 */
router.get(
  "/command_history/:hostId",
  authenticateJWT,
  requireDataAccess,
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    const hostId = Array.isArray(req.params.hostId)
      ? req.params.hostId[0]
      : req.params.hostId;
    const hostIdNum = parseInt(hostId, 10);

    if (!isNonEmptyString(userId) || isNaN(hostIdNum)) {
      authLogger.warn("Invalid command history fetch request", {
        userId,
        hostId: hostIdNum,
      });
      return res.status(400).json({ error: "Invalid request parameters" });
    }

    try {
      const uniqueCommands =
        await createCurrentCommandHistoryRepository().listUniqueCommandsForHost(
          userId,
          hostIdNum,
        );

      res.json(uniqueCommands);
    } catch (err) {
      authLogger.error("Failed to fetch command history", err);
      res.status(500).json({
        error: getErrorMessage(err, "Failed to fetch history"),
      });
    }
  },
);

/**
 * @openapi
 * /terminal/command_history/delete:
 *   post:
 *     summary: Delete a specific command from history
 *     description: Deletes a specific command from the history of a host.
 *     tags:
 *       - Terminal
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               hostId:
 *                 type: integer
 *               command:
 *                 type: string
 *     responses:
 *       200:
 *         description: Command deleted successfully.
 *       400:
 *         description: Missing required parameters.
 *       500:
 *         description: Failed to delete command.
 */
router.post(
  "/command_history/delete",
  authenticateJWT,
  requireDataAccess,
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    const { hostId, command } = req.body;

    if (!isNonEmptyString(userId) || !hostId || !isNonEmptyString(command)) {
      authLogger.warn("Invalid command delete request", {
        operation: "command_history_delete",
        userId,
        hasHostId: !!hostId,
        hasCommand: !!command,
      });
      return res.status(400).json({ error: "Missing required parameters" });
    }

    try {
      const hostIdNum = parseInt(hostId, 10);

      await createCurrentCommandHistoryRepository().deleteCommandForHost(
        userId,
        hostIdNum,
        command.trim(),
      );

      res.json({ success: true });
    } catch (err) {
      authLogger.error("Failed to delete command from history", err);
      res.status(500).json({
        error: getErrorMessage(err, "Failed to delete command"),
      });
    }
  },
);

/**
 * @openapi
 * /terminal/command_history/{hostId}:
 *   delete:
 *     summary: Clear command history
 *     description: Clears the entire command history for a specific host.
 *     tags:
 *       - Terminal
 *     parameters:
 *       - in: path
 *         name: hostId
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: Command history cleared successfully.
 *       400:
 *         description: Invalid request.
 *       500:
 *         description: Failed to clear history.
 */
router.delete(
  "/command_history/:hostId",
  authenticateJWT,
  requireDataAccess,
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    const hostId = Array.isArray(req.params.hostId)
      ? req.params.hostId[0]
      : req.params.hostId;
    const hostIdNum = parseInt(hostId, 10);

    if (!isNonEmptyString(userId) || isNaN(hostIdNum)) {
      authLogger.warn("Invalid command history clear request");
      return res.status(400).json({ error: "Invalid request" });
    }

    try {
      await createCurrentCommandHistoryRepository().deleteByUserAndHost(
        userId,
        hostIdNum,
      );
      databaseLogger.info("Terminal history cleared", {
        operation: "terminal_history_clear",
        userId,
        hostId: hostIdNum,
      });

      res.json({ success: true });
    } catch (err) {
      authLogger.error("Failed to clear command history", err);
      res.status(500).json({
        error: getErrorMessage(err, "Failed to clear history"),
      });
    }
  },
);

/**
 * @openapi
 * /terminal/session_settings:
 *   get:
 *     summary: Get session persistence settings
 *     description: Returns the session timeout and persistence enabled flag.
 *     tags:
 *       - Terminal
 *     responses:
 *       200:
 *         description: Session settings.
 *       500:
 *         description: Failed to fetch settings.
 */
router.get(
  "/session_settings",
  authenticateJWT,
  async (_req: Request, res: Response) => {
    try {
      const settings = createCurrentSettingsRepository();
      const timeoutValue = await settings.get(
        "terminal_session_timeout_minutes",
      );
      const enabled = await settings.getBoolean(
        "terminal_session_persistence_enabled",
        true,
      );

      res.json({
        timeoutMinutes: timeoutValue ? parseInt(timeoutValue, 10) : 30,
        enabled,
      });
    } catch (err) {
      authLogger.error("Failed to fetch session settings", err);
      res.status(500).json({
        error: getErrorMessage(err, "Failed to fetch settings"),
      });
    }
  },
);

/**
 * @openapi
 * /terminal/session_settings:
 *   post:
 *     summary: Update session persistence settings
 *     description: Saves session timeout and persistence enabled flag.
 *     tags:
 *       - Terminal
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               timeoutMinutes:
 *                 type: integer
 *               enabled:
 *                 type: boolean
 *     responses:
 *       200:
 *         description: Settings saved successfully.
 *       400:
 *         description: Invalid parameters.
 *       500:
 *         description: Failed to save settings.
 */
router.post(
  "/session_settings",
  authenticateJWT,
  async (req: Request, res: Response) => {
    const { timeoutMinutes, enabled } = req.body;

    if (
      timeoutMinutes !== undefined &&
      (typeof timeoutMinutes !== "number" ||
        timeoutMinutes < 1 ||
        timeoutMinutes > 1440)
    ) {
      return res
        .status(400)
        .json({ error: "timeoutMinutes must be between 1 and 1440" });
    }

    try {
      const settings = createCurrentSettingsRepository();
      if (timeoutMinutes !== undefined) {
        await settings.set(
          "terminal_session_timeout_minutes",
          String(timeoutMinutes),
        );
      }

      if (enabled !== undefined) {
        await settings.set(
          "terminal_session_persistence_enabled",
          String(enabled),
        );
      }

      res.json({ success: true });
    } catch (err) {
      authLogger.error("Failed to save session settings", err);
      res.status(500).json({
        error: getErrorMessage(err, "Failed to save settings"),
      });
    }
  },
);

export default router;
