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

import JSON5 from 'json5';
import {
  AbsolutePath,
  ProjectDependenciesManager,
  FS,
  DescriptiveError,
} from '../core';
import Mustache from 'mustache';
import Case from 'case';
import { Logger } from '../io';
import pathUtils from 'node:path';
import { JSON5Writer } from './JSON5Writer';

type Version = string;
type DependencySpecifier = `file:${string}` | Version;

export type AutolinkingConfig = {
  harmonyProjectPath: AbsolutePath;
  nodeModulesPath: AbsolutePath;
  ohPackagePathRelativeToHarmony: string;
  etsRNOHPackagesFactoryPathRelativeToHarmony: string;
  cppRNOHPackagesFactoryPathRelativeToHarmony: string;
  cmakeAutolinkPathRelativeToHarmony: string;
  excludedNpmPackageNames: Set<string>;
  includedNpmPackageNames: Set<string>;
};

/**
 * ohPackageName 数组配置项类型
 * 用于将 HAR 文件名映射到指定的 ohPackage 名
 */
type OhPackageNameMapping = {
  harName: string;
  packageName: string;
  /**
   * 可选版本号,指定后使用远程依赖(版本号)而非本地依赖(file: 路径)
   */
  version?: string;
};

/**
 * ohPackageName 支持的类型
 * - string: 所有 HAR 共用同一个包名(兼容旧格式)
 * - OhPackageNameMapping[]: 每个 HAR 可以有独立的包名
 */
type OhPackageNameConfig = string | OhPackageNameMapping[];

/**
 * 解析后的 HAR 配置
 * 每个 HAR 文件对应一个 ResolvedHar
 */
type ResolvedHar = {
  harFilePathRelativeToHarmony: string;
  ohPackageName: string;
  /**
   * 可选版本号,有值时使用远程依赖(版本号)而非本地依赖(file: 路径)
   */
  version?: string; 
};

/**
 * 可链接的库配置
 * 支持单个或多个 HAR 文件
 */
type AutolinkableLibrary = {
  npmPackageName: string;
  // 解析后的 HAR 配置数组(支持多个 HAR)
  resolvedHars: ResolvedHar[];
  // 以下配置所有 HAR 共享
  etsRNOHPackageClassName?: string;
  cppRNOHPackageClassName?: string;
  cmakeLibraryTargetName?: string;
};

type AutolinkingInput = {
  projectRootPath: AbsolutePath;
  etsRNOHPackagesFactoryPath: AbsolutePath;
  cppRNOHPackagesFactoryPath: AbsolutePath;
  cmakeAutolinkingPath: AbsolutePath;
  ohPackagePathAndContent: [AbsolutePath, string];
  autolinkableLibraries: AutolinkableLibrary[];
  nodeModuleHarFilePathsRelativeToHarmony: string[];
  skippedLibraryNpmPackageNames: string[];
};

type AutolinkingOutput = {
  linkedLibraryNpmPackageNames: string[];
  skippedLibraryNpmPackageNames: string[];
  projectRootPath: AbsolutePath;
  etsRNOHPackagesFactoryPathAndContent: [AbsolutePath, string];
  cppRNOHPackagesFactoryPathAndContent: [AbsolutePath, string];
  cmakeAutolinkingPathAndContent: [AbsolutePath, string];
  ohPackagePathAndContent: [AbsolutePath, string];
};

const ETS_RNOH_PACKAGES_FACTORY_TEMPLATE = `
/*
 * This file was generated by RNOH autolinking.
 * DO NOT modify it manually, your changes WILL be overwritten.
 */
import type { RNPackageContext, RNOHPackage } from '@rnoh/react-native-openharmony';
{{#libraries}}
import {{etsRNOHPackageClassName}} from '{{{ohPackageName}}}';
{{/libraries}}

export function createRNOHPackages(ctx: RNPackageContext): RNOHPackage[] {
  return [
{{#libraries}}
    new {{etsRNOHPackageClassName}}(ctx),
{{/libraries}}
  ];
}
`.trimStart();

const CPP_RNOH_PACKAGES_FACTORY_TEMPLATE = `
/*
 * This file was generated by RNOH autolinking.
 * DO NOT modify it manually, your changes WILL be overwritten.
 */
// clang-format off
#pragma once
#include "RNOH/Package.h"
{{#libraries}}
#include "{{cppRNOHPackageClassName}}.h"
{{/libraries}}

std::vector<rnoh::Package::Shared> createRNOHPackages(const rnoh::Package::Context &ctx) {
  return {
{{#libraries}}
    std::make_shared<rnoh::{{cppRNOHPackageClassName}}>(ctx),
{{/libraries}}
  };
}
`.trimStart();

