/*
 * Copyright (c) 2022-2026 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as fs from 'node:fs';
import * as path from 'node:path';
import * as ts from 'typescript';
import * as qEd from './autofixes/QuasiEditor';
import type { BaseTypeScriptLinter } from './BaseTypeScriptLinter';
import type { CommandLineOptions } from './CommandLineOptions';
import { InteropTypescriptLinter } from './InteropTypescriptLinter';
import type { LinterConfig } from './LinterConfig';
import type { LinterOptions } from './LinterOptions';
import type { LintRunResult } from './LintRunResult';
import { Logger } from './Logger';
import type { ProblemInfo } from './ProblemInfo';
import type { CmdProgressInfo } from './progress/CmdProgressInfo';
import {
  FixedLineProgressBar,
  postProcessCmdProgressBar,
  preProcessCmdProgressBar,
  processCmdProgressBar
} from './progress/FixedLineProgressBar';
import type { MigrationInfo } from './progress/MigrationInfo';
import type { ProgressBarInfo } from './progress/ProgressBarInfo';
import { ProjectStatistics } from './statistics/ProjectStatistics';
import { generateMigrationStatisicsReport } from './statistics/scan/ProblemStatisticsCommonFunction';
import type { TimeRecorder } from './statistics/scan/TimeRecorder';
import type { createProgramCallback } from './ts-compiler/Compiler';
import { compileLintOptions } from './ts-compiler/Compiler';
import { getTscDiagnostics } from './ts-diagnostics/GetTscDiagnostics';
import { transformTscDiagnostics } from './ts-diagnostics/TransformTscDiagnostics';
import { TypeScriptLinter } from './TypeScriptLinter';
import {
  ARKTS_IGNORE_DIRS_NO_OH_MODULES,
  ARKTS_IGNORE_DIRS_OH_MODULES,
  ARKTS_IGNORE_FILES
} from './utils/consts/ArktsIgnorePaths';
import { EXTNAME_JS, EXTNAME_TS } from './utils/consts/ExtensionName';
import { USE_STATIC } from './utils/consts/InteropAPI';
import { LibraryTypeCallDiagnosticChecker } from './utils/functions/LibraryTypeCallDiagnosticChecker';
import { mergeArrayMaps } from './utils/functions/MergeArrayMaps';
import { clearPathHelperCache, pathContainsDirectory } from './utils/functions/PathHelper';
import { processSyncErr } from './utils/functions/ProcessWrite';
import type { LinterInputInfo } from './LinterInputInfo';
import { collectCommonApiInfo, clearCommonApiInfoCache } from './utils/functions/CommonApiInfo';
import { tryCallGC, tryForceCallGC } from './utils/functions/GarbageCollectorUtils';

function prepareInputFilesList(cmdOptions: CommandLineOptions): string[] {
  let inputFiles = cmdOptions.inputFiles.map((x) => {
    return path.normalize(x);
  });
  if (!cmdOptions.isCommandConfig || !cmdOptions.parsedConfigFile) {
    return inputFiles;
  }

  inputFiles = cmdOptions.parsedConfigFile.fileNames;
  if (cmdOptions.inputFiles.length <= 0) {
    return inputFiles;
  }

  /*
   * Apply linter only to the project source files that are specified
   * as a command-line arguments. Other source files will be discarded.
   */
  const cmdInputsResolvedPaths = cmdOptions.inputFiles.map((x) => {
    return path.resolve(x);
  });
  const configInputsResolvedPaths = inputFiles.map((x) => {
    return path.resolve(x);
  });
  inputFiles = configInputsResolvedPaths.filter((x) => {
    return cmdInputsResolvedPaths.some((y) => {
      return x === y;
    });
  });

  return inputFiles;
}

export function lint(
  config: LinterConfig,
  timeRecorder: TimeRecorder,
  hcResults?: Map<string, ProblemInfo[]>
): LintRunResult {
  const lintResult = lintImpl(config);
  timeRecorder.endScan();
  return config.cmdOptions.linterOptions.migratorMode ?
    migrate(config, lintResult, timeRecorder, hcResults) :
    lintResult;
}

