import fs from "fs-extra";
import path from "path";
import Enquirer from "enquirer";

import { formatMessage, logger } from "../utils/logger";
import {
  copyFileToProject,
  getFitsFromDirectory,
  matchSetFile,
  getImageSpecFromSetName,
  isAsiAirDirectoryF,
} from "../utils/utils";
import {
  FileImageSpec,
  LayerSet,
  LightsFlatsMatch,
  PotoProject,
} from "../utils/types";
import { POTO_JSON, POTO_VERSION } from "../utils/const";

export type PrepareProps = {
  inputDirectories: string[];
  projectDirectory: string;
};

const enquirer = new Enquirer();

const prepare = async ({
  inputDirectories,
  projectDirectory,
}: PrepareProps) => {
  // TODO. logger.command("prepare"); to introduce the command in the logs.

  const continueProcess = await ensureProjectDirectoryExists(projectDirectory);

  if (!continueProcess) {
    logger.warning("Aborted.");
    return;
  }

  logger.step("Reading input directories");

  const inputFiles: FileImageSpec[] = [];
  for (const inputDirectory of inputDirectories) {
    logger.debug(`Reading input directory ${inputDirectory}`);
    const files = await getAllFitsInInputDirectory(
      inputDirectory,
      projectDirectory,
    );
    inputFiles.push(...files);
  }

  let allLights = inputFiles.filter(file => file.type === "Light");
  const allFlats = inputFiles.filter(file => file.type === "Flat");
  const allDarksBiases = inputFiles.filter(
    file => file.type === "Dark" || file.type === "Bias",
  );

  logger.info(`Found ${inputFiles.length} .fit files in input directories 🌋.`);
  logger.info(`Including ${allLights.length} lights 🌟.`);
  logger.space();
  logger.info("Light sequences:");
  logger.info(
    `${[
      ...new Set(
        allLights
          .sort((a, b) => a.sequenceId.localeCompare(b.sequenceId))
          .map(light => `${light.setName} ${light.sequenceId}`),
      ),
    ]
      .map(x => `- ${x}`)
      .join("\n")}`,
  );

  logger.step("Matching lights and flats (early stage)");

  const allFlatsMatchingLights = getFlatsMatchingLightsNaive({
    lights: allLights,
    flats: allFlats,
  });

  const notMatchedLights = getLightsMatchingLightNaive({
    lights: allLights,
    flats: allFlats,
  });
  allLights = allLights.filter(light => !notMatchedLights.includes(light));

  logger.step("Matching lights and flats (final stage)");

  const lightsFlatsMatches = await matchLightsToFlats(
    allFlatsMatchingLights,
    allLights,
  );

  let layerSets: LayerSet[] = initLayerSetsWithLightsnFlats(
    lightsFlatsMatches,
    allLights,
    allFlatsMatchingLights,
    projectDirectory,
  );

  logger.step("Tagging darks and biases");

  logger.info(
    `Found ${allDarksBiases.length} darks+baises files (without temperature filtering).`,
  );

  layerSets = await AssignDarksBiasesToLayerSets(layerSets, allDarksBiases);

  logger.step("Preview before dispatching");

  const { go, metrics } = await previewBeforeDispatching(layerSets);

  if (!go) {
    logger.warning("Aborted.");
    return;
  }
  logger.step("Dispatching");

  const potoProject: PotoProject = {
    generatedAt: new Date(),
    potoVersion: POTO_VERSION,
    metrics,
    layerSets,
  };

  await dispatchProject(potoProject, projectDirectory);

  logger.success("Dispatch complete.");

  // TODO. custom sort if LRGBSHO filter names to ease reading.
};

export default prepare;

/**
 * Ensure that the project directory exists.
 * If it does not exist, ask the user if they want to create it.
 * If it already exists, warn the user that the project will be overwritten (but may have extra files in it).
 *
 * @param projectDirectory - The directory of the current project.
 * @returns A boolean indicating if we can continue.
 */
