import sharp from 'sharp';
import { randomUUID } from 'crypto';
import { existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
import { dirname, extname, join, resolve, sep } from 'path';

const SUPPORTED_FORMATS = new Set(['jpeg', 'webp', 'png']);
const WATERMARK_POSITIONS = new Set(['bottom-right', 'bottom-left', 'top-right', 'top-left', 'center']);
const MAX_PIXELS = 60_000_000;
const RESIZE_FITS = new Set(['inside', 'contain', 'cover']);

function clamp(value, min, max, fallback = min) {
  const number = Number(value);
  return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback;
}

function normalizeRelativePath(value) {
  return String(value || '').replace(/\\/g, '/').replace(/^\/+/, '').trim();
}

export function resolveEditableImage(uploadRoot, relativePath) {
  const normalized = normalizeRelativePath(relativePath);
  if (!normalized || normalized.includes('\0')) throw new Error('Bildpfad fehlt.');
  const root = resolve(uploadRoot);
  const absolute = resolve(root, normalized);
  if (absolute !== root && !absolute.startsWith(root + sep)) throw new Error('Ungültiger Bildpfad.');
  if (!existsSync(absolute) || !statSync(absolute).isFile()) throw new Error('Bilddatei wurde nicht gefunden.');
  if (!['.jpg', '.jpeg', '.png', '.webp', '.gif', '.heic', '.heif'].includes(extname(absolute).toLowerCase())) {
    throw new Error('Dieses Bildformat kann nicht bearbeitet werden.');
  }
  return { normalized, absolute, root };
}

export function normalizeEditorOperations(input = {}) {
  const cropInput = input.crop && typeof input.crop === 'object' ? input.crop : null;
  const crop = cropInput ? {
    x: clamp(cropInput.x, 0, 1, 0),
    y: clamp(cropInput.y, 0, 1, 0),
    width: clamp(cropInput.width, 0.01, 1, 1),
    height: clamp(cropInput.height, 0.01, 1, 1),
  } : null;
  if (crop) {
    crop.width = Math.min(crop.width, 1 - crop.x);
    crop.height = Math.min(crop.height, 1 - crop.y);
  }

  const resizeInput = input.resize && typeof input.resize === 'object' ? input.resize : {};
  const width = Math.round(clamp(resizeInput.width, 0, 12000, 0));
  const height = Math.round(clamp(resizeInput.height, 0, 12000, 0));
  const resizeFit = RESIZE_FITS.has(String(resizeInput.fit || '').toLowerCase()) ? String(resizeInput.fit).toLowerCase() : 'inside';
  const resizeBackground = /^#[0-9a-f]{6}$/i.test(String(resizeInput.background || '')) ? String(resizeInput.background) : '#ffffff';

  const rotation = Math.round(clamp(input.rotation, -1080, 1080, 0) / 90) * 90;
  const watermarkInput = input.watermark && typeof input.watermark === 'object' ? input.watermark : {};
  const watermarkText = String(watermarkInput.text || 'Vendoo').replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 80);
  const watermarkPosition = WATERMARK_POSITIONS.has(String(watermarkInput.position || '')) ? String(watermarkInput.position) : 'bottom-right';
  return {
    rotation: ((rotation % 360) + 360) % 360,
    straighten: clamp(input.straighten, -15, 15, 0),
    flip_h: Boolean(input.flip_h),
    flip_v: Boolean(input.flip_v),
    crop,
    exposure: clamp(input.exposure, -2, 2, 0),
    brightness: clamp(input.brightness, -100, 100, 0),
    contrast: clamp(input.contrast, -100, 100, 0),
    highlights: clamp(input.highlights, -100, 100, 0),
    shadows: clamp(input.shadows, -100, 100, 0),
    temperature: clamp(input.temperature, -100, 100, 0),
    saturation: clamp(input.saturation, -100, 100, 0),
    sharpness: clamp(input.sharpness, 0, 100, 0),
    remove_background: Boolean(input.remove_background),
    background_sensitivity: Math.round(clamp(input.background_sensitivity, 0, 100, 45)),
    watermark: {
      enabled: Boolean(watermarkInput.enabled) && Boolean(watermarkText),
      text: watermarkText || 'Vendoo',
      position: watermarkPosition,
      opacity: Math.round(clamp(watermarkInput.opacity, 10, 100, 40)),
    },
    resize: { width, height, fit: resizeFit, background: resizeBackground },
    format: SUPPORTED_FORMATS.has(String(input.format || '').toLowerCase()) ? String(input.format).toLowerCase() : 'jpeg',
    quality: Math.round(clamp(input.quality, 40, 100, 90)),
  };
}

