#!/usr/bin/env tsx

import path from 'path';
import { fileURLToPath } from 'url';
import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'fs/promises';
import {
  askConfirmation,
  createReadlineInterface,
  gitExec,
  gitOutput,
  isValidPath
} from './utils.ts';

export interface InjectionPlan {
  targetPath: string;
  isObsidianPlugin: boolean;
  hasPackageJson: boolean;
  hasManifest: boolean;
  hasScriptsFolder: boolean;
  currentDependencies: string[];
}

/**
 * Analyze the target plugin directory
 */
export async function analyzePlugin(pluginPath: string): Promise<InjectionPlan> {
  const packageJsonPath = path.join(pluginPath, 'package.json');
  const manifestPath = path.join(pluginPath, 'manifest.json');
  const scriptsPath = path.join(pluginPath, 'scripts');

  const plan: InjectionPlan = {
    targetPath: pluginPath,
    isObsidianPlugin: false,
    hasPackageJson: await isValidPath(packageJsonPath),
    hasManifest: await isValidPath(manifestPath),
    hasScriptsFolder: await isValidPath(scriptsPath),
    currentDependencies: []
  };

  if (plan.hasManifest) {
    try {
      const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
      plan.isObsidianPlugin = !!(manifest.id && manifest.name && manifest.version);
    } catch {
      console.warn('Warning: Could not parse manifest.json');
    }
  }

  if (plan.hasPackageJson) {
    try {
      const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
      plan.currentDependencies = [
        ...Object.keys(packageJson.dependencies || {}),
        ...Object.keys(packageJson.devDependencies || {})
      ];
    } catch {
      console.warn('Warning: Could not parse package.json');
    }
  }

  return plan;
}

/**
 * Find plugin-config root directory (handles NPM global installs)
 */
export async function findPluginConfigRoot(): Promise<string> {
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
  const npmPackageRoot = path.resolve(scriptDir, '..');
  const npmPackageJson = path.join(npmPackageRoot, 'package.json');

  if (await isValidPath(npmPackageJson)) {
    try {
      const packageContent = JSON.parse(await readFile(npmPackageJson, 'utf8'));
      if (packageContent.name === 'obsidian-plugin-config') {
        return npmPackageRoot;
      }
    } catch {
      // Ignore parsing errors
    }
  }

  return process.cwd();
}

/**
 * Copy file content from local plugin-config directory
 */
export async function copyFromLocal(filePath: string): Promise<string> {
  const configRoot = await findPluginConfigRoot();
  const sourcePath = path.join(configRoot, filePath);

  try {
    return await readFile(sourcePath, 'utf8');
  } catch (error) {
    throw new Error(`Failed to copy ${filePath}: ${error}`);
  }
}

/**
 * Check if plugin-config repo is clean and commit if needed
 */
export async function ensurePluginConfigClean(): Promise<void> {
  const configRoot = await findPluginConfigRoot();
  const gitDir = path.join(configRoot, '.git');

  // Skip git check if not a git repo
  // (e.g. NPM global install)
  if (!(await isValidPath(gitDir))) {
    console.log(`✅ Plugin-config repo is clean` + ` (NPM install, no git check)`);
    return;
  }

  try {
    const gitStatus = gitOutput('git status --porcelain', configRoot);

    if (gitStatus) {
      console.log(`\n⚠️  Plugin-config has uncommitted changes:`);
      console.log(gitStatus);
      console.log(`\n🔧 Auto-committing changes...`);

      const msg = '🔧 Update plugin-config templates';
      gitExec('git add -A', configRoot);
      gitExec(`git commit -m "${msg}"`, configRoot);

      const branch = gitOutput('git rev-parse --abbrev-ref HEAD', configRoot);

      try {
        gitExec(`git push origin ${branch}`, configRoot);
        console.log(`✅ Changes committed and pushed`);
      } catch {
        try {
          gitExec(`git push --set-upstream origin ${branch}`, configRoot);
          console.log(`✅ New branch pushed with upstream`);
        } catch {
          console.log(`⚠️  Committed locally, push failed`);
        }
      }
    } else {
      console.log(`✅ Plugin-config repo is clean`);
    }
  } catch (error) {
    console.error(`⚠️  Failed to check or commit plugin-config: ${error}`);
  }
}