const ensureProjectDirectoryExists = async (
  projectDirectory: string,
): Promise<boolean> => {
  if (!fs.existsSync(projectDirectory)) {
    const { createProjectDirectory } = (await enquirer.prompt({
      type: "confirm",
      name: "createProjectDirectory",
      message: `Directory ${projectDirectory} does not exist. Do you want to create it?`,
      initial: true,
    })) as { createProjectDirectory: boolean };

    if (createProjectDirectory) {
      fs.mkdirSync(projectDirectory, { recursive: true });
      return true;
    } else {
      return false;
    }
  } else {
    if (fs.existsSync(path.join(projectDirectory, POTO_JSON))) {
      const { continueEvenIfProjectAlreadyExists } = (await enquirer.prompt({
        type: "confirm",
        name: "continueEvenIfProjectAlreadyExists",
        message: `Directory ${projectDirectory} already have a ${POTO_JSON}. Continue? (will overwrite the project, but may contains extra files that were previously imported.)`,
        initial: true,
      })) as { continueEvenIfProjectAlreadyExists: boolean };
      return continueEvenIfProjectAlreadyExists;
    } else {
      return true;
    }
  }
};

export type SelectedInputSubDirectoryChoices =
  | "Use Autorun directory"
  | "Use Plan directory"
  | "Use both directory";
export const selectedInputSubDirectoryChoices: SelectedInputSubDirectoryChoices[] =
  ["Use Autorun directory", "Use Plan directory", "Use both directory"];

/**
 * Retrieve all FITS files from the input directory.
 *
 * @param inputDirectory - The directory to scan.
 * @param projectDirectory - The directory of the current project.
 * @returns An array of FileImageSpec objects representing the FITS files.
 */
const getAllFitsInInputDirectory = async (
  inputDirectory: string,
  projectDirectory: string,
): Promise<FileImageSpec[]> => {
  // if ASIAIR used, may stores the lights+flats files in either the Autorun or Plan directories.
  const { isAsiAirDirectory, autorunDirectory, planDirectory } =
    isAsiAirDirectoryF(inputDirectory);

  const logFiles = (files: unknown[]) => {
    logger.info(`Found ${files.length} FITS in input dir ${inputDirectory}.`);
  };

  if (!fs.existsSync(inputDirectory)) {
    logger.errorThrow(`Input directory ${inputDirectory} does not exists.`);
  }

  if (!isAsiAirDirectory) {
    const allFiles = getFitsFromDirectory({
      directory: inputDirectory,
      projectDirectory,
    });
    if (allFiles.length === 0) {
      logger.errorThrow(`No FITS files found in input dir ${inputDirectory}.`);
    }

    logFiles(allFiles);
    return allFiles;
  }

  logger.debug(`ASIAIR dump detected in input dir ${inputDirectory}.`);

  const autorunFiles = fs.existsSync(autorunDirectory)
    ? getFitsFromDirectory({
        directory: autorunDirectory,
        projectDirectory,
      })
    : [];

  const planFiles = fs.existsSync(planDirectory)
    ? getFitsFromDirectory({
        directory: planDirectory,
        projectDirectory,
      })
    : [];

  if (autorunFiles.length === 0 && planFiles.length === 0) {
    logger.errorThrow("No FITS files found in Autorun nor Plan folders.");
  }

  if (autorunFiles.length > 0 && planFiles.length === 0) {
    logFiles(autorunFiles);
    return autorunFiles;
  }

  if (autorunFiles.length === 0 && planFiles.length > 0) {
    logFiles(planFiles);
    return planFiles;
  }

  if (autorunFiles.length > 0 && planFiles.length > 0) {
    const { selectedInputSubDirectory } = (await enquirer.prompt({
      type: "select",
      name: "selectedInputSubDirectory",
      message:
        "Files found in both Autorun and Plan directories. How do we proceed?",
      choices: selectedInputSubDirectoryChoices,
    })) as { selectedInputSubDirectory: SelectedInputSubDirectoryChoices };

    switch (selectedInputSubDirectory) {
      case "Use Autorun directory":
        logFiles(autorunFiles);
        return autorunFiles;
      case "Use Plan directory":
        logFiles(planFiles);
        return planFiles;
      case "Use both directory":
        logFiles([...autorunFiles, ...planFiles]);
        return [...autorunFiles, ...planFiles];
    }
  }
};