function lintImpl(config: LinterConfig, migrationInfo?: MigrationInfo): LintRunResult {
  const { cmdOptions, tscCompiledProgram } = config;
  const tsProgram = tscCompiledProgram.getProgram();
  const options = cmdOptions.linterOptions;

  // Prepare list of input files for linter and retrieve AST for those files.
  let inputFiles = prepareInputFilesList(cmdOptions);
  inputFiles = inputFiles.filter((input) => {
    return shouldProcessFile(options, input);
  });
  options.inputFiles = inputFiles;
  const srcFiles: ts.SourceFile[] = [];
  for (const inputFile of inputFiles) {
    const srcFile = tsProgram.getSourceFile(inputFile);
    if (srcFile) {
      srcFiles.push(srcFile);
    }
    collectCommonApiInfo(tsProgram);
  }

  const tscStrictDiagnostics = getTscDiagnostics(tscCompiledProgram, srcFiles);
  LibraryTypeCallDiagnosticChecker.instance.rebuildTscDiagnostics(tscStrictDiagnostics);
  const lintResult = lintFiles(tsProgram, srcFiles, options, tscStrictDiagnostics, migrationInfo);
  LibraryTypeCallDiagnosticChecker.instance.clear();

  if (!options.ideInteractive) {
    lintResult.problemsInfos = mergeArrayMaps(lintResult.problemsInfos, transformTscDiagnostics(tscStrictDiagnostics));
  }

  freeMemory();
  return lintResult;
}

function lintFiles(
  tsProgram: ts.Program,
  srcFiles: ts.SourceFile[],
  options: LinterOptions,
  tscStrictDiagnostics: Map<string, ts.Diagnostic[]>,
  migrationInfo?: MigrationInfo
): LintRunResult {
  TypeScriptLinter.initGlobals();
  InteropTypescriptLinter.initGlobals();
  const cmdProgressBar = new FixedLineProgressBar();
  const cmdProgressInfo: CmdProgressInfo = {
    cmdProgressBar: cmdProgressBar,
    migrationInfo: migrationInfo,
    srcFiles: srcFiles,
    options: options
  };

  if (options.ideInteractive) {
    process.stderr.write('\n');
    preProcessCmdProgressBar(cmdProgressInfo);
  }
  const linterInputInfo: LinterInputInfo = {
    tsProgram: tsProgram,
    srcFiles: srcFiles,
    options: options,
    tscStrictDiagnostics: tscStrictDiagnostics,
    migrationInfo: migrationInfo,
    cmdProgressInfo: cmdProgressInfo
  };

  const lintResult = executeLinter(linterInputInfo);
  if (options.ideInteractive) {
    postProcessCmdProgressBar(cmdProgressInfo);
  }
  return lintResult;
}

function executeLinter(linterInputInfo: LinterInputInfo): LintRunResult {
  const { tsProgram, srcFiles, options, tscStrictDiagnostics, migrationInfo, cmdProgressInfo } = linterInputInfo;
  const projectStats: ProjectStatistics = new ProjectStatistics();
  const problemsInfos: Map<string, ProblemInfo[]> = new Map();
  let fileCount: number = 0;
  for (const srcFile of srcFiles) {
    const linter: BaseTypeScriptLinter = !options.interopCheckMode ?
      new TypeScriptLinter(tsProgram.getTypeChecker(), options, srcFile, tscStrictDiagnostics) :
      new InteropTypescriptLinter(tsProgram.getTypeChecker(), tsProgram.getCompilerOptions(), options, srcFile);

    if (migrationInfo?.migrateforUI && linter instanceof TypeScriptLinter) {
      linter.lintForUI();
    } else {
      linter.lint();
    }
    const problems = linter.problemsInfos;
    problemsInfos.set(path.normalize(srcFile.fileName), [...problems]);
    projectStats.fileStats.push(linter.fileStats);
    fileCount += 1;
    if (options.ideInteractive) {
      processCmdProgressBar(cmdProgressInfo, fileCount);
      processIdeProgressBar(
        { migrationInfo: migrationInfo, currentSrcFile: srcFile, srcFiles: srcFiles, options: options },
        fileCount,
        cmdProgressInfo.cmdProgressBar
      );
    }
  }
  return {
    hasErrors: projectStats.hasError(),
    problemsInfos: problemsInfos,
    projectStats: projectStats
  };
}