const CMAKE_AUTOLINKING_TEMPLATE = `
# This file was generated by RNOH autolinking.
# DO NOT modify it manually, your changes WILL be overwritten.
cmake_minimum_required(VERSION 3.5)

# @actor RNOH_APP
function(autolink_libraries target)
{{#libraries}}
    add_subdirectory("\${OH_MODULES_DIR}/{{{ohPackageName}}}/src/main/cpp" ./{{cmakeLibraryTargetName}})
{{/libraries}}

    set(AUTOLINKED_LIBRARIES
{{#libraries}}
        {{cmakeLibraryTargetName}}
{{/libraries}}
    )

    foreach(lib \${AUTOLINKED_LIBRARIES})
        target_link_libraries(\${target} PUBLIC \${lib})
    endforeach()
endfunction()
`.trimStart();

export class Autolinking {
  constructor(
    private fs: FS,
    private logger: Logger
  ) { }

  async prepareInput(config: AutolinkingConfig): Promise<AutolinkingInput> {
    if (
      config.includedNpmPackageNames.size > 0 &&
      config.excludedNpmPackageNames.size > 0
    ) {
      throw new DescriptiveError({
        whatHappened: 'Tried to exclude and include npm packages.',
        whatCanUserDo: ['Include or exclude npm packages, but not both.'],

        extraData: {
          includedNpmPackageNamesCount: config.includedNpmPackageNames.size,
          excludedNpmPackageNamesCount: config.excludedNpmPackageNames.size,
          includedNpmPackageNames: config.includedNpmPackageNames,
          excludedNpmPackageNames: config.excludedNpmPackageNames,
        },
      });
    }
    const harmonyProjectPath =
      config.harmonyProjectPath ?? new AbsolutePath('./');
    const nodeModuleHarPaths: AbsolutePath[] = [];
    const autolinkableLibraries: AutolinkableLibrary[] = [];
    const projectRootPath = config.nodeModulesPath.copyWithNewSegment('..');
    const skippedLibraryNpmPackageNames: string[] = [];
    await new ProjectDependenciesManager(this.fs, projectRootPath).forEachAsync(
      (dependency) => {
        // 读取 package.json 配置
        const packageJson = dependency.readPackageJSON();
        const providedAutolinkingConfig = packageJson.harmony?.autolinking;

        // 解析 autolinking 配置
        const autolinkingConfig =
          providedAutolinkingConfig === true ? {} : providedAutolinkingConfig;

        // 获取自定义的 HAR 扫描路径
        const mainHarPath = autolinkingConfig?.mainHarPath;

        // 扫描 HAR 文件
        const harFilePaths = dependency.getHarFilePaths(mainHarPath);
        if (harFilePaths.length === 0) {
          return;
        }
        nodeModuleHarPaths.push(...harFilePaths);

        // 检查是否应该跳过该包
        if (
          providedAutolinkingConfig === undefined ||
          providedAutolinkingConfig === null ||
          (config.excludedNpmPackageNames.has(packageJson.name) &&
            config.excludedNpmPackageNames.size > 0) ||
          (!config.includedNpmPackageNames.has(packageJson.name) &&
            config.includedNpmPackageNames.size > 0)
        ) {
          skippedLibraryNpmPackageNames.push(packageJson.name);
          return;
        }

        // 解析 ohPackageName 配置,生成每个 HAR 对应的包名
        const ohPackageNameConfig = autolinkingConfig?.ohPackageName as OhPackageNameConfig | undefined;
        const resolvedHars = this.resolveHarPackageNames(
          harFilePaths,
          ohPackageNameConfig,
          packageJson.name,
          harmonyProjectPath
        );

        autolinkableLibraries.push({
          npmPackageName: packageJson.name,
          resolvedHars,
          etsRNOHPackageClassName: autolinkingConfig?.etsPackageClassName,
          cppRNOHPackageClassName: autolinkingConfig?.cppPackageClassName,
          cmakeLibraryTargetName: autolinkingConfig?.cmakeLibraryTargetName,
        });
      }
    );
    const ohPackagePath = harmonyProjectPath.copyWithNewSegment(
      config.ohPackagePathRelativeToHarmony
    );
    const ohPackageContent = await this.fs.readText(ohPackagePath);
    const cppRNOHPackagesFactoryPath = harmonyProjectPath.copyWithNewSegment(
      config.cppRNOHPackagesFactoryPathRelativeToHarmony
    );
    const cmakeAutolinkingPath = harmonyProjectPath.copyWithNewSegment(
      config.cmakeAutolinkPathRelativeToHarmony
    );

    return {
      projectRootPath,
      skippedLibraryNpmPackageNames,
      etsRNOHPackagesFactoryPath: harmonyProjectPath.copyWithNewSegment(
        config.etsRNOHPackagesFactoryPathRelativeToHarmony
      ),
      cppRNOHPackagesFactoryPath,
      cmakeAutolinkingPath: cmakeAutolinkingPath,
      ohPackagePathAndContent: [ohPackagePath, ohPackageContent],
      autolinkableLibraries,
      nodeModuleHarFilePathsRelativeToHarmony: nodeModuleHarPaths.map((p) =>
        p.relativeTo(harmonyProjectPath).toString()
      ),
    };
  }