/**
 * Naive matching of flats with lights.
 * Sufficient enough to skip the flats that do not have a matching light at all.
 *
 * @param flats - The input files to process. Searching for the flats in this list.
 * @param lights - The lights to match with the flats.
 */
const getFlatsMatchingLightsNaive = ({
  flats,
  lights,
}: {
  flats: FileImageSpec[];
  lights: FileImageSpec[];
}): FileImageSpec[] => {
  const notMatchedFlatsLog: string[] = [];
  const matchingFlats = flats.filter(flat => {
    if (lights.find(light => matchSetFile(light, flat))) {
      return flat;
    } else {
      notMatchedFlatsLog.push(
        `No matching lights for ${flat.setName} (seq ${flat.sequenceId}). The flat sequence won't be used.`,
      );
    }
  });

  const sequences = [...new Set(matchingFlats.map(file => file.sequenceId))];
  for (const skip of [...new Set(notMatchedFlatsLog)]) {
    logger.warning(skip);
  }

  const sequencesLightsCount = [
    ...new Set(matchingFlats.map(file => file.sequenceId)),
  ].length;

  logger.info(
    `Pre-selected ${matchingFlats.length} flats (${sequences.length} sequences) that matches with ${lights.length} lights (${sequencesLightsCount} sequences).`,
  );

  return matchingFlats;
};

/**
 * Reverse of `getFlatsMatchingLightsNaive`.
 * @returns The lights that have not matched with a flat.
 */
const getLightsMatchingLightNaive = ({
  lights,
  flats,
}: {
  lights: FileImageSpec[];
  flats: FileImageSpec[];
}): FileImageSpec[] => {
  const notMatchedLightsLog: string[] = [];
  const notMatchedLights: FileImageSpec[] = [];
  const matchingLights = lights.filter(light => {
    if (flats.find(flat => matchSetFile(light, flat))) {
      return light;
    } else {
      notMatchedLightsLog.push(
        `No matching flats for ${light.setName} (seq ${light.sequenceId}). The light sequence won't be used.`,
      );
      notMatchedLights.push(light);
    }
  });

  const sequences = [...new Set(matchingLights.map(file => file.sequenceId))];
  for (const skip of [...new Set(notMatchedLightsLog)]) {
    logger.warning(skip);
  }

  const sequencesLightsCount = [
    ...new Set(matchingLights.map(file => file.sequenceId)),
  ].length;

  logger.info(
    `Pre-selected ${matchingLights.length} lights (${sequences.length} sequences) that matches with ${flats.length} flats (${sequencesLightsCount} sequences).`,
  );

  return notMatchedLights;
};

/**
 * Process the light and flat matching.
 * If multiple sequences are found for a flat, ask the user to select the flat sequence to use for each light sequence.
 * NOTE. We expect that all flats have a matching light at this point, thanks to the `getFlatsMatchingLightsNaive` function.
 *
 * @param matchingFlats - The flats that have a matching light.
 * @param lights - The lights to match with the flats.
 */
