/**
 * Unified image metadata utilities for embedding/extracting user color and banner
 * Supports PNG, WebP, JPEG, and GIF formats
 */

/* eslint-disable no-plusplus */
/* eslint-disable no-console */

import {
  embedColorInPNG,
  extractColorFromPNG,
  removeColorFromPNG,
  embedMetadataInPNG,
  extractMetadataFromPNG,
  extractBannerFromPNG,
} from './pngMetadata';
import { embedColorInWebP, extractColorFromWebP, removeColorFromWebP } from './webpMetadata';
import { embedColorInJPEG, extractColorFromJPEG, removeColorFromJPEG } from './jpegMetadata';
import { embedColorInGIF, extractColorFromGIF, removeColorFromGIF } from './gifMetadata';

/**
 * LRU Cache implementation with max size limit
 */
class LRUCache<K, V> {
  private cache: Map<K, V>;

  private maxSize: number;

  constructor(maxSize: number) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }

  get(key: K): V | undefined {
    const value = this.cache.get(key);
    if (value !== undefined) {
      this.cache.delete(key);
      this.cache.set(key, value);
    }
    return value;
  }

  set(key: K, value: V): void {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    else if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      if (firstKey !== undefined) {
        this.cache.delete(firstKey);
      }
    }
    this.cache.set(key, value);
  }

  clear(): void {
    this.cache.clear();
  }
}

/**
 * Gradient configuration for profile styling
 * All colors support RGBA format (e.g., #FF550080 for 50% opacity)
 */
export type ProfileGradient = {
  /** CSS gradient direction (e.g., "to bottom", "45deg", "to bottom right") */
  direction: string;
  /** Start color in hex format with optional alpha (#RRGGBB or #RRGGBBAA) */
  startColor: string;
  /** End color in hex format with optional alpha (#RRGGBB or #RRGGBBAA) */
  stopColor: string;
};

export type ImageMetadata = {
  /** Profile name color */
  color?: string;
  /** Banner MXC URL */
  banner?: string;
  /** Avatar border color with optional transparency (#RRGGBB or #RRGGBBAA) */
  avatarBorderColor?: string;
  /** Profile card gradient */
  gradient?: ProfileGradient;
};

/** PNG signature bytes */
const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];

/** RIFF/WebP signature bytes */
const RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46]; // "RIFF"
const WEBP_OFFSET_SIGNATURE = [0x57, 0x45, 0x42, 0x50]; // "WEBP" at offset 8

/** JPEG signature */
const JPEG_SIGNATURE = [0xFF, 0xD8];

/** GIF signatures */
const GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]; // "GIF87a"
const GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; // "GIF89a"

export type ImageFormat = 'png' | 'webp' | 'jpeg' | 'gif' | 'unknown';

/**
 * Detect image format from raw data
 */
export function detectImageFormat(data: ArrayBuffer | Uint8Array): ImageFormat {
  const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
  
  if (bytes.length < 12) return 'unknown';
  
  // Check PNG signature
  let isPNG = true;
  for (let i = 0; i < 8; i++) {
    if (bytes[i] !== PNG_SIGNATURE[i]) {
      isPNG = false;
      break;
    }
  }
  if (isPNG) return 'png';
  
  // Check WebP signature (RIFF + WEBP)
  let isRIFF = true;
  for (let i = 0; i < 4; i++) {
    if (bytes[i] !== RIFF_SIGNATURE[i]) {
      isRIFF = false;
      break;
    }
  }
  if (isRIFF) {
    let isWebP = true;
    for (let i = 0; i < 4; i++) {
      if (bytes[8 + i] !== WEBP_OFFSET_SIGNATURE[i]) {
        isWebP = false;
        break;
      }
    }
    if (isWebP) return 'webp';
  }
  
  // Check JPEG signature (FFD8)
  if (bytes[0] === JPEG_SIGNATURE[0] && bytes[1] === JPEG_SIGNATURE[1]) {
    return 'jpeg';
  }
  
  // Check GIF signatures
  let isGIF87a = true;
  for (let i = 0; i < 6; i++) {
    if (bytes[i] !== GIF87A_SIGNATURE[i]) {
      isGIF87a = false;
      break;
    }
  }
  if (isGIF87a) return 'gif';
  
  let isGIF89a = true;
  for (let i = 0; i < 6; i++) {
    if (bytes[i] !== GIF89A_SIGNATURE[i]) {
      isGIF89a = false;
      break;
    }
  }
  if (isGIF89a) return 'gif';
  
  return 'unknown';
}

