import { randomUUID } from "node:crypto";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import sharp from "sharp";
import { z } from "zod";
import { formatZodErrors } from "../../lib/errors.js";
import {
  buildTagArgs,
  type EditMetadataSettings,
  inspectMetadata,
  writeMetadata,
} from "../../lib/exiftool.js";
import { validateImageBuffer } from "../../lib/file-validation.js";
import { sanitizeFilename } from "../../lib/filename.js";
import { decodeHeic } from "../../lib/heic-converter.js";
import { putObject } from "../../lib/object-storage.js";
import { registerToolProcessFn } from "../tool-factory.js";

const settingsSchema = z.object({
  title: z.string().optional(),
  author: z.string().optional(),
  artist: z.string().optional(),
  copyright: z.string().optional(),
  imageDescription: z.string().optional(),
  software: z.string().optional(),
  dateTime: z.string().optional(),
  dateTimeOriginal: z.string().optional(),
  clearGps: z.boolean().default(false),
  fieldsToRemove: z.array(z.string()).default([]),
  gpsLatitude: z.number().min(-90).max(90).optional(),
  gpsLongitude: z.number().min(-180).max(180).optional(),
  gpsAltitude: z.number().optional(),
  keywords: z.array(z.string()).optional(),
  keywordsMode: z.enum(["add", "set"]).default("add"),
  dateShift: z
    .string()
    .regex(/^[+-]\d{1,2}:\d{2}$/)
    .optional(),
  setAllDates: z.string().optional(),
  iptcTitle: z.string().optional(),
  iptcHeadline: z.string().optional(),
  iptcCity: z.string().optional(),
  iptcState: z.string().optional(),
  iptcCountry: z.string().optional(),
});

type Settings = z.infer<typeof settingsSchema>;

// edit-metadata writes tags in place and streams the ORIGINAL bytes back, so
// the download's content-type must match the real format, not a default. A
// previous fallback to image/jpeg made a BMP/PSD/PPM download claim to be a
// JPEG. Map every format validateImageBuffer can report.
const MIME_BY_FORMAT: Record<string, string> = {
  jpeg: "image/jpeg",
  png: "image/png",
  webp: "image/webp",
  avif: "image/avif",
  tiff: "image/tiff",
  gif: "image/gif",
  heif: "image/heif",
  bmp: "image/bmp",
  svg: "image/svg+xml",
  ico: "image/x-icon",
  cur: "image/x-icon",
  psd: "image/vnd.adobe.photoshop",
  jxl: "image/jxl",
  jp2: "image/jp2",
  qoi: "image/qoi",
  dds: "image/vnd.ms-dds",
  exr: "image/x-exr",
  dpx: "image/x-dpx",
  fits: "image/fits",
  eps: "application/postscript",
  pbm: "image/x-portable-bitmap",
  pgm: "image/x-portable-graymap",
  ppm: "image/x-portable-pixmap",
  pfm: "image/x-portable-floatmap",
  tga: "image/x-tga",
  cr3: "image/x-canon-cr3",
  raw: "image/x-dcraw",
};

const BROWSER_PREVIEWABLE = new Set([
  "image/jpeg",
  "image/png",
  "image/gif",
  "image/webp",
  "image/svg+xml",
  "image/bmp",
  "image/avif",
]);

