import * as fs from "node:fs/promises";
import { CREDENTIALS_DIR } from "./credentials";
import path from "node:path";
import ora from "ora";

export const checkNodeVersion = async () => {
  const spinner = ora("Checking node version...").start();
  const [major, minor] = process.versions.node.split(".").map(Number);

  let packageJson;
  try {
    packageJson = JSON.parse(
      await fs.readFile(
        path.join(import.meta.dirname, "../package.json"),
        "utf-8",
      ),
    );
  } catch (e) {
    throw new Error("Failed to read package.json of core-sdk");
  }

  const {
    engines: { node },
  } = packageJson;

  const [minMajor, minMinor] = node
    .split(".")
    .map((v: string) => v.replace(/[^\d]/g, ""))
    .map(Number);

  if (major < minMajor || (major === minMajor && minor < minMinor)) {
    spinner.fail(
      `Node version ${minMajor}.${minMinor} or higher is required. You are running ${major}.${minor}.`,
    );

    process.exit(1);
  } else {
    spinner.stop();
    return true;
  }
};

export const shouldSkipModelCheck = () => process.argv.includes("--skip-model-check");

/**
 *  Get the value of a process argument by key
 *  Example: getArgumentByKey("--email") or getArgumentByKey(["--email", "-e"])
 * @param key  The key to search for in the process arguments
 * @returns
 */
export const getArgumentByKey = (key: string | string[]) => {
  if (Array.isArray(key)) {
    for (const k of key) {
      if (process.argv.includes(k)) {
        const index = process.argv.indexOf(k);
        return index !== -1 ? process.argv[index + 1] : undefined;
      }
    }

    return undefined;
  }

  const index = process.argv.indexOf(key);
  return index !== -1 ? process.argv[index + 1] : undefined;
};

export const SUCCESS_FLAG_FILE = `${CREDENTIALS_DIR}/success`;
/**
 *  Store a flag in the credentials directory to indicate a successful build
 *  This is used to determine if the build was successful or not
 */
export const storeBuildSuccessFlag = async () => {
  try {
    await fs.access(CREDENTIALS_DIR);
  } catch (_e) {
    await fs.mkdir(CREDENTIALS_DIR);
  }

  await fs.writeFile(SUCCESS_FLAG_FILE, "true");
};

/**
 * Remove the success flag from the credentials directory
 */
export const removeBuildSuccessFlag = async () => {
  try {
    await fs.unlink(SUCCESS_FLAG_FILE);
  } catch (_e) {}
};

/**
 * Check if the build was successful
 */
export const checkBuildSuccess = async () => {
  try {
    await fs.access(SUCCESS_FLAG_FILE);
    return true;
  } catch (_e) {
    return false;
  }
};

const getPackageVersion = async (packageName: string) => {
  const packageJsonPath = path.join(
    process.cwd(),
    "node_modules",
    packageName,
    "package.json",
  );

  let packageJson;
  try {
    packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
    return packageJson.version;
  } catch (e) {
    return undefined;
  }
};

/**
 * Attempts to resolve a local file reference to get the actual package version
 * @param packageName The name of the package
 * @param filePath The file path reference (e.g. "file:../packages/core")
 * @returns The resolved version or "local-dev" if not found
 */
const resolveLocalFileVersion = async (
  packageName: string,
  filePath: string,
) => {
  try {
    // Remove the file: prefix and resolve the path
    const refPath = filePath.replace(/^file:/, "");
    const absPath = path.resolve(process.cwd(), refPath, "package.json");

    // Read the package.json from the referenced path
    const refPackageJson = JSON.parse(await fs.readFile(absPath, "utf-8"));
    return refPackageJson.version || "local-dev";
  } catch (e) {
    console.warn(`Failed to resolve local version for ${packageName}`, e);
    return "local-dev";
  }
};

export const getSDKVersions = async () => {
  const packageNames = [
    "@embeddable.com/core",
    "@embeddable.com/react",
    "@embeddable.com/sdk-core",
    "@embeddable.com/sdk-react",
    "@embeddable.com/sdk-utils",
  ];

  // First try to get versions from node_modules
  const sdkVersions = await packageNames.reduce<
    Promise<Record<string, string>>
  >(
    async (accPromise, packageName) => {
      const acc = await accPromise; // Wait for the previous accumulator to resolve
      const version = await getPackageVersion(packageName);
      if (version) {
        acc[packageName] = version;
      }
      return acc;
    },
    Promise.resolve({}), // Start with a resolved promise containing an empty object
  );

  // If no versions were found, try to get them from package.json dependencies/devDependencies
  if (
    Object.keys(sdkVersions).length === 0 &&
    process.env.NODE_ENV !== "test"
  ) {
    try {
      const packageJsonPath = path.join(process.cwd(), "package.json");
      const packageJson = JSON.parse(
        await fs.readFile(packageJsonPath, "utf-8"),
      );

      const { dependencies = {}, devDependencies = {} } = packageJson;
      const allDeps = { ...dependencies, ...devDependencies };

      for (const packageName of packageNames) {
        if (allDeps[packageName]) {
          // For file: references, try to get the actual version from the referenced package
          if (allDeps[packageName].startsWith("file:")) {
            sdkVersions[packageName] = await resolveLocalFileVersion(
              packageName,
              allDeps[packageName],
            );
          } else {
            sdkVersions[packageName] = allDeps[packageName];
          }
        }
      }
    } catch (e) {
      console.warn("Failed to read package.json for SDK versions", e);
    }
  }

  // If we're in a test environment and still have no versions, add fallback values
  if (Object.keys(sdkVersions).length === 0) {
    const isTestEnv = process.cwd().includes("test-sdk");
    if (isTestEnv) {
      console.warn("Test environment detected, using fallback SDK versions");
      packageNames.forEach((pkg) => {
        sdkVersions[pkg] = "local-dev";
      });
    }
  }

  return sdkVersions;
};

export const hrtimeToISO8601 = (
  hrtime: number[] | null | undefined,
): String => {
  if (hrtime === null || hrtime === undefined) {
    return "";
  }
  const seconds = hrtime[0];
  const nanoseconds = hrtime[1];

  // Convert time components
  const totalSeconds = seconds + nanoseconds / 1e9;
  const minutes = Math.floor(totalSeconds / 60);
  const remainingSeconds = totalSeconds % 60;

  // Format ISO 8601 duration without hours
  return `PT${minutes > 0 ? minutes + "M" : ""}${remainingSeconds.toFixed(3)}S`;
};