/**
 * Extract paarrot color from image data (PNG, WebP, JPEG, or GIF)
 * @param imageData - Raw image data as ArrayBuffer or Uint8Array
 * @returns The color string if found, or undefined
 */
export function extractColorFromImage(imageData: ArrayBuffer | Uint8Array): string | undefined {
  const format = detectImageFormat(imageData);
  
  console.log('[extractColorFromImage] Detected format:', format);
  
  switch (format) {
    case 'png':
      return extractColorFromPNG(imageData);
    case 'webp':
      return extractColorFromWebP(imageData);
    case 'jpeg':
      return extractColorFromJPEG(imageData);
    case 'gif':
      return extractColorFromGIF(imageData);
    default:
      console.warn('[extractColorFromImage] Unknown image format');
      return undefined;
  }
}

/**
 * Extract paarrot banner URL from image data (any supported format)
 * @param imageData - Raw image data as ArrayBuffer or Uint8Array
 * @returns The banner MXC URL if found, or undefined
 */
export function extractBannerFromImage(imageData: ArrayBuffer | Uint8Array): string | undefined {
  const format = detectImageFormat(imageData);
  
  switch (format) {
    case 'png':
      return extractBannerFromPNG(imageData);
    // For now, only PNG supports banner. Add other formats later
    default:
      console.warn('[extractBannerFromImage] Format not yet supported for banner');
      return undefined;
  }
}

/**
 * Extract all paarrot metadata from image data (any supported format)
 * @param imageData - Raw image data as ArrayBuffer or Uint8Array
 * @returns Object with color and banner if found
 */
export function extractMetadataFromImage(imageData: ArrayBuffer | Uint8Array): ImageMetadata {
  const format = detectImageFormat(imageData);
  
  console.log('[extractMetadataFromImage] Detected format:', format);
  
  switch (format) {
    case 'png':
      const pngMetadata = extractMetadataFromPNG(imageData);
      console.log('[extractMetadataFromImage] PNG metadata:', pngMetadata);
      return pngMetadata;
    // For other formats, extract color only for now
    case 'webp':
    case 'jpeg':
    case 'gif': {
      const color = extractColorFromImage(imageData);
      console.log('[extractMetadataFromImage] Non-PNG color:', color);
      return color ? { color } : {};
    }
    default:
      console.warn('[extractMetadataFromImage] Unknown image format');
      return {};
  }
}

/**
 * Embed paarrot color into image data (PNG, WebP, JPEG, or GIF)
 * @param imageData - Raw image data as ArrayBuffer or Uint8Array
 * @param color - The color string to embed (e.g., '#ff5500')
 * @returns New image data with embedded color, or null if not a supported format
 */
export function embedColorInImage(imageData: ArrayBuffer | Uint8Array, color: string): Uint8Array | null {
  const format = detectImageFormat(imageData);
  
  switch (format) {
    case 'png':
      return embedColorInPNG(imageData, color);
    case 'webp':
      return embedColorInWebP(imageData, color);
    case 'jpeg':
      return embedColorInJPEG(imageData, color);
    case 'gif':
      return embedColorInGIF(imageData, color);
    default:
      console.warn('[embedColorInImage] Unknown image format, cannot embed color');
      return null;
  }
}

/**
 * True when metadata includes fields that only PNG tEXt chunks can store.
 */
export function needsPngForMetadata(metadata: ImageMetadata): boolean {
  return Boolean(metadata.banner || metadata.avatarBorderColor || metadata.gradient);
}

/**
 * Convert image data to PNG via canvas (needed for banner/border/gradient metadata).
 */