export function registerEditMetadata(app: FastifyInstance) {
  // Inspect endpoint - returns parsed metadata via ExifTool
  app.post(
    "/api/v1/tools/image/edit-metadata/inspect",
    async (request: FastifyRequest, reply: FastifyReply) => {
      let fileBuffer: Buffer | null = null;
      let filename = "image";

      try {
        const parts = request.parts();
        for await (const part of parts) {
          if (part.type === "file") {
            const chunks: Buffer[] = [];
            for await (const chunk of part.file) {
              chunks.push(chunk);
            }
            fileBuffer = Buffer.concat(chunks);
            filename = sanitizeFilename(part.filename ?? "image");
          }
        }
      } catch (err) {
        return reply.status(400).send({
          error: "Failed to parse multipart request",
          details: err instanceof Error ? err.message : String(err),
        });
      }

      if (!fileBuffer || fileBuffer.length === 0) {
        return reply.status(400).send({ error: "No image file provided" });
      }

      try {
        const result = await inspectMetadata(fileBuffer, filename);
        return reply.send(result);
      } catch (err) {
        return reply.status(422).send({
          error: "Failed to read image metadata",
          details: err instanceof Error ? err.message : "Unknown error",
        });
      }
    },
  );

  // Edit endpoint - writes metadata in-place using ExifTool (no pixel re-encoding)
  app.post(
    "/api/v1/tools/image/edit-metadata",
    async (request: FastifyRequest, reply: FastifyReply) => {
      let fileBuffer: Buffer | null = null;
      let filename = "image";
      let settingsRaw: string | null = null;

      try {
        const parts = request.parts();
        for await (const part of parts) {
          if (part.type === "file") {
            const chunks: Buffer[] = [];
            for await (const chunk of part.file) {
              chunks.push(chunk);
            }
            fileBuffer = Buffer.concat(chunks);
            filename = sanitizeFilename(part.filename ?? "image");
          } else if (part.fieldname === "settings") {
            settingsRaw = part.value as string;
          }
        }
      } catch (err) {
        return reply.status(400).send({
          error: "Failed to parse multipart request",
          details: err instanceof Error ? err.message : String(err),
        });
      }

      if (!fileBuffer || fileBuffer.length === 0) {
        return reply.status(400).send({ error: "No image file provided" });
      }

      const validation = await validateImageBuffer(fileBuffer, filename);
      if (!validation.valid) {
        return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
      }

      // Parse and validate settings
      let settings: Settings;
      try {
        const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
        const result = settingsSchema.safeParse(parsed);
        if (!result.success) {
          return reply.status(400).send({
            error: "Invalid settings",
            details: formatZodErrors(result.error.issues),
          });
        }
        settings = result.data;
      } catch {
        return reply.status(400).send({ error: "Settings must be valid JSON" });
      }

      try {
        // Build ExifTool arguments from settings
        const tags = buildTagArgs(settings as EditMetadataSettings);

        // If no changes requested, return the original buffer
        let outputBuffer: Buffer;
        if (tags.length === 0) {
          outputBuffer = fileBuffer;
        } else {
          outputBuffer = await writeMetadata(fileBuffer, filename, tags);
        }

        // Determine content type from validated format
        const contentType = MIME_BY_FORMAT[validation.format] ?? "image/jpeg";

        // Save output to object storage
        const jobId = randomUUID();
        await putObject(`outputs/${jobId}/${filename}`, outputBuffer);

        // Generate preview for non-browser-previewable formats (HEIF, TIFF)
        let previewUrl: string | undefined;
        if (!BROWSER_PREVIEWABLE.has(contentType)) {
          try {
            let previewInput = outputBuffer;
            if (contentType === "image/heif" || contentType === "image/heic") {
              previewInput = await decodeHeic(outputBuffer);
            }
            const previewBuffer = await sharp(previewInput).webp({ quality: 80 }).toBuffer();
            await putObject(`outputs/${jobId}/preview.webp`, previewBuffer);
            previewUrl = `/api/v1/download/${jobId}/preview.webp`;
          } catch {
            // Non-fatal - frontend shows fallback
          }
        }

        return reply.send({
          jobId,
          downloadUrl: `/api/v1/download/${jobId}/${encodeURIComponent(filename)}`,
          previewUrl,
          originalSize: fileBuffer.length,
          processedSize: outputBuffer.length,
        });
      } catch (err) {
        request.log.error({ err, toolId: "edit-metadata" }, "Metadata edit failed");
        return reply.status(422).send({
          error: "Metadata edit failed",
          details: err instanceof Error ? err.message : "Unknown error",
        });
      }
    },
  );

  // Register in pipeline/batch registry
  registerToolProcessFn({
    toolId: "edit-metadata",
    settingsSchema,
    process: async (inputBuffer, settings, filename) => {
      const s = settings as Settings;
      const tags = buildTagArgs(s as EditMetadataSettings);
      const buffer =
        tags.length > 0 ? await writeMetadata(inputBuffer, filename, tags) : inputBuffer;

      // Determine content type from extension
      const ext = filename.split(".").pop()?.toLowerCase() ?? "";
      const extToMime: Record<string, string> = {
        jpg: "image/jpeg",
        jpeg: "image/jpeg",
        png: "image/png",
        webp: "image/webp",
        avif: "image/avif",
        tiff: "image/tiff",
        tif: "image/tiff",
        gif: "image/gif",
        heic: "image/heif",
        heif: "image/heif",
      };

      return {
        buffer,
        filename,
        contentType: extToMime[ext] ?? "image/jpeg",
      };
    },
  });
}
