import "server-only";

import { promises as fs } from "node:fs";
import path from "node:path";

import { nanoid } from "nanoid";

import { getMediaProcessingConfig } from "@/lib/server/media-config";
import { MediaProcessingError } from "@/lib/server/media-errors";
import { sanitizeBaseName } from "@/lib/validations/media-file";
import type { StoredUploadFile } from "@/types/media-processing";

function normalizePathForComparison(targetPath: string) {
  return path.resolve(targetPath).toLowerCase();
}

export function assertPathWithinRoot(targetPath: string, rootPath: string) {
  const normalizedRootPath = normalizePathForComparison(rootPath);
  const normalizedTargetPath = normalizePathForComparison(targetPath);

  const rootWithSeparator = normalizedRootPath.endsWith(path.sep)
    ? normalizedRootPath
    : `${normalizedRootPath}${path.sep}`;

  if (normalizedTargetPath !== normalizedRootPath && !normalizedTargetPath.startsWith(rootWithSeparator)) {
    throw new MediaProcessingError({
      message: "Unsafe path detected outside configured media temp root.",
      code: "path_safety_violation",
      statusCode: 500,
      details: {
        targetPath,
        rootPath
      }
    });
  }
}

function ensureSafePath(targetPath: string) {
  const config = getMediaProcessingConfig();
  assertPathWithinRoot(targetPath, config.tempRootDirectory);
}

export async function ensureMediaTempDirectories() {
  const config = getMediaProcessingConfig();
  await fs.mkdir(config.uploadsDirectory, { recursive: true });
  await fs.mkdir(config.outputsDirectory, { recursive: true });
}

export async function writeUploadToTemp(file: File): Promise<StoredUploadFile> {
  const config = getMediaProcessingConfig();
  await ensureMediaTempDirectories();

  const extension = path.extname(file.name).toLowerCase();
  const safeBaseName = sanitizeBaseName(file.name);
  const fileName = `${safeBaseName}-${nanoid(10)}${extension}`;
  const tempPath = path.join(config.uploadsDirectory, fileName);

  ensureSafePath(tempPath);

  const fileBuffer = Buffer.from(await file.arrayBuffer());
  await fs.writeFile(tempPath, fileBuffer);

  return {
    tempPath,
    originalName: file.name,
    safeBaseName,
    extension,
    sizeBytes: fileBuffer.byteLength
  };
}

interface CreateTempOutputPathOptions {
  extension: string;
  prefix: string;
}

export async function createTempOutputPath({
  extension,
  prefix
}: CreateTempOutputPathOptions) {
  const config = getMediaProcessingConfig();
  await ensureMediaTempDirectories();

  const normalizedExtension = extension.replace(/^\./, "").toLowerCase();
  const safePrefix = sanitizeBaseName(prefix);
  const outputFileName = `${safePrefix}-${nanoid(10)}.${normalizedExtension}`;
  const outputPath = path.join(config.outputsDirectory, outputFileName);

  ensureSafePath(outputPath);
  return outputPath;
}

export async function removeFileIfExists(targetPath: string | null | undefined) {
  if (!targetPath) {
    return;
  }

  ensureSafePath(targetPath);

  try {
    await fs.unlink(targetPath);
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
      throw new MediaProcessingError({
        message: "Failed to remove temporary file.",
        code: "filesystem_failure",
        statusCode: 500,
        details: {
          targetPath
        }
      });
    }
  }
}

export async function cleanupFiles(paths: Array<string | null | undefined>) {
  await Promise.all(paths.map((filePath) => removeFileIfExists(filePath)));
}

interface CleanupStaleFilesOptions {
  directory?: string;
  maxAgeMs?: number;
}

export async function cleanupStaleTempFiles({
  directory,
  maxAgeMs
}: CleanupStaleFilesOptions = {}) {
  const config = getMediaProcessingConfig();
  const targetDirectory = directory || config.tempRootDirectory;
  const maxAge = maxAgeMs || config.cleanupMaxAgeMs;
  const now = Date.now();

  ensureSafePath(targetDirectory);

  const entries = await fs.readdir(targetDirectory, {
    withFileTypes: true
  });

  for (const entry of entries) {
    const entryPath = path.join(targetDirectory, entry.name);
    ensureSafePath(entryPath);

    if (entry.isDirectory()) {
      await cleanupStaleTempFiles({
        directory: entryPath,
        maxAgeMs: maxAge
      });
      continue;
    }

    const stats = await fs.stat(entryPath);
    if (now - stats.mtimeMs > maxAge) {
      await removeFileIfExists(entryPath);
    }
  }
}
