/*
 * Copyright (c) 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 ts from 'typescript';
import { logger } from './compile_info';
import { harFilesRecord, GeneratedFileInHar, toUnixPath } from './utils';
import { resolveModuleNames as resolveModuleNamesOrig } from './ets_checker';

export function escapeRegExp(text: string): string {
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

export function splitLeadingTrivia(text: string): { leading: string; body: string } {
  let pos: number = 0;
  while (pos < text.length) {
    const wsMatch: RegExpMatchArray | null = text.substring(pos).match(/^\s+/);
    if (wsMatch) {
      pos += wsMatch[0].length; continue;
    }
    const blockMatch: RegExpMatchArray | null = text.substring(pos).match(/^\/\*[\s\S]*?\*\//);
    if (blockMatch) {
      pos += blockMatch[0].length; continue;
    }
    const lineMatch: RegExpMatchArray | null = text.substring(pos).match(/^\/\/.*$/m);
    if (lineMatch) {
      pos += lineMatch[0].length; continue;
    }
    break;
  }
  return { leading: text.substring(0, pos), body: text.substring(pos) };
}

export function isExternalModuleSpecifier(specifier: string): boolean {
  return !specifier.startsWith('.') && !specifier.startsWith('/');
}

export function isNameReferencedInText(name: string, text: string): boolean {
  return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(text);
}

export function isNamespaceReferencedInText(nsName: string, text: string): boolean {
  return new RegExp(`\\b${escapeRegExp(nsName)}\\.`).test(text);
}

export function findNamesMatchingPattern(
  names: string[], text: string, requireNamespaceDot: boolean
): string[] {
  return names.filter((name: string): boolean =>
    requireNamespaceDot ? isNamespaceReferencedInText(name, text) : isNameReferencedInText(name, text)
  );
}

export function sortImportLinesFirst(lines: string[]): string[] {
  const importLines: string[] = [];
  const declarationLines: string[] = [];
  for (const line of lines) {
    if (line.startsWith('import ')) {
      importLines.push(line);
    } else {
      declarationLines.push(line);
    }
  }
  return [...importLines, ...declarationLines];
}

// ─── AST Predicates ───

export function isStructDeclaration(node: ts.Node): boolean {
  return ts.isStructDeclaration ? ts.isStructDeclaration(node) : false;
}

export function isTypeParameterSymbol(symbol: ts.Symbol): boolean {
  return (symbol.flags & ts.SymbolFlags.TypeParameter) !== 0;
}

export function isEnumMemberSymbol(symbol: ts.Symbol): boolean {
  return (symbol.flags & ts.SymbolFlags.EnumMember) !== 0;
}

export function isAutoGeneratedHeritageExpression(expr: ts.Expression): boolean {
  if (ts.isObjectLiteralExpression(expr)) {
    return true;
  }
  if (ts.isIdentifier(expr) && expr.pos === expr.end) {
    return true;
  }
  return false;
}

export function isAutoGeneratedConstructor(member: ts.ClassElement): boolean {
  if (!ts.isConstructorDeclaration(member)) {
    return false;
  }
  if (member.getStart() === member.getEnd()) {
    return true;
  }
  if ((member as ts.ConstructorDeclaration).virtual === true) {
    return true;
  }
  if (member.parameters.length > 0 &&
    member.parameters.every((p: ts.ParameterDeclaration): boolean =>
      ts.isIdentifier(p.name) && p.name.text === '' && !!p.questionToken)) {
    return true;
  }
  return false;
}

export function isRightOfMatchingQualifiedName(n: ts.Identifier, emitName: string): boolean {
  if (!n.parent || !ts.isQualifiedName(n.parent) || n.parent.right !== n) {
    return false;
  }
  const dotIdx: number = emitName.indexOf('.');
  if (dotIdx < 0) {
    return false;
  }
  const nsPart: string = emitName.substring(0, dotIdx);
  const leftText: string = n.parent.left.getText(n.getSourceFile());
  return leftText === nsPart;
}

export function getLocalNameOfDeclaration(declaration: ts.Node): string {
  const named = declaration as ts.Node & { name?: ts.Node };
  if (named.name && ts.isIdentifier(named.name)) {
    return named.name.text;
  }
  return 'default';
}

export function findAncestorByKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node | null {
  let current: ts.Node | undefined = node.parent;
  while (current) {
    if (current.kind === kind) {
      return current;
    }
    if (ts.isSourceFile(current)) {
      break;
    }
    current = current.parent;
  }
  return null;
}

export function getDeclarationNode(node: ts.Node): ts.Node {
  if (ts.isVariableDeclaration(node)) {
    return findAncestorByKind(node, ts.SyntaxKind.VariableStatement) ?? node;
  }
  return node;
}

export function nodeHasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
  const decl: ts.Node = getDeclarationNode(node);
  if (!ts.canHaveModifiers(decl)) {
    return false;
  }
  const modifiers: readonly ts.ModifierLike[] | undefined = ts.getModifiers(decl);
  if (!modifiers) {
    return false;
  }
  return modifiers.some((m: ts.ModifierLike): boolean => m.kind === kind);
}

export function isTypeAliasLike(node: ts.Node): boolean {
  const decl: ts.Node = getDeclarationNode(node);
  return ts.isTypeAliasDeclaration(decl);
}

// ─── AST Transformation ───

export function applyModifiersToNode(
  node: ts.Node, newModifiers: readonly (ts.Decorator | ts.Modifier)[]
): ts.Node {
  const modArray = ts.factory.createNodeArray(newModifiers);
  if (isStructDeclaration(node)) {
    const n = node as ts.StructDeclaration;
    return ts.factory.updateStructDeclaration(n, modArray, n.name, n.typeParameters, n.heritageClauses, n.members);
  }
  if (ts.isClassDeclaration(node)) {
    return ts.factory.updateClassDeclaration(node, modArray, node.name, node.typeParameters, node.heritageClauses, node.members);
  }
  if (ts.isInterfaceDeclaration(node)) {
    return ts.factory.updateInterfaceDeclaration(node, modArray, node.name, node.typeParameters, node.heritageClauses, node.members);
  }
  if (ts.isEnumDeclaration(node)) {
    return ts.factory.updateEnumDeclaration(node, modArray, node.name, node.members);
  }
  if (ts.isTypeAliasDeclaration(node)) {
    return ts.factory.updateTypeAliasDeclaration(node, modArray, node.name, node.typeParameters, node.type);
  }
  if (ts.isFunctionDeclaration(node)) {
    return ts.factory.updateFunctionDeclaration(node, modArray, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body);
  }
  if (ts.isVariableStatement(node)) {
    return ts.factory.updateVariableStatement(node, modArray, node.declarationList);
  }
  if (ts.isModuleDeclaration(node)) {
    return ts.factory.updateModuleDeclaration(node, modArray, node.name, node.body);
  }
  return node;
}

export function withNamespaceMemberModifiers(node: ts.Node): ts.Node {
  const decorators: readonly ts.Decorator[] = ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : [];
  const modifiers: readonly ts.Modifier[] = ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : [];
  const keptModifiers: ts.Modifier[] = modifiers.filter((m: ts.Modifier): boolean =>
    m.kind !== ts.SyntaxKind.ExportKeyword &&
    m.kind !== ts.SyntaxKind.DeclareKeyword &&
    m.kind !== ts.SyntaxKind.DefaultKeyword
  );
  const newModifiers: readonly (ts.Decorator | ts.Modifier)[] = [...decorators, ts.factory.createModifier(ts.SyntaxKind.ExportKeyword), ...keptModifiers];
  return applyModifiersToNode(node, newModifiers);
}

export function transformToExportDeclare(text: string, node: ts.Node): string {
  const hasDeclare: boolean = nodeHasModifier(node, ts.SyntaxKind.DeclareKeyword);
  const hasDefault: boolean = nodeHasModifier(node, ts.SyntaxKind.DefaultKeyword);
  const { leading, body } = splitLeadingTrivia(text);
  let transformed: string = body;
  if (hasDefault) {
    transformed = transformed.replace(/^export\s+default\s+/, 'export declare ');
  } else {
    const hasExport: boolean = nodeHasModifier(node, ts.SyntaxKind.ExportKeyword);
    const isTypeAlias: boolean = isTypeAliasLike(node);
    if (isTypeAlias) {
      if (hasExport && hasDeclare) {
        transformed = transformed.replace(/^export\s+declare\s+/, 'export ');
      }
      else if (hasDeclare) {
        transformed = transformed.replace(/^declare\s+/, 'export ');
      }
      else if (!hasExport) {
        transformed = 'export ' + transformed;
      }
    } else if (hasExport && hasDeclare) {
      // keep as is
    } else if (hasExport) {
      transformed = transformed.replace(/^export\s+/, 'export declare ');
    } else if (hasDeclare) {
      transformed = transformed.replace(/^declare\s+/, 'export declare ');
    } else {
      transformed = 'export declare ' + transformed;
    }
  }
  return leading + transformed;
}

export function ensureDeclareKeyword(text: string, node: ts.Node): string {
  const { leading, body } = splitLeadingTrivia(text);
  if (/^declare\s/.test(body)) {
    return text;
  }
  return leading + 'declare ' + body;
}

export function stripExportFromText(text: string, node: ts.Node): string {
  const hasDeclare: boolean = nodeHasModifier(node, ts.SyntaxKind.DeclareKeyword);
  const hasDefault: boolean = nodeHasModifier(node, ts.SyntaxKind.DefaultKeyword);
  const { leading, body } = splitLeadingTrivia(text);
  let stripped: string = body;
  if (hasDefault) {
    stripped = stripped.replace(/^export\s+default\s+/, hasDeclare ? '' : 'declare ');
  } else {
    stripped = stripped.replace(/^export\s+/, hasDeclare ? '' : 'declare ');
  }
  return leading + stripped;
}

export function stripDefaultKeywordFromText(text: string, node: ts.Node): string {
  if (nodeHasModifier(node, ts.SyntaxKind.DefaultKeyword)) {
    const { leading, body } = splitLeadingTrivia(text);
    return leading + body.replace(/^export\s+default\s+/, 'export declare ');
  }
  return text;
}

export function filterAutoGeneratedHeritage(
  clauses: ts.NodeArray<ts.HeritageClause> | undefined
): ts.NodeArray<ts.HeritageClause> | undefined {
  if (!clauses) {
    return clauses;
  }
  const filtered: ts.HeritageClause[] = clauses.filter((clause: ts.HeritageClause): boolean => {
    if (clause.token !== ts.SyntaxKind.ExtendsKeyword) {
      return true;
    }
    return !clause.types.some((t: ts.ExpressionWithTypeArguments): boolean =>
      isAutoGeneratedHeritageExpression(t.expression));
  });
  if (filtered.length === clauses.length) {
    return clauses;
  }
  if (filtered.length === 0) {
    return undefined;
  }
  return ts.factory.createNodeArray(filtered);
}

export function sanitizeStructOrClass(node: ts.ClassLikeDeclaration): ts.Node {
  const filteredMembers: ts.ClassElement[] = node.members.filter(
    (m: ts.ClassElement): boolean => !isAutoGeneratedConstructor(m)
  );
  const filteredHeritage: ts.NodeArray<ts.HeritageClause> | undefined = filterAutoGeneratedHeritage(node.heritageClauses);
  if (isStructDeclaration(node)) {
    return ts.factory.updateStructDeclaration(node, node.modifiers, node.name, node.typeParameters,
      filteredHeritage, ts.factory.createNodeArray(filteredMembers));
  }
  return ts.factory.updateClassDeclaration(node, node.modifiers, node.name, node.typeParameters, filteredHeritage, ts.factory.createNodeArray(filteredMembers));
}

export function sanitizeDeclarationNode(node: ts.Node): ts.Node {
  if (isStructDeclaration(node) || ts.isClassDeclaration(node)) {
    return sanitizeStructOrClass(node);
  }
  if (ts.isModuleDeclaration(node) && node.body && ts.isModuleBlock(node.body)) {
    let changed: boolean = false;
    const sanitizedStmts: ts.Statement[] = node.body.statements.map(
      (stmt: ts.Statement): ts.Statement => {
        const sanitized: ts.Node = sanitizeDeclarationNode(stmt);
        if (sanitized !== stmt) {
          changed = true;
        }
        return sanitized as ts.Statement;
      }
    );
    if (changed) {
      const newBody: ts.ModuleBlock = ts.factory.updateModuleBlock(
        node.body, ts.factory.createNodeArray(sanitizedStmts)
      );
      return ts.factory.updateModuleDeclaration(
        node, node.modifiers, node.name, newBody
      );
    }
  }
  return node;
}

// ─── Module Specifier Utilities ───

export function findAncestorImportDeclaration(node: ts.Node): ts.ImportDeclaration | null {
  let current: ts.Node | undefined = node;
  while (current) {
    if (ts.isImportDeclaration(current)) {
      return current;
    }
    if (ts.isSourceFile(current)) {
      break;
    }
    current = current.parent;
  }
  return null;
}

export function extractModuleSpecifierFromExport(decl: ts.ExportSpecifier): string | null {
  if (!ts.isNamedExports(decl.parent)) {
    return null;
  }
  const exportDecl: ts.Node = decl.parent.parent;
  if (ts.isExportDeclaration(exportDecl) && exportDecl.moduleSpecifier && ts.isStringLiteral(exportDecl.moduleSpecifier)) {
    return exportDecl.moduleSpecifier.text;
  }
  return null;
}

export function extractModuleSpecifierFromImport(decl: ts.Declaration): string | null {
  const importDecl: ts.ImportDeclaration | null = findAncestorImportDeclaration(decl);
  if (importDecl && importDecl.moduleSpecifier && ts.isStringLiteral(importDecl.moduleSpecifier)) {
    return importDecl.moduleSpecifier.text;
  }
  return null;
}

export function extractModuleSpecifier(decl: ts.Declaration): string | null {
  if (ts.isExportSpecifier(decl)) {
    return extractModuleSpecifierFromExport(decl);
  }
  return extractModuleSpecifierFromImport(decl);
}

// ─── Validation ───

export function validateOutput(content: string, entryFile: string): void {
  if (!content || content.trim().length === 0) {
    return;
  }
  try {
    const isDets: boolean = entryFile.endsWith('.d.ets');
    const scriptKind: ts.ScriptKind = isDets
      ? (ts.ScriptKind as unknown as Record<string, number>).ETS ?? ts.ScriptKind.TS
      : ts.ScriptKind.TS;
    const sf: ts.SourceFile = ts.createSourceFile(entryFile, content, ts.ScriptTarget.Latest, true, scriptKind);
    if (sf.parseDiagnostics && sf.parseDiagnostics.length > 0) {
      const errors: string = sf.parseDiagnostics.map((d: ts.Diagnostic): string => {
        const msg: string = typeof d.messageText === 'string'
          ? d.messageText
          : (d.messageText as ts.DiagnosticMessageChain).messageText;
        return `line ${d.line + 1}: ${msg}`;
      }).join('; ');
      logger.debug(`Declaration merge output has parse errors in ${entryFile}: ${errors}`);
    }
  } catch (e) {
    const errMsg: string = e instanceof Error ? e.message : String(e);
    logger.debug(`Declaration merge output validation failed for ${entryFile}: ${errMsg}`);
  }
}

export function resolveWithFallback(
  moduleName: string,
  sourceContainingFile: string | null,
  resolved: ts.ResolvedModuleFull | null
): ts.ResolvedModuleFull | null {
  if (resolved) {
    return resolved;
  }
  if (!sourceContainingFile) {
    return null;
  }
  const fallback: (ts.ResolvedModuleFull | null)[] =
    resolveModuleNamesOrig([moduleName], sourceContainingFile);
  return fallback[0] ?? null;
}

export function buildBidirectionalMaps(): {
  sourceToDecl: Map<string, string>;
  declToSource: Map<string, string>;
} {
  const sourceToDecl: Map<string, string> = new Map();
  const declToSource: Map<string, string> = new Map();
  harFilesRecord.forEach((value: GeneratedFileInHar): void => {
    if (value.originalDeclarationCachePath) {
      sourceToDecl.set(toUnixPath(value.sourcePath), toUnixPath(value.originalDeclarationCachePath));
      declToSource.set(toUnixPath(value.originalDeclarationCachePath), toUnixPath(value.sourcePath));
    }
  });
  return { sourceToDecl, declToSource };
}

export function createDeclarationModuleResolver(
  projectPath: string
): (moduleNames: string[], containingFile: string) => (ts.ResolvedModuleFull | null)[] {
  const { sourceToDecl, declToSource } = buildBidirectionalMaps();

  return (
    moduleNames: string[],
    containingFile: string
  ): (ts.ResolvedModuleFull | null)[] => {
    const resolved: (ts.ResolvedModuleFull | null)[] =
      resolveModuleNamesOrig(moduleNames, containingFile);
    const sourceContainingFile: string | null =
      declToSource.get(toUnixPath(containingFile)) ?? null;

    for (let i = 0; i < resolved.length; i++) {
      resolved[i] = resolveWithFallback(moduleNames[i], sourceContainingFile, resolved[i]);
    }

    for (let i = 0; i < resolved.length; i++) {
      const mod = resolved[i];
      if (!mod?.resolvedFileName) {
        continue;
      }
      const declPath: string | undefined = sourceToDecl.get(toUnixPath(mod.resolvedFileName));
      if (declPath) {
        resolved[i] = {
          resolvedFileName: declPath,
          extension: declPath.endsWith('.d.ets') ? ts.Extension.Dets : ts.Extension.Dts,
        };
      }
    }
    return resolved;
  };
}

export enum EntityKind {
  Exported,
  TypeDependency,
  SystemApiImport,
  SystemApiReexport,
}

export type SourceFileExt = '.d.ets' | '.d.ts';

export interface MergeEntity {
  kind: EntityKind;
  symbol: ts.Symbol;
  declarations: ts.Declaration[];
  emitName: string;
  exportNames: string[];
  isDefaultExport: boolean;
  systemApiInfo?: { moduleName: string; name: string; statementText: string; exportStatementText?: string };
  isolationNamespace?: string;
  preferredName: string;
  sourceFileExt: SourceFileExt;
  aliases: string[];
  usageExts?: SourceFileExt[];
}

export interface NamespaceEmitItem {
  symbol: ts.Symbol;
  declarations: ts.Declaration[];
  emitName: string;
  aliases: string[];
}

export interface NamespaceBlockMember extends NamespaceEmitItem {
  sourceFileExt: SourceFileExt;
}

export interface NamespaceSystemApiMember {
  name: string;
  importStatement: string;
}

export interface NamespaceBlock {
  name: string;
  members: NamespaceBlockMember[];
  sourceFileExt: SourceFileExt;
  systemApiMembers: NamespaceSystemApiMember[];
}

export interface EmitContext {
  primaryLines: string[];
  companionLines: string[];
  emittedStatements: Set<string>;
  companionEmittedStatements: Set<string>;
  primaryEmittedTexts: Set<string>;
  companionEmittedTexts: Set<string>;
  crossExtExportedNames: string[];
  crossExtImportNamespaces: Set<string>;
  crossExtImportNames: string[];
  crossExtNamespaceImports: Set<string>;
  exportRenames: Array<{ emitName: string; exportName: string }>;
  primaryExt: SourceFileExt;
  companionModuleSpecifier: string;
}

export interface DeclarationMergeOptions {
  entryFile?: string;
  entryFiles?: string[];
  projectPath: string;
  isByteCodeHar: boolean;
  moduleRootPath?: string;
  packageDir?: string;
  systemModules?: string[];
  sdkPath?: string;
  resolveModuleNames?: (
    moduleNames: string[],
    containingFile: string
  ) => (ts.ResolvedModuleFull | null)[];
}