// No "server-only" guard: scripts/reprocess-variants.mjs runs this same
// pipeline outside Next. sharp keeps it out of client bundles anyway.
import sharp from "sharp";
import exifReader from "exif-reader";
import { readFile } from "node:fs/promises";
import path from "node:path";
import type { C2pa, ManifestBuilder as ManifestBuilderClass } from "c2pa-node";

const ARTIST = "Erwan Bonniot";
const LICENSE_PAGE = "https://bonniot.com/license";
const CC_LICENSE = "https://creativecommons.org/licenses/by-nc-nd/4.0/";
const COPYRIGHT = `© Erwan Bonniot — CC BY-NC-ND 4.0 — no commercial use — ${LICENSE_PAGE}`;
const USAGE_TERMS =
  "Licensed under CC BY-NC-ND 4.0: personal use and sharing with attribution only. " +
  `No commercial use, no derivatives, no AI training. Commercial licensing: ${LICENSE_PAGE}`;

// XMP is where rights metadata actually survives (Photoshop, Lightroom, stock
// tools all read xmpRights over EXIF). tdm-reservation is the TDMRep opt-out
// for text-and-data-mining / AI training crawlers.
const XMP_PACKET = `<?xpacket begin="\u{FEFF}" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  <rdf:Description rdf:about=""
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:xmpRights="http://ns.adobe.com/xap/1.0/rights/"
    xmlns:cc="http://creativecommons.org/ns#"
    xmlns:tdm="http://www.w3.org/ns/tdmrep#"
    xmpRights:Marked="True"
    xmpRights:WebStatement="${LICENSE_PAGE}"
    cc:attributionName="${ARTIST}"
    tdm:tdm-reservation="1">
   <dc:creator><rdf:Seq><rdf:li>${ARTIST}</rdf:li></rdf:Seq></dc:creator>
   <dc:rights><rdf:Alt><rdf:li xml:lang="x-default">${COPYRIGHT}</rdf:li></rdf:Alt></dc:rights>
   <xmpRights:UsageTerms><rdf:Alt><rdf:li xml:lang="x-default">${USAGE_TERMS}</rdf:li></rdf:Alt></xmpRights:UsageTerms>
   <cc:license rdf:resource="${CC_LICENSE}"/>
  </rdf:Description>
 </rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>`;

// C2PA Content Credentials: cryptographically signed provenance embedded in
// each variant — any later edit breaks the signature. Signing pair lives in
// DATA_DIR/c2pa (scripts/c2pa-cert.sh); missing files = variants ship
// unsigned, same graceful degradation as the SMTP-less contact form.
const DATA_DIR = process.env.DATA_DIR ?? "./data";

interface C2paContext {
  c2pa: C2pa;
  ManifestBuilder: typeof ManifestBuilderClass;
}

let c2paPromise: Promise<C2paContext | null> | null = null;

// Dynamic import: c2pa-node's native binary may fail to load (e.g. glibc too
// old on the host) and a static import would take the whole pipeline down
// with it — unsigned variants beat broken uploads.
function getC2pa(): Promise<C2paContext | null> {
  c2paPromise ??= (async () => {
    try {
      const dir = path.resolve(DATA_DIR, "c2pa");
      const [certificate, privateKey] = await Promise.all([
        readFile(path.join(dir, "cert.pem")),
        readFile(path.join(dir, "key.pem")),
      ]);
      const { createC2pa, ManifestBuilder, SigningAlgorithm } = await import("c2pa-node");
      const c2pa = createC2pa({
        signer: {
          type: "local",
          certificate,
          privateKey,
          algorithm: SigningAlgorithm.ES256,
          tsaUrl: "http://timestamp.digicert.com",
        },
        thumbnail: false,
      });
      return { c2pa, ManifestBuilder };
    } catch (err) {
      console.warn(
        `[images] C2PA signing unavailable, variants ship unsigned: ${(err as Error).message}`
      );
      return null;
    }
  })();
  return c2paPromise;
}

/** Embed a signed C2PA manifest; on any failure the unsigned JPEG ships as-is. */
async function signVariant(buffer: Buffer): Promise<Buffer> {
  const ctx = await getC2pa();
  if (!ctx) return buffer;
  const { c2pa, ManifestBuilder } = ctx;
  try {
    const manifest = new ManifestBuilder({
      claim_generator: "bonniot.com/1.0",
      format: "image/jpeg",
      title: `Photograph by ${ARTIST}`,
      assertions: [
        {
          label: "stds.schema-org.CreativeWork",
          data: {
            "@context": "https://schema.org",
            "@type": "CreativeWork",
            author: [{ "@type": "Person", name: ARTIST, url: "https://bonniot.com" }],
            copyrightNotice: COPYRIGHT,
            license: CC_LICENSE,
            url: LICENSE_PAGE,
          },
        },
      ],
    });
    const { signedAsset } = await c2pa.sign({
      asset: { buffer, mimeType: "image/jpeg" },
      manifest,
      thumbnail: false,
    });
    return signedAsset.buffer;
  } catch (err) {
    console.warn(`[images] C2PA signing failed, shipping unsigned: ${(err as Error).message}`);
    return buffer;
  }
}