  /**
   * 解析 HAR 文件的 ohPackage 名称
   * @param harFilePaths HAR 文件路径列表
   * @param ohPackageNameConfig ohPackageName 配置(字符串或数组)
   * @param npmPackageName npm 包名
   * @param harmonyProjectPath Harmony 项目路径
   */
  private resolveHarPackageNames(
    harFilePaths: AbsolutePath[],
    ohPackageNameConfig: OhPackageNameConfig | undefined,
    npmPackageName: string,
    harmonyProjectPath: AbsolutePath
  ): ResolvedHar[] {
    // 默认的 ohPackage 名称
    const defaultOhPackageName = this.npmPackageNameToOHPackageName(npmPackageName);

    // 构建 HAR 文件名到路径的映射
    const harPathMap = new Map<string, AbsolutePath>();
    for (const harPath of harFilePaths) {
      const harFileName = pathUtils.basename(harPath.toString());
      harPathMap.set(harFileName, harPath);
    }

    // 辅助函数:创建 ResolvedHar
    const createResolvedHar = (harPath: AbsolutePath, ohPkgName: string, version?: string): ResolvedHar => ({
      harFilePathRelativeToHarmony: harPath.relativeTo(harmonyProjectPath).toString(),
      ohPackageName: ohPkgName,
      version,
    });

    const result: ResolvedHar[] = [];
    const processedHarNames = new Set<string>();

    // 1. 如果是数组配置,按照配置顺序处理
    if (Array.isArray(ohPackageNameConfig)) {
      for (const mapping of ohPackageNameConfig) {
        const harPath = harPathMap.get(mapping.harName);
        if (harPath) {
          result.push(createResolvedHar(harPath, mapping.packageName, mapping.version));
          processedHarNames.add(mapping.harName);
        }
      }
    }

    // 2. 处理未在配置中指定的 HAR(按文件系统扫描顺序)
    for (const harPath of harFilePaths) {
      const harFileName = pathUtils.basename(harPath.toString());
      if (processedHarNames.has(harFileName)) {
        continue;
      }

      // 如果是字符串格式,所有 HAR 共用
      if (typeof ohPackageNameConfig === 'string') {
        const suffix = harFilePaths.length > 1
          ? '--' + pathUtils.basename(harFileName, '.har')
          : '';
        result.push(createResolvedHar(harPath, ohPackageNameConfig + suffix));
        continue;
      }

      // 默认命名规则
      const suffix = harFilePaths.length > 1
        ? '--' + pathUtils.basename(harFileName, '.har')
        : '';
      result.push(createResolvedHar(harPath, defaultOhPackageName + suffix));
    }

    return result;
  }

