const fs = require('node:fs');
const path = require('node:path');
const sharp = require('sharp');

function escapeXml(value) {
  return String(value || '').replace(/[<>&'"]/g, char => ({
    '<': '&lt;',
    '>': '&gt;',
    '&': '&amp;',
    "'": '&apos;',
    '"': '&quot;',
  }[char]));
}

function normalizedToPixel(box, width, height) {
  const x1 = Math.round((box.x1 * width) / 1000);
  const y1 = Math.round((box.y1 * height) / 1000);
  const x2 = Math.round((box.x2 * width) / 1000);
  const y2 = Math.round((box.y2 * height) / 1000);
  return {
    x1: Math.max(0, Math.min(width - 1, x1)),
    y1: Math.max(0, Math.min(height - 1, y1)),
    x2: Math.max(1, Math.min(width, x2)),
    y2: Math.max(1, Math.min(height, y2)),
  };
}

async function markImage({ sourcePath, targetPath, bbox, label }) {
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
  if (!bbox) {
    fs.copyFileSync(sourcePath, targetPath);
    return { marked: false, width: null, height: null, pixel: null };
  }
  const metadata = await sharp(sourcePath).metadata();
  const width = metadata.width || 1;
  const height = metadata.height || 1;
  const pixel = normalizedToPixel(bbox, width, height);
  const rectWidth = Math.max(2, pixel.x2 - pixel.x1);
  const rectHeight = Math.max(2, pixel.y2 - pixel.y1);
  const fontSize = Math.max(14, Math.round(Math.min(width, height) * 0.028));
  const labelText = escapeXml(label);
  const labelWidth = Math.min(width - pixel.x1, Math.max(80, labelText.length * fontSize * 0.75 + 12));
  const labelY = Math.max(0, pixel.y1 - fontSize - 8);
  const svg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
    <rect x="${pixel.x1}" y="${pixel.y1}" width="${rectWidth}" height="${rectHeight}" fill="none" stroke="#ff1f1f" stroke-width="4"/>
    <rect x="${pixel.x1}" y="${labelY}" width="${labelWidth}" height="${fontSize + 8}" fill="#ff1f1f"/>
    <text x="${pixel.x1 + 6}" y="${labelY + fontSize + 1}" font-size="${fontSize}" fill="white" font-family="Arial, sans-serif">${labelText}</text>
  </svg>`;
  await sharp(sourcePath)
    .composite([{ input: Buffer.from(svg), top: 0, left: 0 }])
    .jpeg({ quality: 92 })
    .toFile(targetPath);
  return { marked: true, width, height, pixel };
}

async function cropZoom({ sourcePath, targetPath, bbox, scale = 2.2, padding = 0.55 }) {
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
  const image = sharp(sourcePath);
  const metadata = await image.metadata();
  const width = metadata.width || 1;
  const height = metadata.height || 1;
  let crop;
  if (bbox) {
    const pixel = normalizedToPixel(bbox, width, height);
    const boxW = Math.max(20, pixel.x2 - pixel.x1);
    const boxH = Math.max(20, pixel.y2 - pixel.y1);
    const padX = Math.round(boxW * padding);
    const padY = Math.round(boxH * padding);
    const left = Math.max(0, pixel.x1 - padX);
    const top = Math.max(0, pixel.y1 - padY);
    crop = {
      left,
      top,
      width: Math.min(width - left, boxW + padX * 2),
      height: Math.min(height - top, boxH + padY * 2),
    };
  } else {
    crop = { left: 0, top: 0, width, height };
  }
  await sharp(sourcePath)
    .extract(crop)
    .resize({
      width: Math.round(crop.width * scale),
      height: Math.round(crop.height * scale),
      fit: 'fill',
    })
    .jpeg({ quality: 94 })
    .toFile(targetPath);
  return { crop, width, height };
}

module.exports = {
  normalizedToPixel,
  markImage,
  cropZoom,
};