const matchLightsToFlats = async (
  matchingFlats: FileImageSpec[],
  lights: FileImageSpec[],
): Promise<LightsFlatsMatch[]> => {
  const flatSets = [...new Set(matchingFlats.map(file => file.setName))];

  const LightFlatMatches: LightsFlatsMatch[] = [];

  let introManualMatchingDisplayed = false;

  // TODO. Review the archi overall. We should walk light by light instead of flat by flat. This will be easier to discover and setup the project.
  for (const flatSet of flatSets) {
    const flatSetSpecs = getImageSpecFromSetName(flatSet);

    // Search for sequences that are similar.
    const flatSetNameSequenceIds = [
      ...new Set(
        matchingFlats
          .filter(
            flat =>
              // https://pixinsight.com/forum/index.php?threads/can-flats-be-different-iso-than-lights.23686/
              flat.bin === flatSetSpecs.bin &&
              flat.filter === flatSetSpecs.filter,
          )
          .sort((a, b) => a.sequenceId.localeCompare(b.sequenceId))
          .map(flat => `${flat.setName}__${flat.sequenceId}`),
      ),
    ];
    if (flatSetNameSequenceIds.length === 0) {
      throw new Error(`❓❓❓❗️ No sequences found for flat ${flatSet}.`);
    }

    if (flatSetNameSequenceIds.length === 1) {
      // Auto match the flat to the light. Nothing to ask from the user.
      const flatSetNameSequenceId = flatSetNameSequenceIds[0];
      const matchingLights = lights.filter(light =>
        matchSetFile(light, flatSetSpecs),
      );
      const matchingLightSetNameSequenceIds = [
        ...new Set(
          matchingLights.map(light => `${light.setName}__${light.sequenceId}`),
        ),
      ];

      for (const lightSetSequence of matchingLightSetNameSequenceIds) {
        LightFlatMatches.push({
          lightSetName: lightSetSequence.split("__")[0],
          lightSequenceId: lightSetSequence.split("__")[1],

          flatSetName: flatSetNameSequenceId.split("__")[0],
          flatSequenceId: flatSetNameSequenceId.split("__")[1],

          isManualMatch: false,
        });
      }
      continue;
    }

    const lightsConcerned = [
      ...new Set(
        lights
          .filter(light =>
            matchSetFile(light, getImageSpecFromSetName(flatSet)),
          )
          .map(light => ({
            setName: light.setName,
            sequenceId: light.sequenceId,
          }))
          .sort((a, b) => {
            return a.sequenceId.localeCompare(b.sequenceId);
          })
          .map(light => `${light.setName}__${light.sequenceId}`),
      ),
    ];

    const allAlreadyMatched =
      lightsConcerned.filter(lightConcerned =>
        LightFlatMatches.map(
          x => `${x.lightSetName}__${x.lightSequenceId}`,
        ).includes(lightConcerned),
      ).length === lightsConcerned.length;
    // To avoid re-asking for a light sequence that has already been matched.
    if (allAlreadyMatched) {
      continue;
    }

    if (introManualMatchingDisplayed) {
      logger.space();
    }

    logger.info(
      `🤚 Several sequences of flats are compatible with ${
        flatSetSpecs.filter
          ? `${flatSetSpecs.bin} Filter ${flatSetSpecs.filter}`
          : flatSetSpecs.bin
      }:`,
    );
    logger.info(`${flatSetNameSequenceIds.map(x => `- ${x}`).join("\n")}`);

    if (!introManualMatchingDisplayed) {
      logger.debug(
        "We assume that multiple sequences of the same flat kind indicate multiple night sessions where the flats had to be re-shot in between (e.g., a significant date gap between shooting sessions and the lights were not collected with the same collimation and/or same dust in the optical train).",
      );
      logger.debug(
        "We will ask you to tag each concerned light sequence to the right flat sequence (this disclaimer won't be displayed again 🤓).",
      );
      introManualMatchingDisplayed = true;
    }

    for (const lightConcerned of lightsConcerned) {
      if (
        LightFlatMatches.map(
          x => `${x.lightSetName}__${x.lightSequenceId}`,
        ).includes(lightConcerned)
      ) {
        // Skip if the light sequence has already been matched to a flat sequence.
        continue;
      }

      logger.space();

      const lightConcernedSetName = lightConcerned.split("__")[0];
      const lightConcernedSequenceId = lightConcerned.split("__")[1];

      const { selectedFlatSequence } = (await enquirer.prompt({
        type: "select",
        name: "selectedFlatSequence",
        message: formatMessage(
          `${lightConcernedSetName} ${lightConcernedSequenceId} will use`,
        ),
        choices: flatSetNameSequenceIds.map(x => ({
          name: x,
          message: formatMessage(x.replace("__", " ")), // TODO. Print sequence length.
        })),
      })) as { selectedFlatSequence: string };

      if (!selectedFlatSequence) {
        logger.errorThrow("No flat sequence selected.", {
          lightConcernedSetName,
          lightConcernedSequenceId,
          choices: flatSetNameSequenceIds,
        });
      }

      LightFlatMatches.push({
        lightSetName: lightConcernedSetName,
        lightSequenceId: lightConcernedSequenceId,

        flatSetName: selectedFlatSequence.split("__")[0],
        flatSequenceId: selectedFlatSequence.split("__")[1],

        isManualMatch: true,
      });
    }
  }

  logger.space();
  logger.info("👉 Light - Flat matching summary:");
  for (const pair of LightFlatMatches) {
    logger.debug(
      `- ${pair.lightSetName} ${pair.lightSequenceId} 🏹 ${pair.flatSetName} ${pair.flatSequenceId}`,
    );
  }

  return LightFlatMatches;
};