function applyToneAdjustments(data, channels, operations) {
  const exposure = 2 ** operations.exposure;
  const brightness = operations.brightness / 100 * 0.22;
  const contrastValue = Math.max(-0.95, Math.min(0.95, operations.contrast / 100));
  const contrast = (1 + contrastValue) / (1 - contrastValue);
  const highlights = operations.highlights / 100;
  const shadows = operations.shadows / 100;
  const temperature = operations.temperature / 100;
  const saturation = 1 + operations.saturation / 100;

  for (let index = 0; index < data.length; index += channels) {
    let red = data[index] / 255;
    let green = data[index + 1] / 255;
    let blue = data[index + 2] / 255;

    red = red * exposure + brightness;
    green = green * exposure + brightness;
    blue = blue * exposure + brightness;

    red = (red - 0.5) * contrast + 0.5;
    green = (green - 0.5) * contrast + 0.5;
    blue = (blue - 0.5) * contrast + 0.5;

    const luminanceBefore = Math.max(0, Math.min(1, red * 0.2126 + green * 0.7152 + blue * 0.0722));
    const shadowWeight = (1 - luminanceBefore) ** 2;
    const highlightWeight = luminanceBefore ** 2;
    const shadowDelta = shadows >= 0 ? shadows * shadowWeight * 0.36 : shadows * shadowWeight * 0.28;
    const highlightDelta = highlights >= 0 ? highlights * highlightWeight * 0.28 : highlights * highlightWeight * 0.38;
    red += shadowDelta + highlightDelta;
    green += shadowDelta + highlightDelta;
    blue += shadowDelta + highlightDelta;

    red += temperature * 0.12;
    green += temperature * 0.015;
    blue -= temperature * 0.12;

    const luminance = red * 0.2126 + green * 0.7152 + blue * 0.0722;
    red = luminance + (red - luminance) * saturation;
    green = luminance + (green - luminance) * saturation;
    blue = luminance + (blue - luminance) * saturation;

    data[index] = Math.round(Math.max(0, Math.min(1, red)) * 255);
    data[index + 1] = Math.round(Math.max(0, Math.min(1, green)) * 255);
    data[index + 2] = Math.round(Math.max(0, Math.min(1, blue)) * 255);
  }
  return data;
}

async function applyGeometry(sourcePath, operations) {
  // Sharp applies only one rotation per pipeline. Keep EXIF orientation,
  // flipping and the combined user rotation/straightening in separate stages.
  let rendered = await sharp(sourcePath, { failOn: 'none', limitInputPixels: MAX_PIXELS })
    .rotate()
    .toBuffer({ resolveWithObject: true });

  if (operations.flip_h || operations.flip_v) {
    let flipStage = sharp(rendered.data, { failOn: 'none', limitInputPixels: MAX_PIXELS });
    if (operations.flip_v) flipStage = flipStage.flip();
    if (operations.flip_h) flipStage = flipStage.flop();
    rendered = await flipStage.toBuffer({ resolveWithObject: true });
  }

  const angle = operations.rotation + operations.straighten;
  if (Math.abs(angle) >= 0.05) {
    rendered = await sharp(rendered.data, { failOn: 'none', limitInputPixels: MAX_PIXELS })
      .rotate(angle, { background: { r: 0, g: 0, b: 0, alpha: 0 } })
      .toBuffer({ resolveWithObject: true });
  }

  let current = sharp(rendered.data, { failOn: 'none', limitInputPixels: MAX_PIXELS });

  if (operations.crop) {
    const { width, height } = rendered.info;
    const left = Math.max(0, Math.min(width - 1, Math.round(operations.crop.x * width)));
    const top = Math.max(0, Math.min(height - 1, Math.round(operations.crop.y * height)));
    const cropWidth = Math.max(1, Math.min(width - left, Math.round(operations.crop.width * width)));
    const cropHeight = Math.max(1, Math.min(height - top, Math.round(operations.crop.height * height)));
    current = current.extract({ left, top, width: cropWidth, height: cropHeight });
  }

  if (operations.resize.width || operations.resize.height) {
    const fit = operations.resize.fit || 'inside';
    current = current.resize({
      width: operations.resize.width || undefined,
      height: operations.resize.height || undefined,
      fit,
      background: operations.resize.background || '#ffffff',
      withoutEnlargement: fit === 'inside',
      position: 'centre',
      kernel: sharp.kernel.lanczos3,
    });
  }
  return current;
}


