/**
 * Port-logo upload service.
 *
 * Layer 1 of the design (`docs/superpowers/specs/2026-05-12-pdf-stack-overhaul-design.md`):
 * sharp normalization + svgo sanitization + atomic system_settings upsert +
 * audit logging. Single entry point for the admin upload endpoint and the
 * branding-preview endpoint. Returns processed bytes + the file row, plus a
 * collected list of warnings the UI surfaces in the preview swatch.
 */

import { and, eq } from 'drizzle-orm';
import sharp from 'sharp';
import { optimize as svgoOptimize } from 'svgo';

import { db } from '@/lib/db';
import { files } from '@/lib/db/schema/documents';
import { systemSettings } from '@/lib/db/schema/system';
import { ports } from '@/lib/db/schema/ports';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { ValidationError } from '@/lib/errors';
import { getStorageBackend } from '@/lib/storage';
import { generateStorageKey } from '@/lib/services/storage';
import { logger } from '@/lib/logger';
import { PORT_LOGO_SETTING_KEY } from '@/lib/pdf/brand-kit/logo';

const MAX_RAW_BYTES = 5 * 1024 * 1024;
const MAX_FINAL_BYTES = 1 * 1024 * 1024;
const MIN_DIMENSION = 200;
const TARGET_LONG_EDGE = 1200;
const SVG_RASTER_DENSITY = 300;

const SUPPORTED_INPUT_FORMATS = new Set([
  'png',
  'jpeg',
  'jpg',
  'webp',
  'svg',
  'heic',
  'heif',
  'avif',
]);

export type LogoWarning =
  | 'trimmed'
  | 'resized'
  | 'no-alpha'
  | 'jpeg-source'
  | 'svg-rasterized'
  | 'heic-converted'
  | 'webp-converted';

export interface LogoCrop {
  /** Crop coordinates in raw-image pixel units. */
  x: number;
  y: number;
  width: number;
  height: number;
}

export interface ProcessLogoResult {
  pngBuffer: Buffer;
  warnings: LogoWarning[];
  originalFormat: string;
  originalDimensions: { width: number; height: number };
  finalDimensions: { width: number; height: number };
  finalBytes: number;
}