export async function convertImageDataToPng(
  imageData: ArrayBuffer | Uint8Array
): Promise<Uint8Array | null> {
  const bytes = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
  const format = detectImageFormat(bytes);
  if (format === 'png') return bytes;
  if (format === 'unknown') return null;

  try {
    const blob = uint8ArrayToBlob(bytes, getMimeType(format));
    const bitmap = await createImageBitmap(blob);
    try {
      const canvas = document.createElement('canvas');
      canvas.width = bitmap.width;
      canvas.height = bitmap.height;
      const ctx = canvas.getContext('2d');
      if (!ctx) return null;
      ctx.drawImage(bitmap, 0, 0);

      const pngBlob = await new Promise<Blob | null>((resolve) => {
        canvas.toBlob(resolve, 'image/png');
      });
      if (!pngBlob) return null;
      return new Uint8Array(await pngBlob.arrayBuffer());
    } finally {
      bitmap.close();
    }
  } catch (error) {
    console.error('[convertImageDataToPng] Conversion failed:', error);
    return null;
  }
}

/**
 * Build a Blob from Uint8Array without relying on BufferSource/BlobPart quirks
 * across TS DOM lib / Electron versions (avoids SharedArrayBuffer typing issues).
 */
export function uint8ArrayToBlob(data: Uint8Array, type: string): Blob {
  const copy = new ArrayBuffer(data.byteLength);
  new Uint8Array(copy).set(data);
  return new Blob([copy], { type });
}

/**
 * Embed paarrot metadata (color and/or banner) into image data
 * Preserves existing metadata that is not being updated
 * @param imageData - Raw image data as ArrayBuffer or Uint8Array
 * @param metadata - Object with color and/or banner to embed
 * @returns New image data with embedded metadata, or null if not a supported format
 */
export function embedMetadataInImage(
  imageData: ArrayBuffer | Uint8Array,
  metadata: ImageMetadata
): Uint8Array | null {
  const format = detectImageFormat(imageData);
  const bytes = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);

  switch (format) {
    case 'png':
      return embedMetadataInPNG(imageData, metadata);
    // Non-PNG: color via format-specific chunks; banner/border/gradient need PNG (convert first)
    case 'webp':
    case 'jpeg':
    case 'gif':
      if (needsPngForMetadata(metadata)) {
        console.warn(
          '[embedMetadataInImage] Banner/border/gradient require PNG; convert with convertImageDataToPng first. Got:',
          format
        );
        return null;
      }
      if (metadata.color) {
        return embedColorInImage(imageData, metadata.color);
      }
      // Supported format with nothing to embed — succeed as a no-op so avatar upload isn't blocked
      return bytes;
    default:
      console.warn('[embedMetadataInImage] Unknown image format, cannot embed metadata');
      return null;
  }
}

/**
 * Remove paarrot color from image data (PNG, WebP, JPEG, or GIF)
 * @param imageData - Raw image data as ArrayBuffer or Uint8Array
 * @returns New image data without the color, or null if not a supported format
 */
export function removeColorFromImage(imageData: ArrayBuffer | Uint8Array): Uint8Array | null {
  const format = detectImageFormat(imageData);
  
  switch (format) {
    case 'png':
      return removeColorFromPNG(imageData);
    case 'webp':
      return removeColorFromWebP(imageData);
    case 'jpeg':
      return removeColorFromJPEG(imageData);
    case 'gif':
      return removeColorFromGIF(imageData);
    default:
      console.warn('[removeColorFromImage] Unknown image format');
      return null;
  }
}

/**
 * Get the MIME type for an image format
 */
export function getMimeType(format: ImageFormat): string {
  switch (format) {
    case 'png':
      return 'image/png';
    case 'webp':
      return 'image/webp';
    case 'jpeg':
      return 'image/jpeg';
    case 'gif':
      return 'image/gif';
    default:
      return 'application/octet-stream';
  }
}

/**
 * Get the file extension for an image format
 */
export function getExtension(format: ImageFormat): string {
  switch (format) {
    case 'png':
      return 'png';
    case 'webp':
      return 'webp';
    case 'jpeg':
      return 'jpg';
    case 'gif':
      return 'gif';
    default:
      return 'bin';
  }
}