function removeUniformBackground(data, channels, width, height, sensitivity = 45) {
  if (channels < 4 || !width || !height) return data;
  const indexes = [0, (width - 1) * channels, ((height - 1) * width) * channels, (width * height - 1) * channels];
  const background = indexes.reduce((sum, index) => ({
    r: sum.r + data[index],
    g: sum.g + data[index + 1],
    b: sum.b + data[index + 2],
  }), { r: 0, g: 0, b: 0 });
  background.r /= indexes.length;
  background.g /= indexes.length;
  background.b /= indexes.length;
  const level = Math.max(0, Math.min(100, Number(sensitivity) || 0));
  const solidThreshold = 24 + level * 2.1;
  const featherWidth = 45 + level * 1.2;
  const featherEnd = solidThreshold + featherWidth;
  for (let index = 0; index < data.length; index += channels) {
    const distance = Math.abs(data[index] - background.r) + Math.abs(data[index + 1] - background.g) + Math.abs(data[index + 2] - background.b);
    if (distance <= solidThreshold) data[index + 3] = 0;
    else if (distance < featherEnd) data[index + 3] = Math.round(data[index + 3] * (distance - solidThreshold) / featherWidth);
  }
  return data;
}

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

function createWatermarkSvg(width, height, watermark) {
  const margin = Math.max(10, Math.round(Math.min(width, height) * 0.025));
  const fontSize = Math.max(16, Math.round(width * 0.04));
  const position = watermark.position || 'bottom-right';
  let x = width - margin;
  let y = height - margin;
  let anchor = 'end';
  let baseline = 'auto';
  if (position.includes('left')) { x = margin; anchor = 'start'; }
  if (position.includes('top')) { y = margin + fontSize; baseline = 'auto'; }
  if (position === 'center') { x = width / 2; y = height / 2; anchor = 'middle'; baseline = 'middle'; }
  const opacity = Math.max(0.1, Math.min(1, Number(watermark.opacity || 40) / 100));
  const text = escapeXmlText(watermark.text);
  return Buffer.from(`<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg"><style>.wm{font-family:Arial,sans-serif;font-size:${fontSize}px;font-weight:700;paint-order:stroke;stroke:rgba(0,0,0,.42);stroke-width:${Math.max(2, fontSize * 0.08)}px;stroke-linejoin:round;fill:#fff;fill-opacity:${opacity};stroke-opacity:${opacity}}</style><text x="${x}" y="${y}" text-anchor="${anchor}" dominant-baseline="${baseline}" class="wm">${text}</text></svg>`);
}

