const beeline = require("honeycomb-beeline")({
  writeKey: process.env["HONEYCOMB_WRITE_KEY"],
  dataset:
    process.env["NODE_ENV"] === "production"
      ? "Dress to Impress (2020)"
      : "Dress to Impress (2020, dev)",
  serviceName: "impress-2020-gql-server",
});
import connectToDb from "../src/server/db";
import buildLoaders from "../src/server/loaders";
import {
  loadCustomPetData,
  loadNCMallPreviewImageHash,
} from "../src/server/load-pet-data";
import { gql, loadGraphqlQuery } from "../src/server/ssr-graphql";
import { saveModelingData } from "../src/server/modeling";

async function main() {
  const db = await connectToDb();
  const loaders = buildLoaders(db);
  const context = { db, ...loaders };

  const { data, errors } = await loadGraphqlQuery({
    query: gql`
      query ScriptModelNeededItems_GetNeededItems {
        standardItems: itemsThatNeedModels {
          id
          name
          speciesThatNeedModels {
            id
            name
            withColor(colorId: "8") {
              neopetsImageHash
            }
          }
        }

        babyItems: itemsThatNeedModels(colorId: "6") {
          id
          name
          speciesThatNeedModels(colorId: "6") {
            id
            name
            withColor(colorId: "6") {
              neopetsImageHash
            }
          }
        }

        maraquanItems: itemsThatNeedModels(colorId: "44") {
          id
          name
          speciesThatNeedModels(colorId: "44") {
            id
            name
            withColor(colorId: "44") {
              neopetsImageHash
            }
          }
        }

        mutantItems: itemsThatNeedModels(colorId: "46") {
          id
          name
          speciesThatNeedModels(colorId: "46") {
            id
            name
            withColor(colorId: "46") {
              neopetsImageHash
            }
          }
        }
      }
    `,
  });

  if (errors) {
    console.error(`Couldn't load items that need modeling:`);
    for (const error of errors) {
      console.error(error);
    }
    return 1;
  }

  await modelItems(data.standardItems, context);
  await modelItems(data.babyItems, context);
  await modelItems(data.maraquanItems, context);
  await modelItems(data.mutantItems, context);
}

async function modelItems(items, context) {
  for (const item of items) {
    for (const species of item.speciesThatNeedModels) {
      let imageHash;
      try {
        imageHash = await modelItem(item, species, context);
      } catch (error) {
        console.error(
          `❌ [${item.name} (${item.id}) on ${species.name} (${species.id}))] ` +
            `Modeling failed, skipping:\n`,
          error
        );
        continue;
      }
      console.info(
        `✅ [${item.name} (${item.id}) on ${species.name} (${species.id}))] ` +
          `Modeling data saved! Hash: ${imageHash}`
      );
    }
  }
}

async function modelItem(item, species, context) {
  // First, use the NC Mall try-on feature to get the image hash for this
  // species wearing this item.
  const imageHash = await loadImageHash(item, species);

  // Next, load the detailed customization data, using the special feature
  // where "@imageHash" can be looked up as if it were a pet name.
  const petName = "@" + imageHash;
  const customPetData = await loadCustomPetData(petName);

  // We don't have real pet metadata, but that's okay, that's only relevant for
  // tagging pet appearances, and that's not what we're here to do, so the
  // modeling function will skip that step. (But we do provide the pet "name"
  // to save in our modeling logs!)
  const petMetaData = { name: petName, mood: null, gender: null };

  // Check whether we actually *got* modeling data back. It's possible this
  // item just isn't compatible with this species! (In this case, it would be
  // wise for someone to manually set the `modeling_status_hint` field on this
  // item, so we skip it in the future!)
  //
  // NOTE: It seems like sometimes customPetData.object_asset_registry is
  // an object keyed by asset ID, and sometimes it's an array? Uhhh hm. Well,
  // Object.values does what we want in both cases!
  const itemAssets = Object.values(customPetData.object_asset_registry);
  const hasAssetsForThisItem = itemAssets.some(
    (a) => String(a.obj_info_id) === item.id
  );
  if (!hasAssetsForThisItem) {
    throw new Error(`custom pet data did not have assets for item ${item.id}`);
  }

  // Finally, model this data into the database!
  await saveModelingData(customPetData, petMetaData, context);

  return imageHash;
}

async function loadImageHash(item, species) {
  const basicImageHash = species.withColor.neopetsImageHash;
  try {
    return await loadWithRetries(
      () => loadNCMallPreviewImageHash(basicImageHash, [item.id]),
      {
        numAttempts: 3,
        delay: 5000,
        contextString: `${item.name} (${item.id}) on ${species.name} (${species.id}))`,
      }
    );
  } catch (error) {
    console.error(
      `[${item.name} (${item.id}) on ${species.name} (${species.id}))] ` +
        `Loading failed too many times, giving up`
    );
    throw error;
  }
}

async function loadWithRetries(fn, { numAttempts, delay, contextString }) {
  if (numAttempts <= 0) {
    return;
  }

  try {
    return await fn();
  } catch (error) {
    console.error(
      `[${contextString}] Error loading, will retry in ${delay}ms:\n`,
      error
    );
    await new Promise((resolve) => setTimeout(() => resolve(), delay));
    return await loadWithRetries(fn, {
      numAttempts: numAttempts - 1,
      delay: delay * 2,
      contextString,
    });
  }
}

async function mainWithBeeline() {
  const trace = beeline.startTrace({
    name: "scripts/model-needed-items",
    operation_name: "scripts/model-needed-items",
  });

  try {
    await main();
  } finally {
    beeline.finishTrace(trace);
  }
}

mainWithBeeline()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .then((code = 0) => process.exit(code));