// LRU cache for avatar metadata to avoid refetching
const avatarMetadataCache = new LRUCache<string, { metadata: ImageMetadata; timestamp: number }>(100);
const METADATA_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes

// In-flight fetch tracking to prevent duplicate parallel requests
const inflightColorFetches = new Map<string, Promise<string | undefined>>();
const inflightMetadataFetches = new Map<string, Promise<ImageMetadata>>();

/**
 * Fetch and extract color from an image URL (supports all formats)
 * @param url - HTTP URL to fetch the image from
 * @param accessToken - Optional access token for authenticated media
 * @returns The color string if found, or undefined
 */
export async function fetchAndExtractColor(url: string, accessToken?: string | null): Promise<string | undefined> {
  // Check if full metadata is cached
  const cached = avatarMetadataCache.get(url);
  if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
    console.log('[fetchAndExtractColor] Using cached color for', url, ':', cached.metadata.color);
    return cached.metadata.color;
  }

  // Check if fetch already in progress
  const inflightKey = `${url}:${accessToken || ''}`;
  const existingFetch = inflightColorFetches.get(inflightKey);
  if (existingFetch) {
    console.log('[fetchAndExtractColor] Reusing in-flight fetch for', url);
    return existingFetch;
  }

  console.log('[fetchAndExtractColor] Fetching and extracting color from', url);
  
  const fetchPromise = (async () => {
    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
      });
      
      console.log('[fetchAndExtractColor] Fetch response status:', response.ok, response.status);
      
      if (!response.ok) return undefined;
      
      const data = await response.arrayBuffer();
      console.log('[fetchAndExtractColor] Downloaded', data.byteLength, 'bytes');
      
      const color = extractColorFromImage(data);
      console.log('[fetchAndExtractColor] Extracted color:', color);
      
      // Don't cache here - fetchAndExtractMetadata will cache full metadata
      // This avoids cache collision where color-only fetch overwrites full metadata
      
      return color;
    } catch (error) {
      console.error('[fetchAndExtractColor] Error fetching/extracting color:', error);
      return undefined;
    } finally {
      inflightColorFetches.delete(inflightKey);
    }
  })();

  inflightColorFetches.set(inflightKey, fetchPromise);
  return fetchPromise;
}

/**
 * Fetch and extract all metadata from an image URL
 * @param url - HTTP URL to fetch the image from
 * @param accessToken - Optional access token for authenticated media
 * @returns Object with color and banner if found
 */
export async function fetchAndExtractMetadata(url: string, accessToken?: string | null): Promise<ImageMetadata> {
  // Check cache first
  const cached = avatarMetadataCache.get(url);
  if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
    console.log('[fetchAndExtractMetadata] Using cached metadata for', url, ':', cached.metadata);
    return cached.metadata;
  }

  // Check if fetch already in progress
  const inflightKey = `${url}:${accessToken || ''}`;
  const existingFetch = inflightMetadataFetches.get(inflightKey);
  if (existingFetch) {
    console.log('[fetchAndExtractMetadata] Reusing in-flight fetch for', url);
    return existingFetch;
  }

  console.log('[fetchAndExtractMetadata] Fetching metadata from', url);

  const fetchPromise = (async () => {
    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
      });
      
      console.log('[fetchAndExtractMetadata] Fetch response status:', response.ok, response.status);
      
      if (!response.ok) return {};
      
      const data = await response.arrayBuffer();
      console.log('[fetchAndExtractMetadata] Downloaded', data.byteLength, 'bytes');
      
      const metadata = extractMetadataFromImage(data);
      console.log('[fetchAndExtractMetadata] Extracted metadata:', metadata);
      
      // Cache the result
      avatarMetadataCache.set(url, { 
        metadata, 
        timestamp: Date.now() 
      });
      
      return metadata;
    } catch {
      return {};
    } finally {
      inflightMetadataFetches.delete(inflightKey);
    }
  })();

  inflightMetadataFetches.set(inflightKey, fetchPromise);
  return fetchPromise;
}
