/**
 * Copyright (c) 2025 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 {
  AbsolutePath,
  DescriptiveError,
  maybeMakeDirectories,
  maybeRemoveFilesInDirectory,
} from '../core';
import { Logger, findFilePathsWithExtensions } from '../io';
import fs from 'node:fs';
import {
  ArkTSComponentCodeGeneratorCAPI,
  CppComponentCodeGenerator,
  NativeModuleCodeGenerator,
  UberSchema,
  SharedComponentCodeGenerator,
} from '../codegen';
import Case from 'case';
import { LibGlueCodeGenerator } from '../codegen/generators/LibGlueCodeGenerator';

const COMMAND_NAME = 'codegen-lib-harmony';

export const commandCodegenLibHarmony: Command = {
  name: COMMAND_NAME,
  description:
    'Generates boilerplate, types, and utilities from Turbo Module and Fabric Component specifications. Designed for library developers who want to include generated code in their libraries.',
  options: [
    {
      name: '--npm-package-name <string>',
      description:
        'Name of your React Native library. This value is processed and used as a directory name for C++ files to reduce the chances of conflicts between libraries.',
    },
    {
      name: '--turbo-modules-spec-paths [path...]',
      description:
        'Path or paths to Turbo Module spec files. Each path can point to a spec file or a directory containing spec files.',
      parse: (val, prev) => (prev ? [...prev, val] : [val]),
    },
    {
      name: '--arkts-components-spec-paths [path...]',
      description:
        'Path or paths to component spec files used to generate code for components to be implemented on the ArkTS side. Each path can point to a spec file or a directory containing spec files.',
      parse: (val, prev) => (prev ? [...prev, val] : [val]),
    },
    {
      name: '--cpp-components-spec-paths [path...]',
      description:
        'Path or paths to component spec files used to generate code for components to be implemented on the C++ side. Each path can point to a spec file or a directory containing spec files.',
      parse: (val, prev) => (prev ? [...prev, val] : [val]),
    },
    {
      name: '--cpp-output-path <path>',
      description:
        'Specifies the relative path to the output directory intended for storing generated C++ files.',
    },
    {
      name: '--ets-output-path <path>',
      description:
        'Specifies the relative path to the output directory intended for storing generated ETS files.',
    },
    {
      name: '--no-safety-check [boolean]',
      description:
        'Skips the check that prevents file operations outside the current working directory. This command permanently deletes previously generated files. Files are generated in the path specified by cpp-output-path.',
    },
  ],
  func: async (_argv, _config, rawArgs: any) => {
    const logger = new Logger();
    try {
      const { args, uberSchemaByType } =
        validateArgsAndRetrieveUberSchemaByType(rawArgs);
      const fileContentByPath = generateCodeInMemory({
        args,
        uberSchemaByType,
      });
      saveAndLogCodegenResult(args, logger, fileContentByPath);
    } catch (err) {
      if (err instanceof DescriptiveError && !err.isUnexpected()) {
        logger.descriptiveError(err);
        return;
      }
      throw err;
    }
  },
};

type RawArgs = {
  npmPackageName?: string;
  turboModulesSpecPaths?: string[];
  arktsComponentsSpecPaths?: string[];
  cppComponentsSpecPaths?: string[];
  cppOutputPath?: string;
  etsOutputPath?: string;
  safetyCheck?: boolean;
};

function validateArgsAndRetrieveUberSchemaByType(rawArgs: RawArgs) {
  const args = validateRawArgs(rawArgs);
  const arkTSComponentSpecPaths = specPathsToSpecFilePaths(
    args.arkTSComponentsSpecPaths
  );
  const turboModulesUberSchema = UberSchema.fromSpecFilePaths(
    specPathsToSpecFilePaths(args.tmSpecPaths)
  );
  const arkTSComponentsUberSchema = UberSchema.fromSpecFilePaths(
    arkTSComponentSpecPaths
  );
  const cppComponentSpecPaths = specPathsToSpecFilePaths(
    args.cppComponentsSpecPaths
  );
  const cppComponentsUberSchema = UberSchema.fromSpecFilePaths(
    cppComponentSpecPaths
  );
  const componentsUberSchema = UberSchema.fromSpecFilePaths([
    ...arkTSComponentSpecPaths,
    ...cppComponentSpecPaths,
  ]);

  return {
    args,
    uberSchemaByType: {
      turboModulesUberSchema,
      arkTSComponentsUberSchema,
      cppComponentsUberSchema,
      componentsUberSchema,
    },
  };
}

type Args = {
  npmPackageName: string;
  tmSpecPaths: AbsolutePath[];
  arkTSComponentsSpecPaths: AbsolutePath[];
  cppComponentsSpecPaths: AbsolutePath[];
  cppOutputPath: AbsolutePath;
  etsOutputPath: AbsolutePath;
  safetyCheck: boolean;
};

function validateRawArgs(rawArgs: RawArgs): Args {
  if (!rawArgs.npmPackageName) {
    throw new DescriptiveError({
      whatHappened: '--npm-package-name is required',
      whatCanUserDo: ['Please specify --npm-package-name'],
    });
  }
  if (!rawArgs.cppOutputPath) {
    throw new DescriptiveError({
      whatHappened: '--cpp-output-path is required',
      whatCanUserDo: ['Please specify --cpp-output-path'],
    });
  }
  if (!rawArgs.etsOutputPath) {
    throw new DescriptiveError({
      whatHappened: '--ets-output-path is required',
      whatCanUserDo: ['Please specify --ets-output-path'],
    });
  }
  return {
    npmPackageName: rawArgs.npmPackageName!,
    tmSpecPaths: (rawArgs.turboModulesSpecPaths ?? []).map(
      (path) => new AbsolutePath(path)
    ),
    arkTSComponentsSpecPaths: (rawArgs.arktsComponentsSpecPaths ?? []).map(
      (path) => new AbsolutePath(path)
    ),
    cppComponentsSpecPaths: (rawArgs.cppComponentsSpecPaths ?? []).map(
      (path) => new AbsolutePath(path)
    ),
    etsOutputPath: new AbsolutePath(rawArgs.etsOutputPath!),
    cppOutputPath: new AbsolutePath(rawArgs.cppOutputPath!),
    safetyCheck: !!rawArgs.safetyCheck,
  };
}

function specPathsToSpecFilePaths(specPaths: AbsolutePath[]): AbsolutePath[] {
  const SUPPORTED_SPEC_EXTENSIONS = ['js', 'jsx', 'ts', 'tsx'];
  return specPaths.flatMap((specPath) => {
    if (!fs.existsSync(specPath.getValue())) {
      throw new DescriptiveError({
        whatHappened: `No such file or directory: ${specPath.getValue()}`,
        whatCanUserDo: [`Are you sure provided specPath exists?`],
      });
    }
    if (fs.lstatSync(specPath.getValue()).isDirectory()) {
      return findFilePathsWithExtensions(specPath, SUPPORTED_SPEC_EXTENSIONS);
    } else {
      const ext = specPath.getExtension();
      if (ext && SUPPORTED_SPEC_EXTENSIONS.includes(ext)) {
        return [specPath];
      }
      return [];
    }
  });
}

function generateCodeInMemory({
  args,
  uberSchemaByType: {
    arkTSComponentsUberSchema,
    componentsUberSchema,
    cppComponentsUberSchema,
    turboModulesUberSchema,
  },
}: ReturnType<typeof validateArgsAndRetrieveUberSchemaByType>) {
  const CODEGEN_NOTICE_LINES = [
    `This code was generated by "react-native ${COMMAND_NAME}"`,
  ];
  const fileContentByPath = new Map<AbsolutePath, string>();
  const appendToFileContentByPath = (
    fileContent: string,
    path: AbsolutePath
  ) => {
    fileContentByPath.set(path, fileContent);
  };

  const turboModuleGlueCodeData = turboModulesUberSchema
    .findAllSpecSchemasByType('NativeModule')
    .map((specSchema) => {
      const nativeModuleCodeGenerator = new NativeModuleCodeGenerator(
        args.cppOutputPath.copyWithNewSegment(
          'RNOH',
          'generated',
          'turbo_modules'
        ),
        args.etsOutputPath.copyWithNewSegment('turboModules'),
        CODEGEN_NOTICE_LINES,
        '@rnoh/react-native-openharmony/ts'
      );
      nativeModuleCodeGenerator
        .generate(specSchema)
        .forEach(appendToFileContentByPath);
      return nativeModuleCodeGenerator.getGlueCodeData();
    })
    .flat();

  const arkTSComponentCodeGeneratorCAPI = new ArkTSComponentCodeGeneratorCAPI(
    args.cppOutputPath.copyWithNewSegment('RNOH', 'generated', 'components'),
    args.etsOutputPath.copyWithNewSegment('components'),
    CODEGEN_NOTICE_LINES,
    '@rnoh/react-native-openharmony/ts'
  );
  arkTSComponentCodeGeneratorCAPI
    .generate(arkTSComponentsUberSchema)
    .forEach(appendToFileContentByPath);

  const libraryCppName = deriveLibraryCppNameFromNpmPackageName(
    args.npmPackageName
  );
  const cppComponentCodeGenerator = new CppComponentCodeGenerator(
    (filename) => {
      return args.cppOutputPath.copyWithNewSegment(
        'RNOH',
        'generated',
        'components',
        filename
      );
    },
    CODEGEN_NOTICE_LINES
  );
  cppComponentCodeGenerator
    .generate({
      libraryCppName: libraryCppName,
      uberSchema: cppComponentsUberSchema,
    })
    .forEach(appendToFileContentByPath);

  new SharedComponentCodeGenerator((filename) => {
    return args.cppOutputPath.copyWithNewSegment(
      'react',
      'renderer',
      'components',
      libraryCppName,
      filename
    );
  })
    .generate({
      libraryCppName: libraryCppName,
      uberSchema: componentsUberSchema,
    })
    .forEach(appendToFileContentByPath);

  new LibGlueCodeGenerator(
    args.cppOutputPath,
    args.etsOutputPath,
    CODEGEN_NOTICE_LINES
  )
    .generate({
      libraryCppName,
      arkTSComponents: arkTSComponentCodeGeneratorCAPI.getLibGlueCodeData(),
      cppComponents: cppComponentCodeGenerator
        .getGlueCodeData()
        .map(({ name }) => ({ componentName: name })),
      turboModules: turboModuleGlueCodeData,
    })
    .forEach(appendToFileContentByPath);

  return fileContentByPath;
}

function deriveLibraryCppNameFromNpmPackageName(
  npmPackageName: string
): string {
  let result = npmPackageName;
  if (npmPackageName.includes('/')) {
    result.replace('@', '');
    result = npmPackageName.replace('/', '__');
  }
  result = Case.snake(result);
  return result;
}

function saveAndLogCodegenResult(
  args: Args,
  logger: Logger,
  fileContentByPath: ReturnType<typeof generateCodeInMemory>
) {
  saveCodegenResultToFileSystem(
    fileContentByPath,
    args.cppOutputPath,
    args.etsOutputPath,
    args.safetyCheck
  );
  logCodegenResult(logger, fileContentByPath, new AbsolutePath(''));
}

function saveCodegenResultToFileSystem(
  fileContentByPath: Map<AbsolutePath, string>,
  cppOutputPath: AbsolutePath,
  etsOutputPath: AbsolutePath,
  enableSafetyCheck: boolean
) {
  prepareDirectory(cppOutputPath, enableSafetyCheck);
  prepareDirectory(etsOutputPath, enableSafetyCheck);
  prepareDirectory(
    etsOutputPath.copyWithNewSegment('components'),
    enableSafetyCheck
  );
  prepareDirectory(
    etsOutputPath.copyWithNewSegment('turboModules'),
    enableSafetyCheck
  );
  fileContentByPath.forEach((_fileContent, path) => {
    prepareDirectory(path.getDirectoryPath(), enableSafetyCheck);
  });
  fileContentByPath.forEach((fileContent, path) => {
    fs.writeFileSync(path.getValue(), fileContent);
  });
}

function prepareDirectory(path: AbsolutePath, enableSafetyCheck: boolean) {
  maybeMakeDirectories(path);
  if (enableSafetyCheck && !path.getValue().startsWith(process.cwd())) {
    throw new DescriptiveError({
      whatHappened: `Tried to remove files in ${path.getValue()}\nand that path is outside current working directory`,
      whatCanUserDo: [
        'Run this command with --no-safety-check flag',
        'Run codegen from different location',
      ],
    });
  }
  maybeRemoveFilesInDirectory(path);
}

function logCodegenResult(
  logger: Logger,
  fileContentByPath: Map<AbsolutePath, string>,
  rootPath: AbsolutePath
) {
  const sortedRelativePathStrings = Array.from(fileContentByPath.keys()).map(
    (path) => path.relativeTo(rootPath).getValue()
  );
  sortedRelativePathStrings.sort();
  sortedRelativePathStrings.forEach((pathStr) => {
    logger.info((styles) => styles.gray(`• ${pathStr}`));
  });
  logger.info(() => '');
  logger.info(
    (styles) =>
      `Generated ${styles.green(styles.bold(fileContentByPath.size))} file(s)`
  );
  logger.info(() => '');
}