/**
 * Display injection plan and ask for confirmation
 */
export async function showInjectionPlan(
  plan: InjectionPlan,
  autoConfirm: boolean = false
): Promise<boolean> {
  console.log(`\n🎯 Injection Plan for: ${plan.targetPath}`);
  console.log(`📁 Target: ${path.basename(plan.targetPath)}`);
  console.log(`📦 Package.json: ${plan.hasPackageJson ? '✅' : '❌'}`);
  console.log(`📋 Manifest.json: ${plan.hasManifest ? '✅' : '❌'}`);
  console.log(
    `📂 Scripts folder: ${plan.hasScriptsFolder ? '✅ (will be updated)' : '❌ (will be created)'}`
  );
  console.log(`🔌 Obsidian plugin: ${plan.isObsidianPlugin ? '✅' : '❌'}`);

  if (!plan.isObsidianPlugin) {
    console.log(`\n⚠️  Warning: This doesn't appear to be a valid Obsidian plugin`);
    console.log(`   Missing manifest.json or invalid structure`);
  }

  console.log(`\n📋 Will inject:`);
  console.log(
    `   ✅ Local scripts (esbuild.config.ts, utils.ts, env.ts, constants.ts, etc.)`
  );
  console.log(`   ✅ Updated package.json scripts`);
  console.log(`   ✅ Required dependencies`);

  if (autoConfirm) {
    console.log(`\n✅ Auto-confirming all file replacements...`);
    return true;
  }

  // No global confirmation needed - file-by-file confirmation will happen in diffAndPromptFiles
  return true;
}

/**
 * Clean old script files
 */
export async function cleanOldScripts(
  scriptsPath: string,
  approvedDests: Set<string>
): Promise<void> {
  const scriptNames = [
    'utils',
    'esbuild.config',
    'acp',
    'update-version',
    'release',
    'help',
    'constants',
    'env',
    'reload',
    'typingsPlugin'
  ];
  const extensions = ['.ts', '.mts', '.js', '.mjs'];

  for (const scriptName of scriptNames) {
    for (const ext of extensions) {
      const scriptFile = path.join(scriptsPath, `${scriptName}${ext}`);
      if (await isValidPath(scriptFile)) {
        if (approvedDests.has(scriptFile)) {
          await unlink(scriptFile);
          console.log(`🗑️  Removed existing ${scriptName}${ext} (will be replaced)`);
        }
      }
    }
  }

  const obsoleteRootFiles = ['help-plugin.ts'];
  for (const fileName of obsoleteRootFiles) {
    const filePath = path.join(path.dirname(scriptsPath), fileName);
    if (await isValidPath(filePath)) {
      await unlink(filePath);
      console.log(`🗑️  Removed obsolete root file: ${fileName}`);
    }
  }

  const obsoleteFiles = ['start.mjs', 'start.js'];
  for (const fileName of obsoleteFiles) {
    const filePath = path.join(scriptsPath, fileName);
    if (await isValidPath(filePath)) {
      await unlink(filePath);
      console.log(`🗑️  Removed obsolete file: ${fileName}`);
    }
  }
}

/**
 * Clean old ESLint config files
 */
export async function cleanOldLintFiles(targetPath: string): Promise<void> {
  const oldLintFiles = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintignore'];
  const conflictingLintFiles = [
    'eslint.config.ts',
    'eslint.config.cjs',
    'eslint.config.js',
    'eslint.config.mjs'
  ];

  for (const fileName of oldLintFiles) {
    const filePath = path.join(targetPath, fileName);
    if (await isValidPath(filePath)) {
      await unlink(filePath);
      console.log(
        `🗑️  Removed old ESLint file: ${fileName} (replaced by eslint.config.mts)`
      );
    }
  }

  for (const fileName of conflictingLintFiles) {
    const filePath = path.join(targetPath, fileName);
    if (await isValidPath(filePath)) {
      await unlink(filePath);
      console.log(
        `🗑️  Removed existing ESLint file: ${fileName} (will be replaced by injection)`
      );
    }
  }
}