export async function renderImageVersion({ uploadRoot, sourcePath, operations: rawOperations, context = 'generator', userId = null, rootPath = null, recordVersion = null, outputDirectory = null, outputFilename = null }) {
  const source = resolveEditableImage(uploadRoot, sourcePath);
  const operations = normalizeEditorOperations(rawOperations);
  const resolvedRootPath = String(rootPath || source.normalized).replace(/\\/g, '/').replace(/^\/+/, '').trim() || source.normalized;

  let pipeline = await applyGeometry(source.absolute, operations);
  const raw = await pipeline.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
  const adjusted = applyToneAdjustments(raw.data, raw.info.channels, operations);
  if (operations.remove_background) removeUniformBackground(adjusted, raw.info.channels, raw.info.width, raw.info.height, operations.background_sensitivity);
  pipeline = sharp(adjusted, {
    raw: { width: raw.info.width, height: raw.info.height, channels: raw.info.channels },
    limitInputPixels: MAX_PIXELS,
  });

  if (operations.sharpness > 0) {
    const strength = operations.sharpness / 100;
    pipeline = pipeline.sharpen({ sigma: 0.6 + strength * 1.8, m1: 0.5 + strength * 1.5, m2: 1.5 + strength * 2.5 });
  }
  if (operations.watermark.enabled && operations.watermark.text) {
    pipeline = pipeline.composite([{ input: createWatermarkSvg(raw.info.width, raw.info.height, operations.watermark), top: 0, left: 0 }]);
  }

  const extension = operations.format === 'jpeg' ? '.jpg' : `.${operations.format}`;
  const sourceFolder = dirname(source.normalized).replace(/\\/g, '/');
  const requestedFolder = normalizeRelativePath(outputDirectory || sourceFolder || '.');
  const safeFolder = requestedFolder === '.' ? '' : requestedFolder.split('/').filter(part => part && part !== '.' && part !== '..').join('/');
  const requestedBase = String(outputFilename || randomUUID()).replace(/\\/g, '-').replace(/[^a-zA-Z0-9äöüÄÖÜß._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 120) || randomUUID();
  const cleanBase = requestedBase.replace(/\.(jpe?g|png|webp)$/i, '');
  let outputName = `${cleanBase}${extension}`;
  let outputRelative = safeFolder ? `${safeFolder}/${outputName}` : outputName;
  let outputAbsolute = resolve(uploadRoot, outputRelative);
  const resolvedUploadRoot = resolve(uploadRoot);
  if (outputAbsolute === resolvedUploadRoot || !outputAbsolute.startsWith(resolvedUploadRoot + sep)) throw new Error('Ungültiger Ausgabeordner.');
  let duplicateIndex = 2;
  while (existsSync(outputAbsolute)) {
    outputName = `${cleanBase}-${duplicateIndex}${extension}`;
    outputRelative = safeFolder ? `${safeFolder}/${outputName}` : outputName;
    outputAbsolute = resolve(uploadRoot, outputRelative);
    duplicateIndex += 1;
  }
  mkdirSync(dirname(outputAbsolute), { recursive: true });

  if (operations.format === 'png') pipeline = pipeline.png({ compressionLevel: 9, adaptiveFiltering: true });
  else if (operations.format === 'webp') pipeline = pipeline.webp({ quality: operations.quality, smartSubsample: true });
  else pipeline = pipeline.flatten({ background: '#ffffff' }).jpeg({ quality: operations.quality, mozjpeg: true, chromaSubsampling: '4:4:4' });

  await pipeline.toFile(outputAbsolute);
  const metadata = await sharp(outputAbsolute, { failOn: 'none' }).metadata();
  const stats = statSync(outputAbsolute);
  const versionData = {
    root_path: resolvedRootPath,
    source_path: source.normalized,
    output_path: outputRelative,
    context: String(context || 'generator').slice(0, 40),
    operations,
    width: metadata.width || null,
    height: metadata.height || null,
    format: metadata.format || operations.format,
    quality: operations.quality,
    file_size: stats.size,
    user_id: userId,
  };
  let version;
  try {
    version = typeof recordVersion === 'function' ? await recordVersion(versionData) : versionData;
  } catch (error) {
    try { unlinkSync(outputAbsolute); } catch {}
    throw error;
  }

  return {
    filename: outputRelative,
    root_path: resolvedRootPath,
    version,
    metadata: {
      width: metadata.width || null,
      height: metadata.height || null,
      format: metadata.format || operations.format,
      file_size: stats.size,
    },
    operations,
  };
}
