import config from "../config.ts";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { v4 as uuidv4 } from "uuid";
import path from "node:path";
import fs from "node:fs";
import {
  submittedCalendarIds,
  completedImageURLs,
  resultURLCalendarIdMap,
  uuidURLMap,
  ongoingUploads,
  calendarNumProcessed,
  calendarSQSMap,
  calendarIdCountMap,
} from "../state.ts";
import { exec } from "node:child_process";
import { reportDone } from "./bufferingService.ts";
import { DeleteMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
import { disownExecutor } from "../execHelper.ts";
import { enqueUpdateCalendarStatusEspo } from "./espoStatusService.ts";

const s3 = new S3Client({
  region: config.aws.region,
  credentials: {
    accessKeyId: config.aws.accessKeyId,
    secretAccessKey: config.aws.secretAccessKey,
  },
});

const sqsClient = new SQSClient({
  region: config.aws.region,
  credentials: {
    accessKeyId: config.aws.accessKeyId,
    secretAccessKey: config.aws.secretAccessKey,
  },
});

// returns the S3 URL
async function uploadFile(
  localPath: string,
  S3URL: string,
  calendarId: string
): Promise<string> {
  if (ongoingUploads.includes(S3URL)) {
    console.log("File is already being uploaded", S3URL);
    throw new Error("File is already being uploaded");
  }

  try {
    ongoingUploads.push(S3URL);

    console.log("uploading file", { localPath, S3URL });

    // Parse URL -> bucket/key
    const url = new URL(S3URL);
    const bucket = url.hostname.split(".")[0];
    const key = url.pathname.startsWith("/")
      ? url.pathname.slice(1)
      : url.pathname;

    // Basic existence check
    await fs.promises.access(localPath, fs.constants.F_OK);

    // Infer content type from extension
    const ext = path.extname(localPath).toLowerCase();
    const contentType =
      ext === ".png"
        ? "image/png"
        : ext === ".jpg" || ext === ".jpeg"
        ? "image/jpeg"
        : ext === ".webp"
        ? "image/webp"
        : ext === ".gif"
        ? "image/gif"
        : ext === ".svg"
        ? "image/svg+xml"
        : "application/octet-stream";

    const bodyStream = fs.createReadStream(localPath);
    const putCmd = new PutObjectCommand({
      Bucket: bucket,
      Key: key,
      Body: bodyStream,
      ContentType: contentType,
    });

    await s3.send(putCmd);

    exec(`echo ${S3URL} >> ./uploads.txt`);

    if (!completedImageURLs.has(S3URL)) {
      completedImageURLs.add(S3URL);
    }

    reportDone(calendarId, S3URL);

    // setTimeout(() => {
    //   fs.promises.unlink(localPath);
    // }, config.deleteTimeout);

    disownExecutor(` sleep ${config.deleteTimeout / 1000} && rm ${localPath}`);

    return S3URL;
  } catch (error) {
    console.error("Error uploading file:", error);
    throw error;
  } finally {
    ongoingUploads.splice(ongoingUploads.indexOf(S3URL), 1);
  }
}

async function deleteMessage(messageId: string) {
  const command = new DeleteMessageCommand({
    QueueUrl: config.aws.queueUrl,
    ReceiptHandle: messageId,
  });
  await sqsClient.send(command);
}

async function getUnUploadedImages(): Promise<
  { fileName: string; url: string }[]
> {
  const generatedImages = fs.readdirSync(
    path.join(config.facefusion.path, config.outputDir)
  );

  const unUploadedImages: { fileName: string; url: string }[] = [];

  for (const image of generatedImages) {
    try {
      const uuid = image.split("...---...").at(-1)?.split(".")[0];

      const url = uuidURLMap.get(uuid ?? "");

      if (!url) continue; // image is not a part of the current run

      if (completedImageURLs.has(url)) continue; // image is already uploaded

      unUploadedImages.push({
        fileName: path.resolve(
          path.join(config.facefusion.path, config.outputDir, image)
        ),
        url,
      });
    } catch (error) {
      console.error("Error getting unuploaded images:", error);
      continue;
    }
  }

  return unUploadedImages;
}

async function uploadImages(images: { fileName: string; url: string }[]) {
  const uploadedImages = await Promise.allSettled(
    images.map((image) =>
      uploadFile(
        image.fileName,
        image.url,
        resultURLCalendarIdMap.get(image.url) ?? ""
      )
    )
  );

  // this is not needed because uploadFile already records the URL in completedImageURLs

  // for (const image of uploadedImages) {
  //   if (image.status === "fulfilled") {
  //     const url = image.value;
  //     const calendarId = resultURLCalendarIdMap.get(url) ?? "";

  //     console.log("uploaded image", { calendarId, url });

  //     if (completedImageURLs.has(url)) continue; // image is already completed

  //     completedImageURLs.add(
  //       url
  //     );
  //   }
  // }
}

async function getUnSubmittedCalendars(): Promise<string[]> {
  const processedCalendarIds: string[] = [];

  for (const calendarId of calendarNumProcessed.keys()) {
    const count = calendarIdCountMap.get(calendarId) || 12;
    if (calendarNumProcessed.get(calendarId)?.size === count)
      processedCalendarIds.push(calendarId);
  }

  const unSubmittedCalendars: string[] = [];

  for (const calendarId of processedCalendarIds) {
    if (submittedCalendarIds.has(calendarId)) continue;
    unSubmittedCalendars.push(calendarId);
  }

  return unSubmittedCalendars;
}

async function submitCalendars(calendarIds: string[]) {
  for (const calendarId of calendarIds) {
    console.log("submitting calendar", calendarId);
    exec(`echo "${calendarId}" >> ./submitted.txt`);
    submittedCalendarIds.add(calendarId);
    try {
      const sqsId = calendarSQSMap.get(calendarId) ?? "THIS SHOULD NOT HAPPEN";
      console.log({ calendarId, sqsId });
      deleteMessage(sqsId).then(() => {
        console.log(`Deleted calendar from sqs ${calendarId}`);
        enqueUpdateCalendarStatusEspo(calendarId, "DONE");
        exec(`echo "${calendarId}" >> ./deleted.txt`);
      });
    } catch (error) {
      console.error(`Error deleting calendar from sqs ${calendarId}:`, error);
    }
  }
}

async function uploadLoop() {
  while (true) {
    await new Promise((resolve) => setTimeout(resolve, config.uploadLoopSleep));

    try {
      console.log(`UploadLoop running`);
      const unuploadedImages = await getUnUploadedImages();

      if (unuploadedImages.length === 0) continue;

      console.log(`Found ${unuploadedImages.length} unuploaded images`);

      await uploadImages(unuploadedImages);

      console.log(`already uploaded ${completedImageURLs.size} images`);

      const unsubmittedCalendars = await getUnSubmittedCalendars();

      if (unsubmittedCalendars.length === 0) continue;

      console.log(`Found ${unsubmittedCalendars.length} unsubmitted calendars`);

      await submitCalendars(unsubmittedCalendars);

      console.log(`already submitted ${submittedCalendarIds.size} calendars`);

      // break;
    } catch (error) {
      console.error("Error processing upload loop:", error);
    }
  }
}

export async function startUpload() {
  try {
    uploadLoop();
  } catch (error) {
    console.error("Error processing upload loop:", error);
    throw error;
  }
}