/**
 * Initialize the layer sets with lights and flats matches.
 *
 * @param lightsFlatsMatches - The lights and flats matches.
 * @param allLights - All the lights.
 * @param allFlatsMatchingLights - All the flats matching lights.
 */
const initLayerSetsWithLightsnFlats = (
  lightsFlatsMatches: LightsFlatsMatch[],
  allLights: FileImageSpec[],
  allFlatsMatchingLights: FileImageSpec[],
  projectDirectory: string,
): LayerSet[] => {
  const layerSets: LayerSet[] = [];

  for (const lightsFlatsMatch of lightsFlatsMatches) {
    const lights = lightsFlatsMatch.isManualMatch
      ? allLights.filter(
          light =>
            light.sequenceId === lightsFlatsMatch.lightSequenceId &&
            light.setName === lightsFlatsMatch.lightSetName,
        )
      : allLights.filter(
          light => light.setName === lightsFlatsMatch.lightSetName,
        );
    if (!lights) {
      throw new Error(
        `❓❓❓❗️ Light ${lightsFlatsMatch.lightSetName} ${lightsFlatsMatch.lightSequenceId} not found.`,
      );
    }

    const flats = allFlatsMatchingLights.filter(
      flat => flat.sequenceId === lightsFlatsMatch.flatSequenceId,
    );
    if (!flats) {
      throw new Error(
        `❓❓❓❗️ Flat ${lightsFlatsMatch.lightSetName} ${lightsFlatsMatch.flatSequenceId} not found.`,
      );
    }

    const layerSetId = lightsFlatsMatch.isManualMatch
      ? `${lights[0].setName}__${lights[0].sequenceId}`
      : lights[0].setName;

    const lightSequenceIds = new Set(
      [...lights].map(light => light.sequenceId),
    );
    const lightSequences: LayerSet["lightSequences"] = [];
    for (const lightSequenceId of lightSequenceIds) {
      const lightsOfSequence = lights.filter(
        light => light.sequenceId === lightSequenceId,
      );

      const count = lightsOfSequence.length;
      const integrationMs = lightsOfSequence.reduce(
        (total, light) => total + light.bulbMs,
        0,
      );
      lightSequences.push({
        sequenceId: lightSequenceId,
        count,
        integrationMinutes: integrationMs / 1000 / 60,
      });
    }

    const lightTotalIntegrationMs = lights.reduce(
      (total, light) => total + light.bulbMs,
      0,
    );

    const layerSet = {
      layerSetId,
      filter: lights[0].filter,

      lightSequences,
      lightTotalCount: lights.length,
      lightTotalIntegrationMinutes: lightTotalIntegrationMs / 1000 / 60,
      lights,

      flatSet: flats[0].setName,
      flatSequenceId: flats[0].sequenceId,
      flatsCount: flats.length,
      flats,
    } as LayerSet;

    // Backfill lights `projectFileDirectory` and `projectFilePath`.
    for (const light of lights) {
      light.projectFileDirectory = light.filter
        ? `${projectDirectory}/${light.filter}/${layerSetId}`
        : `${projectDirectory}/${layerSetId}`;
      light.projectFilePath = `${light.projectFileDirectory}/${light.fileName}`;
    }

    layerSets.push(layerSet);
  }
  return layerSets;
};