  evaluate(input: AutolinkingInput): AutolinkingOutput {
    // 处理库配置,应用默认值
    const autolinkableLibraries: (Required<Omit<AutolinkableLibrary, 'resolvedHars'>> & { resolvedHars: ResolvedHar[] })[] =
      input.autolinkableLibraries.map((lib) => {
        return {
          ...lib,
          etsRNOHPackageClassName:
            lib.etsRNOHPackageClassName ??
            this.npmPackageNameToRNOHPackageClassName(lib.npmPackageName),
          cppRNOHPackageClassName:
            lib.cppRNOHPackageClassName ??
            this.npmPackageNameToRNOHPackageClassName(lib.npmPackageName),
          cmakeLibraryTargetName:
            lib.cmakeLibraryTargetName ??
            this.npmPackageNameToCMakeLibraryTargetName(lib.npmPackageName),
          resolvedHars: lib.resolvedHars,
        };
      });
    autolinkableLibraries.sort((a, b) => {
      if (a.npmPackageName < b.npmPackageName) return -1;
      if (a.npmPackageName > b.npmPackageName) return 1;
      return 0;
    });

    const ohPackage = JSON5.parse(input.ohPackagePathAndContent[1]);
    const unmanagedNativeDependencySpecifierByName: Record<
      string,
      DependencySpecifier
    > = {};

    // 收集所有配置了 autolinking 的 npm 包名
    const autolinkedNpmPackageNames = new Set(
      autolinkableLibraries.map(lib => lib.npmPackageName)
    );

    Object.entries<DependencySpecifier>(ohPackage.dependencies).forEach(
      ([name, dependencySpecifier]) => {
        if (
          !(
            dependencySpecifier.includes('file:') &&
            dependencySpecifier.includes('node_modules')
          )
        ) {
          // 不是 file:node_modules 格式 → 非托管,保留
          unmanagedNativeDependencySpecifierByName[name] = dependencySpecifier;
        } else {
          // 是 file:node_modules 格式,判断对应的 npm 包是否配置了 autolinking
          const npmPackageName = this.extractNpmPackageNameFromPath(dependencySpecifier);

          if (!npmPackageName || !autolinkedNpmPackageNames.has(npmPackageName)) {
            // 没有配置 autolinking → 需要检查 HAR 文件是否实际存在
            const harPath = dependencySpecifier.replace('file:', '');
            const normalizedHarPath = pathUtils.normalize(harPath);
            // 依赖路径是相对于 harmony 项目的,oh-package.json5 所在目录就是 harmony 项目目录
            const harmonyProjectPath = input.ohPackagePathAndContent[0].getDirectoryPath();
            const absoluteHarPath = harmonyProjectPath.copyWithNewSegment(normalizedHarPath);

            if (this.fs.existsSync(absoluteHarPath)) {
              // HAR 文件存在 → 非托管,保留原依赖
              unmanagedNativeDependencySpecifierByName[name] = dependencySpecifier;
            }
            // HAR 文件不存在 → 不保留(清理无效依赖)
          }
          // 如果配置了 autolinking → 不保留(后面会用新生成的值替换)
        }
      }
    );

    // 为每个 HAR 生成依赖条目
    const managedNativeDependencySpecifierByName: Record<string, string> = {};
    for (const library of autolinkableLibraries) {
      for (const har of library.resolvedHars) {
        // 如果有 version 字段,使用版本号(远程依赖);否则使用 file: 路径(本地依赖)
        managedNativeDependencySpecifierByName[har.ohPackageName] =
          har.version ??
          `file:${har.harFilePathRelativeToHarmony}`.split(pathUtils.sep).join('/');
      }
    }

    // 为模板准备数据:每个库需要生成 import 语句
    // 由于所有 HAR 共享同一个 Package 类名,每个库只需要一个 import
    const templateLibraries = autolinkableLibraries.map((lib) => {
      // 使用第一个 HAR 的 ohPackageName 作为 import 来源
      const primaryOhPackageName = lib.resolvedHars[0]?.ohPackageName ?? '';
      return {
        ...lib,
        ohPackageName: primaryOhPackageName,
      };
    });

    return {
      skippedLibraryNpmPackageNames: input.skippedLibraryNpmPackageNames,
      linkedLibraryNpmPackageNames: autolinkableLibraries.map(
        (lib) => lib.npmPackageName
      ),
      projectRootPath: input.projectRootPath,
      cppRNOHPackagesFactoryPathAndContent: [
        input.cppRNOHPackagesFactoryPath,
        Mustache.render(CPP_RNOH_PACKAGES_FACTORY_TEMPLATE, {
          libraries: templateLibraries,
        }),
      ],
      etsRNOHPackagesFactoryPathAndContent: [
        input.etsRNOHPackagesFactoryPath,
        Mustache.render(ETS_RNOH_PACKAGES_FACTORY_TEMPLATE, {
          libraries: templateLibraries,
        }),
      ],
      cmakeAutolinkingPathAndContent: [
        input.cmakeAutolinkingPath,
        Mustache.render(CMAKE_AUTOLINKING_TEMPLATE, {
          libraries: templateLibraries,
        }),
      ],
      ohPackagePathAndContent: [
        input.ohPackagePathAndContent[0],
        JSON5Writer.updateDependenciesWithFallback(
          input.ohPackagePathAndContent[1],
          {
            ...unmanagedNativeDependencySpecifierByName,
            ...managedNativeDependencySpecifierByName,
          }
        ),
      ],
    };
  }