export function processIdeProgressBar(
  progressBarInfo: ProgressBarInfo,
  fileCount: number,
  cmdProgressBar: FixedLineProgressBar
): void {
  const { currentSrcFile, srcFiles, options } = progressBarInfo;

  const isMigrationStep = options.migratorMode && progressBarInfo.migrationInfo;
  const phasePrefix = isMigrationStep ? 'Migration Phase' : 'Scan Phase';

  const migrationPhase = isMigrationStep ?
    ` ${(progressBarInfo.migrationInfo!.currentPass ?? 0) + 1} / ${progressBarInfo.migrationInfo!.maxPasses ?? 1}` :
    '';

  const progressRatio = srcFiles.length > 0 ? fileCount / srcFiles.length : 0;
  const displayContent = `currentFile: ${currentSrcFile ? currentSrcFile.fileName : 'N/A'}, ${phasePrefix}${migrationPhase}`;

  const displayInfo = cmdProgressBar.getDisplayInfo();

  process.stderr.write('\x1B7');

  const linesToMove = displayInfo.totalReserved - displayInfo.ideLine;
  process.stderr.write(`\x1B[${linesToMove}A`);

  process.stderr.write('\x1B[2K');

  processSyncErr(
    JSON.stringify({
      content: displayContent,
      messageType: 1,
      indicator: progressRatio
    }) + '\n'
  );

  process.stderr.write('\x1B8');
}

function migrate(
  linterConfig: LinterConfig,
  lintResult: LintRunResult,
  timeRecorder: TimeRecorder,
  hcResults?: Map<string, ProblemInfo[]>
): LintRunResult {
  timeRecorder.startMigration();
  const { cmdOptions } = linterConfig;
  const updatedSourceTexts: Map<string, string> = new Map();
  tryForceCallGC();

  let mergedProblems: Map<string, ProblemInfo[]> = hcResults ?? new Map();
  mergedProblems = mergeArrayMaps(
    mergedProblems,
    filterLinterProblemsWithAutofixConfig(linterConfig.cmdOptions, lintResult.problemsInfos)
  );
  const changedFiles = fix(linterConfig, updatedSourceTexts, mergedProblems);

  if (changedFiles.length !== 0) {
    cmdOptions.inputFiles = changedFiles;
    updateLinterConfig(cmdOptions, linterConfig, updatedSourceTexts);

    const lintUIResults = lintImpl(linterConfig, { migrateforUI: true });
    fix(linterConfig, updatedSourceTexts, mergedProblems, lintUIResults.problemsInfos);
  }

  // Write new text for updated source files.
  updateSourceFiles(updatedSourceTexts, cmdOptions);

  timeRecorder.endMigration();
  generateMigrationStatisicsReport(mergedProblems, timeRecorder, cmdOptions.outputFilePath);

  if (!cmdOptions.linterOptions.ideInteractive) {
    updateLinterConfig(cmdOptions, linterConfig, updatedSourceTexts);
    return lintImpl(linterConfig);
  }

  lintResult.problemsInfos = mergedProblems;
  return lintResult;
}

function updateLinterConfig(
  cmdOptions: CommandLineOptions,
  linterConfig: LinterConfig,
  updatedSourceTexts: Map<string, string>
): void {
  const newLinterConfig = compileLintOptions(cmdOptions, getMigrationCreateProgramCallback(updatedSourceTexts));
  linterConfig.tscCompiledProgram = newLinterConfig.tscCompiledProgram;
  tryCallGC();
}

function filterLinterProblemsWithAutofixConfig(
  cmdOptions: CommandLineOptions,
  problemsInfos: Map<string, ProblemInfo[]>
): Map<string, ProblemInfo[]> {
  const autofixRuleConfigTags = cmdOptions.linterOptions.autofixRuleConfigTags;
  if (!cmdOptions.linterOptions.ideInteractive || !autofixRuleConfigTags) {
    return problemsInfos;
  }

  const needToBeFixedProblemsInfos = new Map<string, ProblemInfo[]>();
  for (const [filePath, problems] of problemsInfos) {
    const needToFix: ProblemInfo[] = problems.filter((problem) => {
      return autofixRuleConfigTags.has(problem.ruleTag);
    });
    if (needToFix.length >= 0) {
      needToBeFixedProblemsInfos.set(filePath, needToFix);
    }
  }
  return needToBeFixedProblemsInfos;
}

