import { rgbToHex } from "@lib/string.ts";
import { createLogger } from "@lib/log/index.ts";
import { generateThumbhash } from "@lib/thumbhash.ts";
import { parseMediaType } from "https://deno.land/std@0.224.0/media_types/parse_media_type.ts";
import path from "node:path";
import { ensureDir } from "https://deno.land/std@0.216.0/fs/mod.ts";
import { DATA_DIR } from "@lib/env.ts";
import { db } from "@lib/db/sqlite.ts";
import { imageTable } from "@lib/db/schema.ts";
import { eq } from "drizzle-orm";
import sharp from "npm:sharp@next";

const log = createLogger("cache/image");

const imageDir = path.join(DATA_DIR, "images");
await ensureDir(imageDir);

async function getRemoteImage(imageUrl: string) {
  try {
    const sourceRes = await fetch(imageUrl);

    if (!sourceRes.ok) {
      throw new Error(
        `Failed to retrieve image from URL: ${imageUrl}. Status: ${sourceRes.status}`,
      );
    }

    const contentType = sourceRes.headers.get("Content-Type");
    if (!contentType) {
      throw new Error("No Content-Type header in response");
    }

    const mediaType = parseMediaType(contentType)[0];
    if (mediaType.split("/")[0] !== "image") {
      throw new Error("URL does not return an image type");
    }

    log.debug("Fetching image", { imageUrl, mediaType });

    const buffer = await sourceRes.arrayBuffer();
    if (buffer.byteLength === 0) {
      throw new Error("Received empty image buffer");
    }

    return {
      buffer,
      mediaType,
    };
  } catch (error) {
    throw error;
  }
}

/**
 * Calculates dimensions maintaining aspect ratio
 */
function calculateDimensions(
  current: { width: number; height: number },
  target: { width?: number; height?: number },
) {
  if (!target.width && !target.height) {
    return current;
  }

  const ratio = current.width / current.height;

  if (target.width && target.height) {
    // Both dimensions specified - maintain aspect ratio using the most constraining dimension
    const widthRatio = target.width / current.width;
    const heightRatio = target.height / current.height;
    const scale = Math.min(widthRatio, heightRatio);
    return {
      width: Math.round(current.width * scale),
      height: Math.round(current.height * scale),
    };
  } else if (target.width) {
    // Only width specified
    return {
      width: target.width,
      height: Math.round(target.width / ratio),
    };
  } else if (target.height) {
    // Only height specified
    return {
      width: Math.round(target.height * ratio),
      height: target.height,
    };
  }

  return current;
}

async function getLocalImagePath(
  url: string,
  { width, height }: { width?: number; height?: number } = {},
) {
  const { hostname, pathname } = new URL(url);
  let imagePath = path.join(
    imageDir,
    hostname,
    pathname.split("/").filter((s) => s.length).join("-"),
  );
  await ensureDir(imagePath);

  if (width || height) {
    imagePath = path.join(imagePath, `${width ?? "-"}x${height ?? "-"}`);
  } else {
    imagePath = path.join(imagePath, "original");
  }
  return imagePath;
}

/**
 * Retrieves a cached image from local storage
 */
async function getLocalImage(
  url: string,
  { width, height }: { width?: number; height?: number } = {},
) {
  const imagePath = await getLocalImagePath(url, { width, height });

  try {
    const fileInfo = await Deno.stat(imagePath);
    if (fileInfo?.isFile) {
      return Deno.readFile(imagePath);
    }
  } catch (_) {
    // File not found - normal case
  }
}

/**
 * Stores an image in local cache
 */
async function storeLocalImage(
  url: string,
  content: ArrayBuffer,
  { width, height }: { width?: number; height?: number } = {},
) {
  const isValid = await verifyImage(new Uint8Array(content));
  if (!isValid) {
    throw new Error("Invalid image content detected during storage");
  }

  const imagePath = await getLocalImagePath(url, { width, height });

  await Deno.writeFile(imagePath, new Uint8Array(content));
}

/**
 * Resizes an image using Sharp with proper error handling
 */