interface FileEntry {
  src: string; // path relative to configRoot
  dest: string; // absolute path in target plugin
  mergeEnv?: boolean; // special .env merge logic
}

/**
 * Build the full list of files to inject, with source and destination paths
 */
function buildFileList(targetPath: string): FileEntry[] {
  const scriptsPath = path.join(targetPath, 'scripts');
  const entries: FileEntry[] = [];

  // Scripts
  const scriptFiles = [
    'templates/scripts/utils.ts',
    'templates/scripts/esbuild.config.ts',
    'templates/scripts/acp.ts',
    'templates/scripts/update-version.ts',
    'templates/scripts/release.ts',
    'templates/scripts/help.ts',
    'templates/scripts/constants.ts',
    'templates/scripts/env.ts',
    'templates/scripts/reload.ts',
    'templates/scripts/typingsPlugin.ts'
  ];
  for (const src of scriptFiles) {
    entries.push({
      src,
      dest: path.join(scriptsPath, path.basename(src))
    });
  }

  // Root config files
  const configFileMap: Array<[string, string, boolean?]> = [
    ['templates/tsconfig.json.template', 'tsconfig.json'],
    ['templates/gitignore.template', '.gitignore'],
    ['templates/eslint.config.mts', 'eslint.config.mts'],
    ['templates/.editorconfig', '.editorconfig'],
    ['templates/.prettierrc', '.prettierrc'],
    ['templates/.prettierignore', '.prettierignore'],
    ['templates/npmrc.template', '.npmrc'],
    ['templates/.gitattributes', '.gitattributes'],
    ['templates/env.template', '.env', true]
  ];
  for (const [src, destName, mergeEnv] of configFileMap) {
    entries.push({
      src,
      dest: path.join(targetPath, destName),
      mergeEnv: !!mergeEnv
    });
  }

  // VSCode config files
  const configVscodeMap: Array<[string, string]> = [
    ['templates/.vscode/settings.json', '.vscode/settings.json'],
    ['templates/.vscode/tasks.json', '.vscode/tasks.json'],
    ['templates/.vscode/extensions.json', '.vscode/extensions.json']
  ];
  for (const [src, destName] of configVscodeMap) {
    entries.push({
      src,
      dest: path.join(targetPath, destName)
    });
  }

  // GitHub workflow files
  const workflowFiles = [
    'templates/.github/workflows/release.yml',
    'templates/.github/workflows/release-body.md'
  ];
  for (const src of workflowFiles) {
    entries.push({
      src,
      dest: path.join(targetPath, src.replace('templates/', ''))
    });
  }

  return entries;
}

/**
 * Compare source templates with existing target files.
 * Prompt user only when content differs and file already exists.
 * Returns the Set of dest paths approved for injection.
 */