function updateSourceFiles(updatedSourceTexts: Map<string, string>, cmdOptions: CommandLineOptions): void {
  updatedSourceTexts.forEach((newText, fileName) => {
    if (!cmdOptions.linterOptions.noMigrationBackupFile) {
      qEd.QuasiEditor.backupSrcFile(fileName);
    }
    const filePathMap = cmdOptions.linterOptions.migrationFilePathMap;
    const writeFileName = filePathMap?.get(fileName) ?? fileName;
    fs.writeFileSync(writeFileName, newText);
  });
}

function hasUseStaticDirective(srcFile: ts.SourceFile): boolean {
  if (!srcFile?.statements.length) {
    return false;
  }
  const statements = srcFile.statements;
  return (
    ts.isExpressionStatement(statements[0]) &&
    ts.isStringLiteral(statements[0].expression) &&
    statements[0].expression.getText() === USE_STATIC
  );
}

function fix(
  linterConfig: LinterConfig,
  updatedSourceTexts: Map<string, string>,
  mergedProblems: Map<string, ProblemInfo[]>,
  lintUIProblems?: Map<string, ProblemInfo[]>
): string[] {
  const program = linterConfig.tscCompiledProgram.getProgram();
  const changedFiles: string[] = [];
  if (lintUIProblems) {
    mergedProblems = lintUIProblems;
  }
  mergedProblems.forEach((problemInfos, fileName) => {
    const srcFile = program.getSourceFile(fileName);
    if (!srcFile) {
      if (!linterConfig.cmdOptions.homecheck) {
        Logger.error(`Failed to retrieve source file: ${fileName}`);
      }
      return;
    }
    const hasAnyAutofixes = qEd.QuasiEditor.hasAnyAutofixes(problemInfos);

    // 'use static' directive is added only when file has no other fixes
    const needToAddUseStatic =
      linterConfig.cmdOptions.linterOptions.arkts2 &&
      linterConfig.cmdOptions.inputFiles.includes(fileName) &&
      !hasUseStaticDirective(srcFile) &&
      linterConfig.cmdOptions.linterOptions.ideInteractive &&
      (!hasAnyAutofixes || !!lintUIProblems);

    // If nothing to fix or don't need to add 'use static', then skip file
    if (!hasAnyAutofixes && !needToAddUseStatic) {
      return;
    }
    const qe: qEd.QuasiEditor = new qEd.QuasiEditor(
      fileName,
      srcFile.text,
      linterConfig.cmdOptions.linterOptions,
      linterConfig.cmdOptions.outputFilePath
    );
    updatedSourceTexts.set(fileName, qe.fix(problemInfos, needToAddUseStatic));
    if (hasAnyAutofixes) {
      changedFiles.push(fileName);
    }
  });
  return changedFiles;
}

function getMigrationCreateProgramCallback(updatedSourceTexts: Map<string, string>): createProgramCallback {
  return (createProgramOptions: ts.CreateProgramOptions): ts.Program => {
    const compilerHost = createProgramOptions.host || ts.createCompilerHost(createProgramOptions.options, true);
    const originalReadFile = compilerHost.readFile;
    compilerHost.readFile = (fileName: string): string | undefined => {
      const newText = updatedSourceTexts.get(path.normalize(fileName));
      return newText || originalReadFile(fileName);
    };
    createProgramOptions.host = compilerHost;
    return ts.createProgram(createProgramOptions);
  };
}

export function shouldProcessFile(options: LinterOptions, fileFsPath: string): boolean {
  if (!options.checkTsAndJs && (path.extname(fileFsPath) === EXTNAME_TS || path.extname(fileFsPath) === EXTNAME_JS)) {
    return false;
  }

  if (
    ARKTS_IGNORE_FILES.some((ignore) => {
      return path.basename(fileFsPath) === ignore;
    })
  ) {
    return false;
  }

  if (
    ARKTS_IGNORE_DIRS_NO_OH_MODULES.some((ignore) => {
      return pathContainsDirectory(path.resolve(fileFsPath), ignore);
    })
  ) {
    return false;
  }

  return (
    !pathContainsDirectory(path.resolve(fileFsPath), ARKTS_IGNORE_DIRS_OH_MODULES) ||
    !!options.isFileFromModuleCb?.(fileFsPath)
  );
}

function freeMemory(): void {
  clearPathHelperCache();
  clearCommonApiInfoCache();
}