async function resizeImage(
  imageBuffer: Uint8Array,
  params: {
    width?: number;
    height?: number;
    mediaType: string;
  },
) {
  try {
    log.debug("Resizing image", { params });

    let sharpInstance = sharp(imageBuffer);

    // Get original dimensions
    const metadata = await sharpInstance.metadata();
    if (!metadata.width || !metadata.height) {
      throw new Error("Could not determine image dimensions");
    }

    // Calculate new dimensions
    const newDimensions = calculateDimensions(
      { width: metadata.width, height: metadata.height },
      params,
    );

    switch (params.mediaType) {
      case "image/webp":
        sharpInstance = sharpInstance.webp({ quality: 85 });
        break;
      case "image/png":
        sharpInstance = sharpInstance.png({ quality: 85 });
        break;
      case "image/jpeg":
      default:
        sharpInstance = sharpInstance.jpeg({ quality: 85 });
        break;
    }

    // Perform resize with proper options
    const resized = await sharpInstance
      .resize({
        width: newDimensions.width,
        height: newDimensions.height,
        fit: "inside",
        withoutEnlargement: true,
      })
      .toBuffer();

    return new Uint8Array(resized);
  } catch (error) {
    log.error("Error during image resize:", error);
    throw error;
  }
}

/**
 * Creates a thumbhash for image preview
 */
async function createThumbhash(
  image: Uint8Array,
): Promise<{ hash: string; average: string }> {
  try {
    const resizedImage = await sharp(image)
      .resize(100, 100, { fit: "cover" }) // Keep aspect ratio within bounds
      .toFormat("png")
      .ensureAlpha()
      .raw()
      .toBuffer();

    const [hash, average] = generateThumbhash(resizedImage, 100, 100);

    return {
      hash: btoa(String.fromCharCode(...hash)),
      average: rgbToHex(average.r, average.g, average.b),
    };
  } catch (err) {
    throw new Error(`Failed to create thumbhash: ${err}`);
  }
}

/**
 * Verifies that an image buffer contains valid image data
 */
async function verifyImage(imageBuffer: Uint8Array): Promise<boolean> {
  try {
    const metadata = await sharp(imageBuffer).metadata();
    return !!(metadata.width && metadata.height && metadata.format);
  } catch (error) {
    log.error("Image verification failed:", error);
    return false;
  }
}

/**
 * Gets image content with proper caching and resizing
 */
export async function getImageContent(
  url: string,
  { width, height }: { width?: number; height?: number } = {},
): Promise<{ content: ArrayBuffer; mimeType: string }> {
  log.debug("Getting image content", { url, width, height });

  // Check if we have the image metadata in database
  const image = await getImage(url);

  // Try to get cached resized version
  const cachedImage = await getLocalImage(url, { width, height });
  if (cachedImage) {
    return { content: cachedImage, mimeType: image.mime };
  }

  // Try to get cached original version
  let originalImage = await getLocalImage(url);

  // Fetch and cache original if needed
  if (!originalImage) {
    const fetchedImage = await getRemoteImage(url);
    await storeLocalImage(url, fetchedImage.buffer);
    originalImage = new Uint8Array(fetchedImage.buffer);
  }

  // Resize image
  const resizedImage = await resizeImage(originalImage, {
    width,
    height,
    mediaType: image.mime,
  });

  // Cache resized version
  await storeLocalImage(url, resizedImage, { width, height });

  return { content: resizedImage, mimeType: image.mime };
}

/**
 * Gets or creates image metadata in database
 */
export async function getImage(url: string) {
  log.debug("Getting image metadata", { url });

  try {
    // Check database first
    const image = await db.select().from(imageTable)
      .where(eq(imageTable.url, url))
      .limit(1)
      .then((images) => images[0]);

    if (image) {
      return image;
    }

    // Fetch and process new image
    const imageContent = await getRemoteImage(url);
    await storeLocalImage(url, imageContent.buffer);

    // Generate thumbhash
    const thumbhash = await createThumbhash(
      new Uint8Array(imageContent.buffer),
    );

    // Store in database
    const [newImage] = await db.insert(imageTable).values({
      url: url,
      blurhash: thumbhash.hash,
      average: thumbhash.average,
      mime: imageContent.mediaType,
    }).returning();

    return newImage;
  } catch (error) {
    log.error("Error in getImage:", error);
    throw error;
  }
}
