/**
 * Image dimension helpers for QQ Bot markdown image syntax.
 *
 * QQ Bot markdown images use `![#widthpx #heightpx](url)`.
 */

import { Buffer } from "buffer";
import { fetchRemoteMedia } from "openclaw/plugin-sdk/media-runtime";
import type { SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
import { debugLog } from "./debug-log.js";

export interface ImageSize {
  width: number;
  height: number;
}

/** Default dimensions used when probing fails. */
export const DEFAULT_IMAGE_SIZE: ImageSize = { width: 512, height: 512 };

/**
 * Parse image dimensions from the PNG header.
 */
function parsePngSize(buffer: Buffer): ImageSize | null {
  // PNG signature: 89 50 4E 47 0D 0A 1A 0A
  if (buffer.length < 24) {
    return null;
  }
  if (buffer[0] !== 0x89 || buffer[1] !== 0x50 || buffer[2] !== 0x4e || buffer[3] !== 0x47) {
    return null;
  }
  // The IHDR chunk begins at byte 8, with width/height at 16..23.
  const width = buffer.readUInt32BE(16);
  const height = buffer.readUInt32BE(20);
  return { width, height };
}

/** Parse image dimensions from JPEG SOF0/SOF2 markers. */
function parseJpegSize(buffer: Buffer): ImageSize | null {
  // JPEG signature: FF D8 FF
  if (buffer.length < 4) {
    return null;
  }
  if (buffer[0] !== 0xff || buffer[1] !== 0xd8) {
    return null;
  }

  let offset = 2;
  while (offset < buffer.length - 9) {
    if (buffer[offset] !== 0xff) {
      offset++;
      continue;
    }

    const marker = buffer[offset + 1];
    // SOF0 (0xC0) and SOF2 (0xC2) contain dimensions.
    if (marker === 0xc0 || marker === 0xc2) {
      // Layout: FF C0 length(2) precision(1) height(2) width(2)
      if (offset + 9 <= buffer.length) {
        const height = buffer.readUInt16BE(offset + 5);
        const width = buffer.readUInt16BE(offset + 7);
        return { width, height };
      }
    }

    // Skip the current block.
    if (offset + 3 < buffer.length) {
      const blockLength = buffer.readUInt16BE(offset + 2);
      offset += 2 + blockLength;
    } else {
      break;
    }
  }

  return null;
}

/** Parse image dimensions from the GIF header. */
function parseGifSize(buffer: Buffer): ImageSize | null {
  if (buffer.length < 10) {
    return null;
  }
  const signature = buffer.toString("ascii", 0, 6);
  if (signature !== "GIF87a" && signature !== "GIF89a") {
    return null;
  }
  const width = buffer.readUInt16LE(6);
  const height = buffer.readUInt16LE(8);
  return { width, height };
}

/** Parse image dimensions from WebP headers. */
function parseWebpSize(buffer: Buffer): ImageSize | null {
  if (buffer.length < 30) {
    return null;
  }

  // Check the RIFF and WEBP signatures.
  const riff = buffer.toString("ascii", 0, 4);
  const webp = buffer.toString("ascii", 8, 12);
  if (riff !== "RIFF" || webp !== "WEBP") {
    return null;
  }

  const chunkType = buffer.toString("ascii", 12, 16);

  // VP8 (lossy)
  if (chunkType === "VP8 ") {
    // The VP8 frame header starts at byte 23 and uses the 9D 01 2A signature.
    if (buffer.length >= 30 && buffer[23] === 0x9d && buffer[24] === 0x01 && buffer[25] === 0x2a) {
      const width = buffer.readUInt16LE(26) & 0x3fff;
      const height = buffer.readUInt16LE(28) & 0x3fff;
      return { width, height };
    }
  }

  // VP8L (lossless)
  if (chunkType === "VP8L") {
    // VP8L signature: 0x2F
    if (buffer.length >= 25 && buffer[20] === 0x2f) {
      const bits = buffer.readUInt32LE(21);
      const width = (bits & 0x3fff) + 1;
      const height = ((bits >> 14) & 0x3fff) + 1;
      return { width, height };
    }
  }

  // VP8X (extended format)
  if (chunkType === "VP8X") {
    if (buffer.length >= 30) {
      // Width and height live at 24..26 and 27..29 as 24-bit little-endian values.
      const width = (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) + 1;
      const height = (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) + 1;
      return { width, height };
    }
  }

  return null;
}

/** Parse image dimensions from raw image bytes. */
export function parseImageSize(buffer: Buffer): ImageSize | null {
  // Try each supported image format in sequence.
  return (
    parsePngSize(buffer) ?? parseJpegSize(buffer) ?? parseGifSize(buffer) ?? parseWebpSize(buffer)
  );
}

/**
 * SSRF policy for image-dimension probing.  Generic public-network-only blocking
 * (no hostname allowlist) because markdown image URLs can legitimately point to
 * any public host, not just QQ-owned CDNs.
 */
const IMAGE_PROBE_SSRF_POLICY: SsrFPolicy = {};

/**
 * Fetch image dimensions from a public URL using only the first 64 KB.
 *
 * Uses {@link fetchRemoteMedia} with SSRF guard to block probes against
 * private/reserved/loopback/link-local/metadata destinations.
 */
export async function getImageSizeFromUrl(
  url: string,
  timeoutMs = 5000,
): Promise<ImageSize | null> {
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    try {
      const { buffer } = await fetchRemoteMedia({
        url,
        maxBytes: 65_536,
        maxRedirects: 0,
        ssrfPolicy: IMAGE_PROBE_SSRF_POLICY,
        requestInit: {
          signal: controller.signal,
          headers: {
            Range: "bytes=0-65535",
            "User-Agent": "QQBot-Image-Size-Detector/1.0",
          },
        },
      });

      const size = parseImageSize(buffer);
      if (size) {
        debugLog(
          `[image-size] Got size from URL: ${size.width}x${size.height} - ${url.slice(0, 60)}...`,
        );
      }
      return size;
    } finally {
      clearTimeout(timeoutId);
    }
  } catch (err) {
    debugLog(`[image-size] Error fetching ${url.slice(0, 60)}...: ${String(err)}`);
    return null;
  }
}

