import * as fs from "node:fs/promises";
import * as fsSync from "node:fs";
import archiver from "archiver";
import axios from "axios";
import ora, { Ora } from "ora";
import { initLogger, logError } from "./logger";
import * as path from "path";

import provideConfig from "./provideConfig";
// @ts-ignore
import reportErrorToRollbar from "./rollbar.mjs";

import { findFiles } from "@embeddable.com/sdk-utils";
import { getToken } from "./login";
import { checkBuildSuccess, checkNodeVersion, getArgumentByKey } from "./utils";
import { selectWorkspace } from "./workspaceUtils";
import { ResolvedEmbeddableConfig } from "./defineConfig";

// grab cube files
export const CUBE_FILES = /^(.*)\.cube\.(ya?ml|js)$/;

export const CLIENT_CONTEXT_FILES = /^(.*)\.cc\.ya?ml$/;
export const SECURITY_CONTEXT_FILES = /^(.*)\.sc\.ya?ml$/;

export default async () => {
  await initLogger("push");
  const breadcrumbs: string[] = [];
  let spinnerPushing;

  try {
    checkNodeVersion();
    breadcrumbs.push("checkNodeVersion");
    const isBuildSuccess = await checkBuildSuccess();
    const config = await provideConfig();

    if (!isBuildSuccess && config.pushComponents) {
      console.error(
        "Build failed or not completed. Please run `embeddable:build` first.",
      );
      process.exit(1);
    }

    if (process.argv.includes("--api-key") || process.argv.includes("-k")) {
      spinnerPushing = ora("Using API key...").start();
      breadcrumbs.push("push by api key");
      try {
        await pushByApiKey(config, spinnerPushing);
      } catch (error: any) {
        if (error.response?.data?.errorCode === "BUILDER-998") {
          spinnerPushing.fail(
            `Authentication failure. Server responded with: "${error.response?.data?.errorMessage}". Ensure that your API key is valid for the region specified in the embeddable.config.ts|js file.
You are trying to push to the following app url: ${config.previewBaseUrl}
Read more about deployment regions at https://docs.embeddable.com/deployment/deployment-regions`,
          );
          process.exit(1);
        }
        spinnerPushing.fail("Publishing failed");
        console.log(error.response?.data || error);
        process.exit(1);
      }
      publishedSectionFeedback(config, spinnerPushing);
      spinnerPushing.succeed("Published using API key");

      return;
    }

    breadcrumbs.push("push by standard login");
    const token = await verify(config);
    spinnerPushing = ora()
      .start()
      .info("No API Key provided. Standard login will be used.");

    breadcrumbs.push("select workspace");
    const { workspaceId, name: workspaceName } = await selectWorkspace(
      ora,
      config,
      token,
    );

    const workspacePreviewUrl = `${config.previewBaseUrl}/workspace/${workspaceId}`;
    const message = getArgumentByKey(["--message", "-m"]);

    breadcrumbs.push("build archive");
    await buildArchive(config);
    spinnerPushing.info(
      `Publishing to ${workspaceName} using ${workspacePreviewUrl}...`,
    );

    breadcrumbs.push("send build");
    await sendBuild(config, { workspaceId, token, message });

    publishedSectionFeedback(config, spinnerPushing);
    spinnerPushing.succeed(
      `Published to ${workspaceName} using ${workspacePreviewUrl}`,
    );
  } catch (error: any) {
    spinnerPushing?.fail("Publishing failed");
    await logError({ command: "push", breadcrumbs, error });
    await reportErrorToRollbar(error);
    console.log(error.response?.data || error);
    process.exit(1);
  }
};

const publishedSectionFeedback = (
  config: ResolvedEmbeddableConfig,
  spinnerPushing: Ora,
) => {
  config.pushModels && spinnerPushing.succeed("Models published");
  config.pushComponents && spinnerPushing.succeed("Components published");
};

async function pushByApiKey(config: ResolvedEmbeddableConfig, spinner: any) {
  const apiKey = getArgumentByKey(["--api-key", "-k"]);

  if (!apiKey) {
    spinner.fail("No API key provided");
    process.exit(1);
  }

  const email = getArgumentByKey(["--email", "-e"]);

  if (!email || !/\S+@\S+\.\S+/.test(email)) {
    spinner.fail(
      "Invalid email provided. Please provide a valid email using --email (-e) flag",
    );
    process.exit(1);
  }

  // message is optional
  const message = getArgumentByKey(["--message", "-m"]);

  await buildArchive(config);

  return sendBuildByApiKey(config, {
    apiKey,
    email,
    message,
  });
}