/**
 * Assign darks and biases to the layer sets.
 * We darks are assigned to lights, and biases are assigned to flats.
 *
 * @param layerSets - The layer sets with lights and flats filled in.
 * @param bankFiles - The bank files available.
 */
const AssignDarksBiasesToLayerSets = async (
  layerSets: LayerSet[],
  bankFiles: FileImageSpec[],
): Promise<LayerSet[]> => {
  const { darkTemperatureTolerance } = (await enquirer.prompt({
    type: "input",
    name: "darkTemperatureTolerance",
    message:
      "Select the temperature tolerance for lights-darks matching (+/-0.5°C to +/-10°C).",
    initial: 3,
    validate: (value: string) => {
      const number = parseFloat(value);
      return number >= 0.2 ? true : "Please enter something >=0.2";
    },
  })) as { darkTemperatureTolerance: number };

  for (const layerSet of layerSets) {
    layerSet.darkSet = "No darks matched";
    layerSet.darksCount = 0;
    layerSet.darkTotalIntegrationMinutes = 0;
    layerSet.darks = [];

    const darksAllTemperature = bankFiles.filter(
      dark => dark.type === "Dark" && matchSetFile(layerSet.lights[0], dark),
    );

    if (darksAllTemperature.length === 0) {
      logger.error(
        `No darks matching light set ${layerSet.lights[0].setName} (regardless of temperature filtering).`,
      );
    } else {
      const darks = darksAllTemperature.filter(dark => {
        const temperatureDiff = Math.abs(
          dark.temperature - layerSet.lights[0].temperature,
        );
        return temperatureDiff <= darkTemperatureTolerance;
      });

      if (darks.length === 0) {
        logger.error(
          `No darks available for ${layerSet.lights[0].setName} with temperature window +-${darkTemperatureTolerance}.`,
        );
        logger.info(
          `There are ${darksAllTemperature.length} darks for ${layerSet.lights[0].setName} if we ignore temperature.`,
        );
      } else {
        layerSet.darkSet = darks[0].setName;
        layerSet.darksCount = darks.length;

        const darkTotalIntegrationMs = darks.reduce(
          (total, dark) => total + dark.bulbMs,
          0,
        );

        layerSet.darkTotalIntegrationMinutes =
          darkTotalIntegrationMs / 1000 / 60;
        layerSet.darks = darks;
      }

      // Warn if there are multiple sequences for the same layer set
      const darkSequences = new Set(darks.map(dark => dark.sequenceId));
      if (darkSequences.size > 1) {
        logger.warning(
          `Multiple dark sequences found for ${layerSet.lights[0].setName}: ${[
            ...darkSequences,
          ].join(", ")}`,
        );
        logger.warning(
          "Gathering them all for the master dark. Make sure that's what you wanted.",
        );
      }
    }

    layerSet.biasSet = "No biases matched";
    layerSet.biasesCount = 0;
    layerSet.biases = [];

    const biases = bankFiles.filter(
      bias => bias.type === "Bias" && matchSetFile(layerSet.flats[0], bias),
    );

    if (biases.length === 0) {
      logger.error(`No biases found for ${layerSet.flats[0].setName}.`);
    } else {
      layerSet.biasSet = biases[0].setName;
      layerSet.biasesCount = biases.length;
      layerSet.biases = biases;
    }

    const biasSequences = new Set(biases.map(bias => bias.sequenceId));
    if (biasSequences.size > 1) {
      logger.warning(
        `Multiple bias sequences found for ${layerSet.flats[0].setName}: ${[
          ...biasSequences,
        ].join(", ")}`,
      );
      logger.warning(
        "Gathering them all for the master bias. Make sure that's what you wanted.",
      );
    }
  }

  logger.space();
  logger.info("👉 Light - Dark matching summary:");
  for (const layerSet of layerSets) {
    for (const lightSequence of layerSet.lightSequences) {
      if (layerSet.layerSetId.includes("__20")) {
        logger.debug(
          `- ${layerSet.layerSetId} 🏹 ${layerSet.darkSet} (${layerSet.darksCount} files / ${layerSet.darkTotalIntegrationMinutes} minutes)`,
        );
      } else {
        logger.debug(
          `- ${layerSet.layerSetId} ${lightSequence.sequenceId} 🏹 ${layerSet.darkSet} (${layerSet.darksCount} files / ${layerSet.darkTotalIntegrationMinutes} minutes)`,
        );
      }
    }
  }

  logger.info("👉 Flat - Bias matching summary:");
  for (const layerSet of layerSets) {
    logger.debug(
      `- ${layerSet.flatSet} ${layerSet.flatSequenceId} 🏹 ${layerSet.biasSet} (${layerSet.biasesCount} files)`,
    );
  }

  return layerSets;
};