/** Parse image dimensions from a Base64 data URL. */
export function getImageSizeFromDataUrl(dataUrl: string): ImageSize | null {
  try {
    // Format: data:image/png;base64,xxxxx
    const matches = dataUrl.match(/^data:image\/[^;]+;base64,(.+)$/);
    if (!matches) {
      return null;
    }

    const base64Data = matches[1];
    const buffer = Buffer.from(base64Data, "base64");

    const size = parseImageSize(buffer);
    if (size) {
      debugLog(`[image-size] Got size from Base64: ${size.width}x${size.height}`);
    }

    return size;
  } catch (err) {
    debugLog(`[image-size] Error parsing Base64: ${String(err)}`);
    return null;
  }
}

/**
 * Resolve image dimensions from either an HTTP URL or a Base64 data URL.
 */
export async function getImageSize(source: string): Promise<ImageSize | null> {
  if (source.startsWith("data:")) {
    return getImageSizeFromDataUrl(source);
  }

  if (source.startsWith("http://") || source.startsWith("https://")) {
    return getImageSizeFromUrl(source);
  }

  return null;
}

/** Format a markdown image with QQ Bot width/height annotations. */
export function formatQQBotMarkdownImage(url: string, size: ImageSize | null): string {
  const { width, height } = size ?? DEFAULT_IMAGE_SIZE;
  return `![#${width}px #${height}px](${url})`;
}

/** Return true when markdown already contains QQ Bot size annotations. */
export function hasQQBotImageSize(markdownImage: string): boolean {
  return /!\[#\d+px\s+#\d+px\]/.test(markdownImage);
}

/** Extract width and height from QQBot markdown image syntax: `![#Wpx #Hpx](url)`. */
export function extractQQBotImageSize(markdownImage: string): ImageSize | null {
  const match = markdownImage.match(/!\[#(\d+)px\s+#(\d+)px\]/);
  if (match) {
    return { width: parseInt(match[1], 10), height: parseInt(match[2], 10) };
  }
  return null;
}
