/**
 * Copyright (c) 2026 Huawei Technologies Co., Ltd.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import { Command } from './types';
import fs from 'node:fs';
import os from 'os';
import fse from 'fs-extra';
import {runBuild, loadConfig, AssetData, RunBuildOptions as BuildOptions} from 'metro';
import pathUtils from 'path';
import { copyAssets } from '../assetResolver';
import { execSync } from 'child_process';
import { Logger } from '../io';
import { DescriptiveError } from '../core';
import { randomUUID } from 'node:crypto';

type MetroConfig = Awaited<ReturnType<typeof loadConfig>>;

const ENABLE_DEBUG_LOG = process.env.RNOH_BUNDLE_DEBUG === 'true';

function debugLog(message: string) {
  if (ENABLE_DEBUG_LOG) {
    const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 23);
    console.log(`[${timestamp}] ${message}`);
  }
}

const HERMESC_BIN_PATH = {
  darwin: pathUtils.normalize('osx-bin/hermesc'),
  linux: pathUtils.normalize('linux64-bin/hermesc'),
  win32: pathUtils.normalize('win64-bin/hermesc.exe'),
};
const isHermesV1Enabled = process.env.HERMES_V1_ENABLED?.toLowerCase() === 'true';
const HERMESC_PATH_PREFIX = pathUtils.normalize(
  isHermesV1Enabled ? './node_modules/hermes-compiler/hermesc' : './node_modules/react-native/sdks/hermesc/'
);
const HARMONY_RESOURCE_PATH = pathUtils.normalize(
  './harmony/entry/src/main/resources/rawfile'
);
const ASSETS_DEFAULT_DEST_PATH = pathUtils.normalize(
  './harmony/entry/src/main/resources/rawfile/assets'
);

// TODO: adjust if https://github.com/facebook/metro/pull/1621 is merged
type Bundle = {
  code: string;
  map: string;
}
type RunBuildResult = Bundle & {
  assets?: ReadonlyArray<AssetData>;
}
type RunBuildOptions = BuildOptions & {assets?: boolean};
type Path = string;
type OSType = 'darwin' | 'linux' | 'win32';

export const commandBundleHarmony: Command = {
  name: 'bundle-harmony',
  description:
    'Creates JS bundle, creates a special cpp header containing the JS code, copies assets directory to the project.',
  options: [
    {
      name: '--dev [boolean]',
      description: 'If false, warnings are disabled and the bundle is minified',
      parse: (val: string) => val !== 'false',
      default: true,
    },
    {
      name: '--entry-file <path>',
      description:
        'Path to the root JS file, either absolute or relative to JS root',
      default: 'index.js',
      parse: (val: string) => pathUtils.normalize(val),
    },
    {
      name: '--reset-cache',
      description: 'Removes cached files',
      default: false,
    },
    {
      name: '--config <path>',
      description: 'Path to the Metro configuration file',
      parse: (val: string) => pathUtils.normalize(val),
    },
    {
      name: '--bundle-output <path>',
      description: `File path where to store the resulting bundle (default: "${HARMONY_RESOURCE_PATH}${pathUtils.sep}bundle.harmony.js")`,
      parse: (val: string) => pathUtils.normalize(val),
    },
    {
      name: '--js-engine <string>',
      description:
        'JavaScript engine used to run the bundle. Supported engines: "hermes" and "any" (default: "any"). Setting this option to "hermes" will generate a HBC bundle instead of a JS bundle.',
      default: 'any',
      parse: (val: string) => {
        if (val !== 'any' && val !== 'hermes') {
          throw new Error(
            `Only "any" and "hermes" are supported as JavaScript engines, but "${val}" was provided`
          );
        }
        return val;
      },
    },
    {
      name: '--assets-dest <path>',
      description:
        'Directory name where to store assets referenced in the bundle',
      default: ASSETS_DEFAULT_DEST_PATH,
      parse: (val: string) => pathUtils.normalize(val),
    },
    {
      name: '--sourcemap-output <path>',
      description:
        'File name where to store the resulting source map, ex. /tmp/groups.map',
      parse: (val: string) => pathUtils.normalize(val),
    },
    {
      name: '--minify [boolean]',
      description: 'Allows overriding whether bundle is minified',
      parse: (val: string) => val !== 'false',
    },
    {
      name: '--hermesc-dir <path>',
      description:
        'Path to hermesc directory. Relevant when --js-engine set to "hermes".',
      default: HERMESC_PATH_PREFIX,
      parse: (val: string) => pathUtils.normalize(val),
    },
    {
      name: '--hermesc-options <string...>',
      description:
        'Additional options to pass to hermesc when generating HBC bundle. Example: --hermesc-options O g reuse-prop-cache. Relevant when --js-engine set to "hermes".',
      parse: (val, prev) => (prev ? [...prev, val] : [val]),
    },
  ],
  func: async (argv, config, args: any) => {
    const logger = new Logger();
    try {
      const defaultBundleName =
        args.jsEngine === 'hermes' ? 'hermes_bundle.hbc' : 'bundle.harmony.js';
      const bundleOutput: string =
        args.bundleOutput ??
        `${HARMONY_RESOURCE_PATH}${pathUtils.sep}${defaultBundleName}`;
      const shouldGenerateHbcBundle = bundleOutput
        .toLowerCase()
        .endsWith('hbc');
      await fse.ensureDir(pathUtils.dirname(bundleOutput));
      const assetsDest: Path = args.assetsDest;
      await fse.ensureDir(assetsDest);
      const buildOptions: RunBuildOptions = {
        assets: true,
        entry: args.entryFile,
        platform: 'harmony',
        minify: args.minify !== undefined ? args.minify : !args.dev,
        dev: args.dev,
        sourceMap: args.sourcemapOutput,
        sourceMapUrl: args.sourcemapOutput,
      };
      const metroConfig = await loadMetroConfig({
        config: args.config,
        resetCache: args.resetCache,
      });

      const {assets, ...bundle}= await createBundle(metroConfig, buildOptions);

      if (shouldGenerateHbcBundle) {
        debugLog('Starting Hermes bytecode compilation...');
        await saveHbcBundle(
          logger,
          bundle.code,
          bundleOutput,
          args.hermescDir,
          args.hermescOptions
        );
        debugLog('Hermes bytecode compilation finished');
      } else {
        await saveJSBundle(logger, bundle, bundleOutput);
      }
      maybeSaveSourceMap(logger, bundle, args.sourcemapOutput);
      if(assets){
        await copyAssets(logger, assets, assetsDest);
        debugLog('Assets copy completed, cleaning up...');
      }
      debugLog('Bundle process finished, exiting');
    } catch (err) {
      if (err instanceof DescriptiveError) {
        logger.descriptiveError(err);
        return;
      } else {
        throw err;
      }
    }
  },
};

async function loadMetroConfig(options: {
  config: Path | undefined;
  resetCache: boolean;
}): Promise<MetroConfig> {
  if (!options.config) {
    delete options.config;
  }
  return await loadConfig(options);
}

async function createBundle(
  metroConfig: MetroConfig,
  buildOptions: RunBuildOptions
) {
  // casting needed as Metro.runBuild returns Promise<{code: string, map: string}>
  // despite being typed as Promise<void>
  // TODO: adjust if https://github.com/facebook/metro/pull/1621 is merged
  return await runBuild(metroConfig, buildOptions) as unknown as RunBuildResult;
}

async function saveHbcBundle(
  logger: Logger,
  bundleCode: string,
  bundleOutput: Path,
  hermescPath: string,
  hermescOptions: string[] | undefined
) {
  if (
    process.platform !== 'darwin' &&
    process.platform !== 'linux' &&
    process.platform !== 'win32'
  ) {
    throw new Error(`Unsupported platform ${process.platform} (⊙︿⊙)`);
  }
  const resolvedHermescPath = pathUtils.resolve(
    hermescPath,
    HERMESC_BIN_PATH[process.platform as OSType]
  );

  if (!fs.existsSync(resolvedHermescPath)) {
    throw new DescriptiveError({
      whatHappened: `Couldn't find hermesc at ${resolvedHermescPath}`,
      whatCanUserDo: [
        'Find hermesc dir and provide it by using --hermesc-dir argument. hermesc dir should be in react-native package.',
      ],
    });
  }

  const tmpBundlePath = `${os.tmpdir()}${pathUtils.sep}b-${randomUUID()}.harmony.js`;
  debugLog('Writing temporary JS bundle...');
  await fse.writeFile(tmpBundlePath, bundleCode);
  const hbcFilePath = bundleOutput.endsWith('.hbc')
    ? bundleOutput
    : pathUtils.join(pathUtils.dirname(bundleOutput), 'hermes_bundle.hbc');
  const hermescOptionsString =
    hermescOptions?.map((e) => `-${e}`).join(' ') || '';
  debugLog(`Executing hermesc compiler: ${resolvedHermescPath}`);
  execSync(
    `${resolvedHermescPath} ${hermescOptionsString} --emit-binary -out ${hbcFilePath} ${tmpBundlePath}`,
    {
      stdio: 'inherit',
    }
  );
  debugLog('Hermesc execution completed, cleaning up temp file...');
  fs.unlinkSync(tmpBundlePath);
  debugLog('Temp file deleted');
  logger.info((s) => `Created ${hbcFilePath}`);
}

async function saveJSBundle(
  logger: Logger,
  bundle: Bundle,
  bundleOutput: Path
) {
  fs.writeFileSync(bundleOutput, bundle.code);
  logger.info((s) => `Created ${bundleOutput}`);
}

function maybeSaveSourceMap(
  logger: Logger,
  bundle: Bundle,
  sourceMapOutput: Path | undefined
) {
  if (sourceMapOutput) {
    fs.writeFileSync(sourceMapOutput, bundle.map);
    logger.info((s) => `Created ${sourceMapOutput}`);
  }
}