async function verify(ctx: ResolvedEmbeddableConfig) {
  if (ctx.pushComponents) {
    try {
      await fs.access(ctx.client.buildDir);
    } catch (_e) {
      console.error("No embeddable build was produced.");
      process.exit(1);
    }
  }

  // TODO: initiate login if no/invalid token.
  const token = await getToken();

  if (!token) {
    console.error("Expired token. Please login again.");
    process.exit(1);
  }

  return token;
}

export async function buildArchive(config: ResolvedEmbeddableConfig) {
  const spinnerArchive = ora("Building...").start();

  if (!config.pushModels && !config.pushComponents) {
    spinnerArchive.fail(
      "Cannot push: both pushModels and pushComponents are disabled",
    );
    process.exit(1);
  }

  const filesList: [string, string][] = [];

  if (config.pushModels) {
    const cubeFilesList = await findFiles(
      config.client.modelsSrc || config.client.srcDir,
      CUBE_FILES,
    );

    const securityContextFilesList = await findFiles(
      config.client.presetsSrc || config.client.srcDir,
      SECURITY_CONTEXT_FILES,
    );

    filesList.push(
      ...cubeFilesList.map((entry): [string, string] => [
        path.basename(entry[1]),
        entry[1],
      ]),

      ...securityContextFilesList.map((entry): [string, string] => [
        path.basename(entry[1]),
        entry[1],
      ]),
    );
  }

  if (config.pushComponents) {
    const clientContextFilesList = await findFiles(
      config.client.presetsSrc || config.client.srcDir,
      CLIENT_CONTEXT_FILES,
    );

    filesList.push(
      ...clientContextFilesList.map((entry): [string, string] => [
        path.basename(entry[1]),
        entry[1],
      ]),
    );
  }

  await archive({
    ctx: config,
    filesList,
    isDev: false,
  });
  return spinnerArchive.succeed("Bundling completed");
}

export async function archive(args: {
  ctx: ResolvedEmbeddableConfig;
  filesList: [string, string][];
  isDev: boolean;
}) {
  const { ctx, filesList, isDev } = args;
  const output = fsSync.createWriteStream(ctx.client.archiveFile);

  const archive = archiver.create("zip", {
    zlib: { level: 9 },
  });

  archive.pipe(output);
  if (!isDev) {
    archive.directory(ctx.client.buildDir, false);
    archive.file(ctx.client.globalCss, {
      name: "global.css",
    });
  }

  for (const fileData of filesList) {
    archive.file(fileData[1], {
      name: fileData[0],
    });
  }

  await archive.finalize();

  return new Promise<void>((resolve: any, _reject) => {
    output.on("close", () => resolve());
  });
}

export async function createFormData(
  filePath: string,
  metadata: Record<string, any>,
) {
  const { FormData, Blob } = await import("formdata-node");
  const { fileFromPath } = await import("formdata-node/file-from-path");

  const file = await fileFromPath(filePath, "embeddable-build.zip");
  const form = new FormData();
  form.set("file", file, "embeddable-build.zip");

  const metadataBlob = new Blob([JSON.stringify(metadata)], {
    type: "application/json",
  });
  form.set("request", metadataBlob, "request.json");

  return form;
}

export async function sendBuildByApiKey(
  ctx: ResolvedEmbeddableConfig,
  {
    apiKey,
    email,
    message,
  }: { apiKey: string; email: string; message?: string },
) {
  const form = await createFormData(ctx.client.archiveFile, {
    pushModels: ctx.pushModels,
    pushComponents: ctx.pushComponents,
    authorEmail: email,
    description: message,
  });

  const response = await uploadFile(
    form,
    `${ctx.pushBaseUrl}/api/v1/bundle/upload`,
    apiKey,
  );
  await fs.rm(ctx.client.archiveFile);

  return { ...response.data, message };
}

export async function sendBuild(
  ctx: ResolvedEmbeddableConfig,
  {
    workspaceId,
    token,
    message,
  }: { workspaceId: string; token: string; message?: string },
) {
  const form = await createFormData(ctx.client.archiveFile, {
    pushModels: ctx.pushModels,
    pushComponents: ctx.pushComponents,
    authorEmail: "",
    description: message,
  });

  await uploadFile(
    form,
    `${ctx.pushBaseUrl}/bundle/${workspaceId}/upload`,
    token,
  );

  await fs.rm(ctx.client.archiveFile);
}

async function uploadFile(formData: any, url: string, token: string) {
  return axios.post(url, formData, {
    headers: {
      "Content-Type": "multipart/form-data",
      Authorization: `Bearer ${token}`,
    },
    maxContentLength: Infinity,
    maxBodyLength: Infinity,
  });
}