/**
 * Preview the project composition before dispatching.
 *
 * @param layerSets - The layer sets to preview.
 */
const previewBeforeDispatching = async (
  layerSets: LayerSet[],
): Promise<{
  go: boolean;
  metrics: PotoProject["metrics"];
}> => {
  const metrics: PotoProject["metrics"] = {
    cumulatedLightIntegrationMinutes: layerSets.reduce(
      (total, layerSet) => total + layerSet.lightTotalIntegrationMinutes,
      0,
    ),
    cumulatedDarksIntegrationMinutes: layerSets.reduce(
      (total, layerSet) => total + layerSet.darkTotalIntegrationMinutes,
      0,
    ),
    totalLights: layerSets.reduce(
      (total, layerSet) => total + layerSet.lightTotalCount,
      0,
    ),
    totalDarks: layerSets.reduce(
      (total, layerSet) => total + layerSet.darksCount,
      0,
    ),
    totalFlats: layerSets.reduce(
      (total, layerSet) => total + layerSet.flatsCount,
      0,
    ),
    totalBiases: layerSets.reduce(
      (total, layerSet) => total + layerSet.biasesCount,
      0,
    ),
  };

  logger.info(
    `🔭 Cumulated light integration: ${metrics.cumulatedLightIntegrationMinutes} minutes.`,
  );
  logger.info(
    `📦 Project size: ${metrics.totalLights} lights, ${metrics.totalDarks} darks, ${metrics.totalFlats} flats, ${metrics.totalBiases} biases.`,
  );

  logger.space();

  logger.info("🌌 Layer sets:");
  for (const layerSet of layerSets) {
    const log = `- ${layerSet.layerSetId} has ${layerSet.lights.length} lights, ${layerSet.darks.length} darks, ${layerSet.flats.length} flats, ${layerSet.biases.length} biases.`;
    if (
      layerSet.lights.length > 0 &&
      layerSet.flats.length > 0 &&
      layerSet.darks.length > 0 &&
      layerSet.biases.length > 0
    ) {
      logger.debug(log);
    } else {
      logger.warning(log);
    }
  }

  logger.space();

  const { go } = (await enquirer.prompt({
    type: "confirm",
    name: "go",
    message: "Do you want to proceed with the dispatch?",
    initial: true,
  })) as { go: boolean };

  return { go, metrics };
};

/**
 * Dispatch the project json and the files to the poto project directory.
 *
 * @param potoProject - The POTO project to prepare.
 * @param projectDirectory - The project directory.
 */
const dispatchProject = (
  potoProject: PotoProject,
  projectDirectory: string,
): void => {
  const potoJsonPath = `${projectDirectory}/${POTO_JSON}`;

  fs.writeFileSync(potoJsonPath, JSON.stringify(potoProject, null, 2));

  let alreadyImported: FileImageSpec[] = [];

  for (const layerSet of potoProject.layerSets) {
    for (const file of [
      ...layerSet.lights,
      ...layerSet.darks,
      ...layerSet.flats,
      ...layerSet.biases,
    ]) {
      alreadyImported = copyFileToProject(file, alreadyImported);
    }
  }
};