export const VARIANTS = [
  { name: "xl", width: 3200, quality: 85 },
  { name: "full", width: 2000, quality: 85 },
  { name: "card", width: 1200, quality: 82 },
  { name: "thumb", width: 600, quality: 78 },
] as const;

export type VariantName = (typeof VARIANTS)[number]["name"];

export interface ExifSummary {
  camera?: string;
  lens?: string;
  focalLength?: string;
  aperture?: string;
  shutter?: string;
  iso?: string;
}

export interface GpsPosition {
  latitude: number;
  longitude: number;
}

export interface ProcessedImage {
  width: number;
  height: number;
  takenAt: string | null;
  exif: ExifSummary;
  /** From the original's EXIF; published variants are always stripped of it. */
  gps: GpsPosition | null;
  variants: Record<VariantName, Buffer>;
}

function watermarkSvg(width: number, height: number): Buffer {
  // Mirrors the design-system watermark: italic serif, bottom-right, 42% white.
  // Server font availability varies, so rely on generic serif rather than Bodoni.
  const fontSize = Math.max(14, Math.round(width / 72));
  const margin = Math.round(fontSize * 1.2);
  return Buffer.from(
    `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
      <text x="${width - margin}" y="${height - margin}"
        text-anchor="end" font-family="Georgia, 'Times New Roman', serif"
        font-style="italic" font-weight="500" font-size="${fontSize}"
        letter-spacing="${(fontSize * 0.03).toFixed(1)}"
        fill="rgba(255,255,255,0.42)">Bonniot</text>
    </svg>`
  );
}

function formatShutter(exposureTime?: number): string | undefined {
  if (!exposureTime) return undefined;
  if (exposureTime >= 1) return `${exposureTime}s`;
  return `1/${Math.round(1 / exposureTime)}`;
}

/** EXIF stores coordinates as [degrees, minutes, seconds] + a N/S/E/W ref. */
function dmsToDecimal(dms: number[] | undefined, ref: string | undefined): number | null {
  if (!dms?.length || !dms.every((n) => Number.isFinite(n))) return null;
  const [degrees = 0, minutes = 0, seconds = 0] = dms;
  const decimal = degrees + minutes / 60 + seconds / 3600;
  return ref === "S" || ref === "W" ? -decimal : decimal;
}

export function extractExif(buffer?: Buffer): {
  takenAt: string | null;
  summary: ExifSummary;
  gps: GpsPosition | null;
} {
  if (!buffer) return { takenAt: null, summary: {}, gps: null };
  try {
    const data = exifReader(buffer);
    const takenAt = data.Photo?.DateTimeOriginal
      ? new Date(data.Photo.DateTimeOriginal).toISOString()
      : null;
    const summary: ExifSummary = {
      camera: [data.Image?.Make, data.Image?.Model].filter(Boolean).join(" ") || undefined,
      lens: data.Photo?.LensModel ?? undefined,
      focalLength: data.Photo?.FocalLength ? `${Math.round(data.Photo.FocalLength)}mm` : undefined,
      aperture: data.Photo?.FNumber ? `ƒ/${data.Photo.FNumber}` : undefined,
      shutter: formatShutter(data.Photo?.ExposureTime),
      iso: data.Photo?.ISOSpeedRatings ? `ISO ${data.Photo.ISOSpeedRatings}` : undefined,
    };
    const latitude = dmsToDecimal(data.GPSInfo?.GPSLatitude, data.GPSInfo?.GPSLatitudeRef);
    const longitude = dmsToDecimal(data.GPSInfo?.GPSLongitude, data.GPSInfo?.GPSLongitudeRef);
    const gps = latitude !== null && longitude !== null ? { latitude, longitude } : null;
    return { takenAt, summary, gps };
  } catch {
    return { takenAt: null, summary: {}, gps: null };
  }
}

/**
 * Original (Lightroom JPEG export) → public variants:
 * bake orientation, resize, watermark, replace metadata with copyright
 * (strips GPS and everything else along the way).
 */
export async function processImage(original: Buffer): Promise<ProcessedImage> {
  const base = sharp(original).rotate();
  const metadata = await base.metadata();
  const { takenAt, summary, gps } = extractExif(metadata.exif);

  const variants = {} as Record<VariantName, Buffer>;
  let fullWidth = 0;
  let fullHeight = 0;

  for (const variant of VARIANTS) {
    const resized = await base
      .clone()
      .resize({ width: variant.width, withoutEnlargement: true })
      .toBuffer({ resolveWithObject: true });

    const jpeg = await sharp(resized.data)
      .composite([{ input: watermarkSvg(resized.info.width, resized.info.height) }])
      .withExif({
        IFD0: {
          Copyright: COPYRIGHT,
          Artist: ARTIST,
          ImageDescription: COPYRIGHT,
        },
      })
      .withXmp(XMP_PACKET)
      .jpeg({ quality: variant.quality, mozjpeg: true })
      .toBuffer();

    variants[variant.name] = await signVariant(jpeg);

    if (variant.name === "full") {
      fullWidth = resized.info.width;
      fullHeight = resized.info.height;
    }
  }

  return { width: fullWidth, height: fullHeight, takenAt, exif: summary, gps, variants };
}