  private npmPackageNameToRNOHPackageClassName(fullNpmPackageName: string) {
    if (fullNpmPackageName.startsWith('@')) {
      const [scopeName, packageName] = fullNpmPackageName
        .replace('@', '')
        .split('/');
      return Case.pascal(scopeName) + Case.pascal(packageName) + 'Package';
    }
    return Case.pascal(fullNpmPackageName) + 'Package';
  }

  private npmPackageNameToCMakeLibraryTargetName(fullNpmPackageName: string) {
    if (fullNpmPackageName.startsWith('@')) {
      const [scopeName, packageName] = fullNpmPackageName
        .replace('@', '')
        .split('/');
      return 'rnoh__' + Case.snake(scopeName) + '__' + Case.snake(packageName);
    }
    return 'rnoh__' + Case.snake(fullNpmPackageName);
  }

  private npmPackageNameToOHPackageName(fullNpmPackageName: string): string {
    if (fullNpmPackageName.startsWith('@')) {
      const [scopeName, packageName] = fullNpmPackageName
        .replace('@', '')
        .split('/');
      return '@rnoh/' + Case.kebab(scopeName) + '--' + Case.kebab(packageName);
    }
    return '@rnoh/' + Case.kebab(fullNpmPackageName);
  }

  /**
   * 从依赖路径中提取 npm 包名
   * @param dependencySpecifier 依赖路径,如 "file:../node_modules/@scope/package/harmony/xxx.har"
   * @returns npm 包名,如 "@scope/package";如果无法解析则返回 null
   */
  private extractNpmPackageNameFromPath(dependencySpecifier: string): string | null {
    // 移除 file: 前缀
    const path = dependencySpecifier.replace(/^file:/, '');

    // 查找 node_modules 后面的部分
    const nodeModulesIndex = path.indexOf('node_modules/');
    if (nodeModulesIndex === -1) {
      return null;
    }

    const afterNodeModules = path.substring(nodeModulesIndex + 'node_modules/'.length);

    // 检查是否是 scoped package (@scope/package)
    if (afterNodeModules.startsWith('@')) {
      const parts = afterNodeModules.split('/');
      if (parts.length >= 2) {
        return `${parts[0]}/${parts[1]}`;  // @scope/package
      }
    } else {
      // 非 scoped package
      const firstSlash = afterNodeModules.indexOf('/');
      if (firstSlash !== -1) {
        return afterNodeModules.substring(0, firstSlash);
      }
      return afterNodeModules || null;
    }

    return null;
  }

  saveAndLogOutput(output: AutolinkingOutput) {
    const pathAndContentPairs: [AbsolutePath, string][] = [];
    pathAndContentPairs.push(output.etsRNOHPackagesFactoryPathAndContent);
    pathAndContentPairs.push(output.cppRNOHPackagesFactoryPathAndContent);
    pathAndContentPairs.push(output.cmakeAutolinkingPathAndContent);
    pathAndContentPairs.push(output.ohPackagePathAndContent);
    pathAndContentPairs.forEach(([path, content]) => {
      this.fs.writeTextSync(path, content);
    });
    output.linkedLibraryNpmPackageNames.forEach((npmPackageName) => {
      this.logger.debug(
        (styles) => `[${styles.green(styles.bold('link'))}] ${npmPackageName}`
      );
    });
    output.skippedLibraryNpmPackageNames.forEach((npmPackageName) => {
      this.logger.debug(
        (styles) => `[${styles.yellow(styles.bold('skip'))}] ${npmPackageName}`
      );
    });
    this.logger.debug(() => '');
    const sortedPathsRelativeToRoot = pathAndContentPairs
      .map(([path]) => path.relativeTo(output.projectRootPath).toString())
      .sort();
    sortedPathsRelativeToRoot.forEach((pathStr) => {
      this.logger.debug((styles) => styles.gray(`• ${pathStr}`));
    });
    this.logger.debug(() => '');
    this.logger.info(
      (styles) =>
        `updated ${styles.green(
          styles.bold(sortedPathsRelativeToRoot.length)
        )} file(s), linked ${styles.green(
          styles.bold(output.linkedLibraryNpmPackageNames.length)
        )} libraries, skipped ${styles.yellow(
          styles.bold(output.skippedLibraryNpmPackageNames.length)
        )} libraries`
    );
    this.logger.debug(() => '');
  }
}