export async function diffAndPromptFiles(
  targetPath: string,
  autoConfirm: boolean
): Promise<Set<string>> {
  const rl = autoConfirm ? null : createReadlineInterface();
  const configRoot = await findPluginConfigRoot();
  const entries = buildFileList(targetPath);
  const approved = new Set<string>();

  console.log(`\n🔍 Comparing files with existing content...`);

  let hasChanges = false;

  for (const entry of entries) {
    // Skip .env merge (always approved, merge logic handled separately)
    if (entry.mergeEnv) {
      approved.add(entry.dest);
      continue;
    }

    const srcPath = path.join(configRoot, entry.src);
    let srcContent: string;
    try {
      srcContent = await readFile(srcPath, 'utf8');
    } catch {
      // Source doesn't exist, skip
      continue;
    }

    // Target doesn't exist yet → inject without prompting
    if (!(await isValidPath(entry.dest))) {
      approved.add(entry.dest);
      continue;
    }

    // Special case: eslint.config.mts - auto-approve if old .eslintrc exists
    if (entry.dest.endsWith('eslint.config.mts')) {
      const oldEslintFiles = [
        '.eslintrc',
        '.eslintrc.js',
        '.eslintrc.json',
        '.eslintrc.cjs'
      ];
      let hasOldEslint = false;
      for (const file of oldEslintFiles) {
        if (await isValidPath(path.join(targetPath, file))) {
          hasOldEslint = true;
          break;
        }
      }
      if (hasOldEslint) {
        console.log(
          `   🔄 ${path.relative(targetPath, entry.dest).replace(/\\/g, '/')} (migrating from old .eslintrc format)`
        );
        approved.add(entry.dest);
        continue;
      }
    }

    const destContent = await readFile(entry.dest, 'utf8');

    // Identical → skip silently
    if (srcContent === destContent) {
      console.log(
        `   ✅ ${path.relative(targetPath, entry.dest).replace(/\\/g, '/')} (unchanged)`
      );
      continue;
    }

    // Different → ask user (or auto-approve if autoConfirm)
    hasChanges = true;
    const relDest = path.relative(targetPath, entry.dest);

    if (autoConfirm) {
      console.log(`   ✅ ${relDest.replace(/\\/g, '/')} (will be updated)`);
      approved.add(entry.dest);
    } else {
      const update = await askConfirmation(
        `   Update ${relDest.replace(/\\/g, '/')}? (content differs)`,
        rl!
      );
      if (update) {
        approved.add(entry.dest);
      } else {
        console.log(`   ⏭️  Kept existing ${relDest.replace(/\\/g, '/')}`);
      }
    }
  }

  if (!hasChanges) {
    console.log(`   ✅ All existing files are up to date`);
  }

  if (rl) rl.close();
  return approved;
}

/**
 * Inject scripts and config files
 */