function sanitizeSvg(rawSvg: string): string {
  // preset-default strips <script>, event handlers, and unused elements.
  // The post-pass regex check below is belt-and-suspenders for the
  // attack surface we explicitly disallow.
  const result = svgoOptimize(rawSvg, {
    multipass: true,
    plugins: ['preset-default'],
  });
  const sanitized = result.data;
  if (
    /<script\b/i.test(sanitized) ||
    /<foreignobject\b/i.test(sanitized) ||
    /\bon\w+\s*=/i.test(sanitized) ||
    /javascript:/i.test(sanitized) ||
    /\bhref\s*=\s*["']https?:/i.test(sanitized)
  ) {
    throw new ValidationError(
      'SVG contained disallowed nodes (script/foreignObject/external href)',
    );
  }
  return sanitized;
}

/**
 * Runs the full sharp normalization pipeline. Throws ValidationError on
 * unsupported formats, undersized dimensions, oversized output, or animated
 * inputs. Caller passes the original upload buffer and optional crop coords.
 */
export async function processLogoUpload(raw: Buffer, crop?: LogoCrop): Promise<ProcessLogoResult> {
  if (raw.length === 0) throw new ValidationError('Empty file');
  if (raw.length > MAX_RAW_BYTES) {
    throw new ValidationError(`File exceeds ${MAX_RAW_BYTES / 1024 / 1024} MB`);
  }

  const warnings: LogoWarning[] = [];

  // Detect format up front so we can route SVGs through svgo first.
  let probe: sharp.Metadata;
  try {
    probe = await sharp(raw).metadata();
  } catch (err) {
    logger.warn({ err }, 'logo upload: sharp could not read metadata');
    throw new ValidationError('File contents do not match a supported image format');
  }
  const format = probe.format ?? 'unknown';
  if (!SUPPORTED_INPUT_FORMATS.has(format)) {
    throw new ValidationError(`Unsupported image format: ${format}`);
  }
  if ((probe.pages ?? 1) > 1) {
    throw new ValidationError('Animated/multi-frame images are not accepted');
  }

  let workingBuffer = raw;

  if (format === 'svg') {
    const sanitized = sanitizeSvg(raw.toString('utf8'));
    workingBuffer = Buffer.from(sanitized, 'utf8');
    warnings.push('svg-rasterized');
  } else if (format === 'heif' || format === 'avif') {
    warnings.push('heic-converted');
  } else if (format === 'webp') {
    warnings.push('webp-converted');
  } else if (format === 'jpeg') {
    warnings.push('jpeg-source');
    if (!probe.hasAlpha) warnings.push('no-alpha');
  }

  const baseSharp =
    format === 'svg' ? sharp(workingBuffer, { density: SVG_RASTER_DENSITY }) : sharp(workingBuffer);

  // Apply crop if provided, after validating bounds against the original metadata.
  let pipeline = baseSharp;
  if (crop) {
    const w = probe.width ?? 0;
    const h = probe.height ?? 0;
    if (
      crop.x < 0 ||
      crop.y < 0 ||
      crop.width <= 0 ||
      crop.height <= 0 ||
      crop.x + crop.width > w ||
      crop.y + crop.height > h
    ) {
      throw new ValidationError('Crop coordinates out of image bounds');
    }
    pipeline = pipeline.extract({
      left: Math.round(crop.x),
      top: Math.round(crop.y),
      width: Math.round(crop.width),
      height: Math.round(crop.height),
    });
  }

  // Trim near-white borders (only for raster; SVG has none).
  if (format !== 'svg') {
    pipeline = pipeline.trim({ threshold: 10 });
    warnings.push('trimmed');
  }

  pipeline = pipeline
    .resize({
      width: TARGET_LONG_EDGE,
      height: TARGET_LONG_EDGE,
      fit: 'inside',
      withoutEnlargement: true,
    })
    .toColorspace('srgb')
    .png({ compressionLevel: 9, palette: true });

  const pngBuffer = await pipeline.toBuffer();
  const finalMeta = await sharp(pngBuffer).metadata();
  const fw = finalMeta.width ?? 0;
  const fh = finalMeta.height ?? 0;
  if (fw < MIN_DIMENSION || fh < MIN_DIMENSION) {
    throw new ValidationError(
      `Logo is too small after trim (${fw}x${fh}). Minimum ${MIN_DIMENSION}px per side.`,
    );
  }
  if (pngBuffer.length > MAX_FINAL_BYTES) {
    throw new ValidationError(
      `Processed logo exceeds ${MAX_FINAL_BYTES / 1024 / 1024} MB. Try a simpler source image.`,
    );
  }
  if (probe.width && fw < probe.width) warnings.push('resized');

  return {
    pngBuffer,
    warnings,
    originalFormat: format,
    originalDimensions: { width: probe.width ?? 0, height: probe.height ?? 0 },
    finalDimensions: { width: fw, height: fh },
    finalBytes: pngBuffer.length,
  };
}

/**
 * Persist the processed logo: write to storage backend, insert files row,
 * upsert system_settings pointer atomically, soft-delete the prior file row.
 * Last-writer-wins; concurrent uploads do not race because the system_settings
 * upsert is atomic.
 */
export async function setPortLogo(
  portId: string,
  processed: ProcessLogoResult,
  meta: AuditMeta,
): Promise<{ fileId: string; storagePath: string; warnings: LogoWarning[] }> {
  const port = await db.query.ports.findFirst({ where: eq(ports.id, portId) });
  if (!port) throw new ValidationError('Unknown port');

  const storagePath = generateStorageKey(port.slug, 'branding', port.id, 'image/png');
  const backend = await getStorageBackend();
  await backend.put(storagePath, processed.pngBuffer, {
    contentType: 'image/png',
    sizeBytes: processed.pngBuffer.length,
  });

  const prior = await readCurrentLogoFileId(portId);

  const [record] = await db
    .insert(files)
    .values({
      portId,
      filename: 'port-logo.png',
      originalName: 'port-logo.png',
      mimeType: 'image/png',
      sizeBytes: String(processed.pngBuffer.length),
      storagePath,
      category: 'branding',
      uploadedBy: meta.userId,
    })
    .returning();
  if (!record) throw new Error('files insert returned no row');

  await db
    .insert(systemSettings)
    .values({
      key: PORT_LOGO_SETTING_KEY,
      value: record.id,
      portId,
      updatedBy: meta.userId,
    })
    .onConflictDoUpdate({
      target: [systemSettings.key, systemSettings.portId],
      set: { value: record.id, updatedBy: meta.userId, updatedAt: new Date() },
    });

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'branding.logo.uploaded',
    entityType: 'port',
    entityId: portId,
    newValue: {
      fileId: record.id,
      finalDimensions: processed.finalDimensions,
      finalBytes: processed.finalBytes,
      warnings: processed.warnings,
      originalFormat: processed.originalFormat,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  if (prior && prior !== record.id) {
    await purgePriorLogo(prior, portId, meta);
  }

  return { fileId: record.id, storagePath, warnings: processed.warnings };
}

async function readCurrentLogoFileId(portId: string): Promise<string | null> {
  const [row] = await db
    .select()
    .from(systemSettings)
    .where(and(eq(systemSettings.key, PORT_LOGO_SETTING_KEY), eq(systemSettings.portId, portId)));
  if (!row) return null;
  return typeof row.value === 'string' ? row.value : null;
}

async function purgePriorLogo(fileId: string, portId: string, meta: AuditMeta): Promise<void> {
  const file = await db.query.files.findFirst({ where: eq(files.id, fileId) });
  if (!file) return;
  try {
    const backend = await getStorageBackend();
    await backend.delete(file.storagePath);
  } catch (err) {
    logger.warn({ err, fileId, storagePath: file.storagePath }, 'prior logo storage purge failed');
  }
  await db.delete(files).where(eq(files.id, fileId));
  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'branding.logo.archived',
    entityType: 'port',
    entityId: portId,
    oldValue: { fileId, storagePath: file.storagePath },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });
}

/** Clear the port logo. Removes the storage object + files row + setting. */
export async function clearPortLogo(portId: string, meta: AuditMeta): Promise<void> {
  const prior = await readCurrentLogoFileId(portId);
  if (!prior) return;
  await db
    .delete(systemSettings)
    .where(and(eq(systemSettings.key, PORT_LOGO_SETTING_KEY), eq(systemSettings.portId, portId)));
  await purgePriorLogo(prior, portId, meta);
}

/** Read the current port logo file row (or null) for admin UI display. */
export async function getPortLogoFile(portId: string) {
  const fileId = await readCurrentLogoFileId(portId);
  if (!fileId) return null;
  const file = await db.query.files.findFirst({
    where: and(eq(files.id, fileId), eq(files.portId, portId)),
  });
  return file ?? null;
}
