import { promises as fs } from 'node:fs';
import path from 'node:path';
import { randomBytes } from 'node:crypto';
import { logger } from '../logger.js';

const SLUG = /^[a-z0-9_-]{1,64}$/;

const MIME_TO_EXT: Record<string, string> = {
  'image/png': 'png',
  'image/jpeg': 'jpg',
  'image/gif': 'gif',
  'image/webp': 'webp',
  'image/svg+xml': 'svg',
  'application/pdf': 'pdf',
  'application/zip': 'zip',
  'application/json': 'json',
  'text/plain': 'txt',
  'text/csv': 'csv',
};

const MAGIC_SIGNATURES: Array<{ mime: string; bytes: number[] | ((b: Buffer) => boolean) }> = [
  { mime: 'image/png', bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
  { mime: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },
  { mime: 'image/gif', bytes: [0x47, 0x49, 0x46, 0x38] },
  { mime: 'image/webp', bytes: (b) => b.slice(0, 4).toString() === 'RIFF' && b.slice(8, 12).toString() === 'WEBP' },
  { mime: 'application/pdf', bytes: [0x25, 0x50, 0x44, 0x46] }, // %PDF
  { mime: 'application/zip', bytes: [0x50, 0x4b, 0x03, 0x04] },
];

function magicMatches(bytes: Buffer, mime: string): boolean {
  const sig = MAGIC_SIGNATURES.find((s) => s.mime === mime);
  if (!sig) return true; // No magic check for plain-text MIMEs
  if (typeof sig.bytes === 'function') return sig.bytes(bytes);
  for (let i = 0; i < sig.bytes.length; i++) {
    if (bytes[i] !== sig.bytes[i]) return false;
  }
  return true;
}

export interface JobQuotaState {
  files: number;
  bytes: number;
}

export interface SaveBinaryInput {
  workspacePath: string;
  serverId: string;
  toolName: string;
  bytes: Buffer;
  mimeType: string;
  maxBytes: number;
  jobQuota: {
    maxFiles: number;
    maxBytes: number;
    state: JobQuotaState;
  };
}

export type SaveBinaryResult =
  | { ok: true; relPath: string; size: number }
  | { ok: false; reason: string };

export async function saveBinary(input: SaveBinaryInput): Promise<SaveBinaryResult> {
  if (!SLUG.test(input.serverId)) return { ok: false, reason: 'invalid serverId slug' };
  if (!SLUG.test(input.toolName)) return { ok: false, reason: 'invalid toolName slug' };
  if (input.bytes.length > input.maxBytes) return { ok: false, reason: `binary exceeds max ${input.maxBytes}B` };
  if (!magicMatches(input.bytes, input.mimeType)) {
    return { ok: false, reason: `magic bytes do not match mimeType ${input.mimeType}` };
  }

  const quota = input.jobQuota;
  if (quota.state.files >= quota.maxFiles) {
    return { ok: false, reason: `job quota: max files ${quota.maxFiles} reached` };
  }
  if (quota.state.bytes + input.bytes.length > quota.maxBytes) {
    return { ok: false, reason: `job quota: max bytes ${quota.maxBytes} reached` };
  }

  const ext = MIME_TO_EXT[input.mimeType] ?? 'bin';
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
  const shortHash = randomBytes(3).toString('hex');
  const relDir = path.posix.join('output', 'mcp', input.serverId);
  const relPath = path.posix.join(relDir, `${input.toolName}-${stamp}-${shortHash}.${ext}`);

  const absDir = path.join(input.workspacePath, relDir);
  const absPath = path.join(input.workspacePath, relPath);

  // Path traversal defence: absPath must be within workspace.
  const resolvedWorkspace = path.resolve(input.workspacePath);
  const resolvedAbs = path.resolve(absPath);
  if (!resolvedAbs.startsWith(resolvedWorkspace + path.sep)) {
    return { ok: false, reason: 'resolved path escapes workspace' };
  }

  await fs.mkdir(absDir, { recursive: true });
  await fs.writeFile(absPath, input.bytes);
  quota.state.files += 1;
  quota.state.bytes += input.bytes.length;
  logger.debug(`[mcp:binary] saved ${relPath} size=${input.bytes.length}`);
  return { ok: true, relPath, size: input.bytes.length };
}