export async function injectScripts(
  targetPath: string,
  approvedDests: Set<string>
): Promise<void> {
  const scriptsPath = path.join(targetPath, 'scripts');

  if (!(await isValidPath(scriptsPath))) {
    await mkdir(scriptsPath, { recursive: true });
    console.log(`📁 Created scripts directory`);
  }

  await cleanOldScripts(scriptsPath, approvedDests);
  await cleanOldLintFiles(targetPath);

  const scriptFiles = [
    'templates/scripts/utils.ts',
    'templates/scripts/esbuild.config.ts',
    'templates/scripts/acp.ts',
    'templates/scripts/update-version.ts',
    'templates/scripts/release.ts',
    'templates/scripts/help.ts',
    'templates/scripts/constants.ts',
    'templates/scripts/env.ts',
    'templates/scripts/reload.ts',
    'templates/scripts/typingsPlugin.ts'
  ];

  // Files that need value-preserving merge instead
  // of full overwrite (user fills in their paths)
  const mergeEnvFile = new Set(['.env']);

  // Files with .template suffix (NPM excludes dotfiles)
  // Map: { source: targetName }
  const configFileMap: Record<string, string> = {
    'templates/tsconfig.json.template': 'tsconfig.json',
    'templates/gitignore.template': '.gitignore',
    'templates/eslint.config.mts': 'eslint.config.mts',
    'templates/.editorconfig': '.editorconfig',
    'templates/.prettierrc': '.prettierrc',
    'templates/.prettierignore': '.prettierignore',
    'templates/npmrc.template': '.npmrc',
    'templates/.gitattributes': '.gitattributes',
    'templates/env.template': '.env'
  };

  const configVscodeMap: Record<string, string> = {
    'templates/.vscode/settings.json': '.vscode/settings.json',
    'templates/.vscode/tasks.json': '.vscode/tasks.json',
    'templates/.vscode/extensions.json': '.vscode/extensions.json'
  };

  const workflowFiles = [
    'templates/.github/workflows/release.yml',
    'templates/.github/workflows/release-body.md'
  ];

  console.log(`\n📥 Copying scripts from local files...`);

  for (const scriptFile of scriptFiles) {
    try {
      const fileName = path.basename(scriptFile);
      const targetFile = path.join(scriptsPath, fileName);
      if (!approvedDests.has(targetFile)) {
        console.log(`   ⏭️  Skipped ${fileName} (kept existing)`);
        continue;
      }
      const content = await copyFromLocal(scriptFile);
      await writeFile(targetFile, content, 'utf8');
      console.log(`   ✅ ${fileName}`);
    } catch (error) {
      console.error(`   ❌ Failed to inject ${scriptFile}: ${error}`);
    }
  }

  console.log(`\n📥 Copying config files...`);

  // Copy root config files
  for (const [src, destName] of Object.entries(configFileMap)) {
    // Skip if not approved by diff step
    const targetFile = path.join(targetPath, destName);
    if (!approvedDests.has(targetFile)) {
      continue; // already logged during diff step
    }

    try {
      const templateContent = await copyFromLocal(src);

      // For .env: merge existing values into the template
      if (mergeEnvFile.has(destName) && (await isValidPath(targetFile))) {
        const existing = await readFile(targetFile, 'utf8');
        const existingVals: Record<string, string> = {};
        for (const line of existing.split(/\r?\n/)) {
          const m = line.match(/^([^#=]+)=(.*)$/);
          if (m) existingVals[m[1].trim()] = m[2].trim();
        }
        const merged = templateContent
          .split(/\r?\n/)
          .map((line) => {
            const m = line.match(/^([^#=]+)=(.*)$/);
            if (m) {
              const key = m[1].trim();
              const val = existingVals[key] ?? m[2].trim();
              return `${key}=${val}`;
            }
            return line;
          })
          .join('\n');
        await writeFile(targetFile, merged, 'utf8');
        console.log(`   ✅ ${destName} (values preserved)`);
        continue;
      }

      await writeFile(targetFile, templateContent, 'utf8');
      console.log(`   ✅ ${destName}`);
    } catch (error) {
      console.error(`   ❌ Failed to inject ${destName}: ${error}`);
    }
  }

  // Copy .vscode config files
  for (const [src, destName] of Object.entries(configVscodeMap)) {
    try {
      const targetFile = path.join(targetPath, destName);
      if (!approvedDests.has(targetFile)) continue;
      const content = await copyFromLocal(src);
      const targetDir = path.dirname(targetFile);
      if (!(await isValidPath(targetDir))) {
        await mkdir(targetDir, { recursive: true });
      }
      await writeFile(targetFile, content, 'utf8');
      console.log(`   ✅ ${destName}`);
    } catch (error) {
      console.error(`   ❌ Failed to inject ${destName}: ${error}`);
    }
  }

  console.log(`\n📥 Copying GitHub workflows from local files...`);
  for (const workflowFile of workflowFiles) {
    try {
      const content = await copyFromLocal(workflowFile);
      const relativePath = workflowFile.replace('templates/', '');
      const targetFile = path.join(targetPath, relativePath);
      if (!approvedDests.has(targetFile)) continue;
      const targetDir = path.dirname(targetFile);
      if (!(await isValidPath(targetDir))) {
        await mkdir(targetDir, { recursive: true });
      }
      await writeFile(targetFile, content, 'utf8');
      console.log(`   ✅ ${relativePath}`);
    } catch (error) {
      console.error(`   ❌ Failed to inject ${workflowFile}: ${error}`);
    }
  }
}

/**
 * Update package.json with autonomous configuration
 */
export async function updatePackageJson(targetPath: string): Promise<void> {
  const packageJsonPath = path.join(targetPath, 'package.json');

  if (!(await isValidPath(packageJsonPath))) {
    console.log(`❌ No package.json found, skipping package.json update`);
    return;
  }

  try {
    const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));

    const configRoot = await findPluginConfigRoot();
    const templatePkg = JSON.parse(
      await readFile(path.join(configRoot, 'templates/package.json.template'), 'utf8')
    );

    const obsoleteScripts = ['version'];
    for (const script of obsoleteScripts) {
      if (packageJson.scripts?.[script]) {
        console.log(`   🧹 Removing obsolete script: "${script}"`);
        delete packageJson.scripts[script];
      }
    }

    packageJson.scripts = {
      ...packageJson.scripts,
      ...templatePkg.scripts
    };

    if (!packageJson.devDependencies) packageJson.devDependencies = {};

    const requiredDeps: Record<string, string> = templatePkg.devDependencies;

    let addedDeps = 0;
    let updatedDeps = 0;
    for (const [dep, version] of Object.entries(requiredDeps)) {
      if (!packageJson.devDependencies[dep]) {
        packageJson.devDependencies[dep] = version as string;
        addedDeps++;
      } else if (packageJson.devDependencies[dep] !== version) {
        packageJson.devDependencies[dep] = version as string;
        updatedDeps++;
      }
    }

    if (!packageJson.engines) packageJson.engines = {};
    packageJson.engines.npm = templatePkg.engines.npm;
    packageJson.engines.yarn = templatePkg.engines.yarn;
    packageJson.type = templatePkg.type;

    await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
    console.log(
      `   ✅ Updated package.json (${addedDeps} new, ${updatedDeps} updated dependencies)`
    );
  } catch (error) {
    console.error(`   ❌ Failed to update package.json: ${error}`);
  }
}

/**
 * Create required directories
 */
export async function createRequiredDirectories(targetPath: string): Promise<void> {
  const directories = [path.join(targetPath, '.github', 'workflows')];
  for (const dir of directories) {
    if (!(await isValidPath(dir))) {
      await mkdir(dir, { recursive: true });
      console.log(`   📁 Created ${path.relative(targetPath, dir).replace(/\\/g, '/')}`);
    }
  }
}

/**
 * Create injection info file
 */
export async function createInjectionInfo(targetPath: string): Promise<void> {
  const configRoot = await findPluginConfigRoot();
  const configPackageJsonPath = path.join(configRoot, 'package.json');
  let injectorVersion = 'unknown';
  try {
    const configPackageJson = JSON.parse(await readFile(configPackageJsonPath, 'utf8'));
    injectorVersion = configPackageJson.version || 'unknown';
  } catch {
    console.warn('Warning: Could not read injector version');
  }
  const injectionInfo = {
    injectorVersion,
    injectionDate: new Date().toISOString(),
    injectorName: 'obsidian-plugin-config'
  };
  const infoPath = path.join(targetPath, '.injection-info.json');
  await writeFile(infoPath, JSON.stringify(injectionInfo, null, 2));
  console.log(`   ✅ Created injection info file (.injection-info.json)`);
}

/**
 * Read injection info from target plugin
 */
export async function readInjectionInfo(
  targetPath: string
): Promise<Record<string, string> | null> {
  const infoPath = path.join(targetPath, '.injection-info.json');
  if (!(await isValidPath(infoPath))) return null;
  try {
    return JSON.parse(await readFile(infoPath, 'utf8'));
  } catch {
    console.warn('Warning: Could not parse .injection-info.json');
    return null;
  }
}

/**
 * Clean NPM/Yarn lock files and node_modules to ensure fresh install
 */
export async function cleanNpmArtifactsIfNeeded(targetPath: string): Promise<void> {
  const packageLockPath = path.join(targetPath, 'package-lock.json');
  const yarnLockPath = path.join(targetPath, 'yarn.lock');
  const nodeModulesPath = path.join(targetPath, 'node_modules');

  const hasPackageLock = await isValidPath(packageLockPath);
  const hasYarnLock = await isValidPath(yarnLockPath);

  if (hasPackageLock) {
    console.log(`\n🧹 Cleaning NPM artifacts (migrating to Yarn)...`);
    try {
      if (await isValidPath(nodeModulesPath)) {
        console.log(`   ⏳ Removing node_modules (this may take a moment)...`);
        try {
          await rm(nodeModulesPath, { recursive: true, force: true });
        } catch {
          // ignore
        }
        if (await isValidPath(nodeModulesPath)) {
          const timestamp = Date.now();
          const oldPath = `${nodeModulesPath}.old.${timestamp}`;
          try {
            await rename(nodeModulesPath, oldPath);
            console.log(`   🔄 Renamed locked node_modules to ${path.basename(oldPath)}`);
            console.log(`   💡 Delete it manually later: ${oldPath}`);
          } catch {
            console.log(
              `   ⚠️  Could not remove/rename node_modules (locked by processes)`
            );
            console.log(`   💡 Close Obsidian/VSCode and run: obsidian-inject again`);
            throw new Error('node_modules locked - close processes and retry');
          }
        } else {
          console.log(`   🗑️  Removed node_modules (will be reinstalled with Yarn)`);
        }
      }
      if (hasPackageLock) {
        await unlink(packageLockPath);
        console.log(`   🗑️  Removed package-lock.json`);
      }
      if (hasYarnLock) {
        await unlink(yarnLockPath);
        console.log(`   🗑️  Removed yarn.lock`);
      }
      console.log(`   ✅ Lock files and artifacts cleaned for fresh install`);
    } catch (error) {
      if (error instanceof Error && error.message.includes('locked')) throw error;
      console.error(`   ❌ Failed to clean artifacts: ${error}`);
      console.log(
        `   💡 You may need to manually remove package-lock.json, yarn.lock and node_modules`
      );
    }
  }
}

/**
 * Check if tsx is installed locally and install it if needed
 */
export async function ensureTsxInstalled(targetPath: string): Promise<void> {
  console.log(`\n🔍 Checking tsx installation...`);
  const packageJsonPath = path.join(targetPath, 'package.json');
  try {
    const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
    const devDependencies = packageJson.devDependencies || {};
    const dependencies = packageJson.dependencies || {};
    if (devDependencies.tsx || dependencies.tsx) {
      console.log(`   ✅ tsx is already installed`);
      return;
    }
    console.log(`   ⚠️  tsx not found, installing as dev dependency...`);
    gitExec('yarn add -D tsx', targetPath);
    console.log(`   ✅ tsx installed successfully`);
  } catch (error) {
    console.error(`   ❌ Failed to install tsx: ${error}`);
    console.log(`   💡 You may need to install tsx manually: yarn add -D tsx`);
    throw new Error('tsx installation failed');
  }
}

/**
 * Run yarn install in target directory
 */
export async function runYarnInstall(targetPath: string): Promise<void> {
  console.log(`\n📦 Installing dependencies...`);
  try {
    gitExec('yarn install', targetPath);
    console.log(`   ✅ Dependencies installed successfully`);
  } catch (error) {
    console.error(`   ❌ Failed to install dependencies: ${error}`);
    console.log(
      `   💡 You may need to run 'yarn install' manually in the target directory`
    );
  }
}

/**
 * Main injection orchestration function
 */
export async function performInjection(
  targetPath: string,
  autoConfirm: boolean = false
): Promise<void> {
  console.log(`\n🚀 Starting injection process...`);
  try {
    const approvedDests = await diffAndPromptFiles(targetPath, autoConfirm);
    await cleanNpmArtifactsIfNeeded(targetPath);
    await ensureTsxInstalled(targetPath);
    await injectScripts(targetPath, approvedDests);

    console.log(`\n📦 Updating package.json...`);
    await updatePackageJson(targetPath);

    console.log(`\n📁 Creating required directories...`);
    await createRequiredDirectories(targetPath);

    await runYarnInstall(targetPath);

    console.log(`\n📝 Creating injection info...`);
    await createInjectionInfo(targetPath);

    console.log(`\n✅ Injection completed successfully!`);
    console.log(`\n📋 Next steps:`);
    console.log(`   1. cd ${targetPath}`);
    console.log(`   2. yarn build    # Test the build`);
    console.log(`   3. yarn start    # Test development mode`);
    console.log(`   4. yarn acp      # Commit changes (or yarn bacp for build+commit)`);

    const allEntries = await readdir(targetPath);
    const oldDirs = allEntries
      .filter((name) => name.startsWith('node_modules.old.'))
      .map((name) => path.basename(name));

    if (oldDirs.length > 0) {
      console.log(`\n🧹 Cleanup reminder:`);
      for (const oldDir of oldDirs) {
        console.log(`   🗑️  Delete manually: ${oldDir}`);
      }
      console.log(`   💡 Close all processes first, then delete these folders`);
    }
  } catch (error) {
    console.error(`\n❌ Injection failed: ${error}`);
    throw error;
  }
}
