'use strict';

const { exec } = require('node:child_process');
const { promisify } = require('node:util');
const path = require('node:path');
const fs = require('fs-extra');
const pkg = require('../../package.json');
const kleur = require('kleur');
const ui = require('../utils/ui');
const { printCompactHeader } = require('../utils/brand');
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
const {
  AVAILABLE_FEATURES,
  normalizeFeature,
  getVisibleFeatures,
} = require('../scaffold/catalog');

const KASY_AUDIENCE = process.env.KASY_INTERNAL === '1' ? 'internal' : 'public';
const {
  writeRouter,
  writeNoOpAnalyticsApi,
  writeNoOpTrackingApi,
  writeNoOpAdminHomeWidgets,
  writeNoOpFeatureRequestRepository,
  writeMainDart,
  writeNoOpSubscriptionStubs,
  writeNoOpSentryUsages,
} = require('../scaffold/shared/generator-utils');
const { toPackageName } = require('../scaffold/backends/firebase/tokens');

const execAsync = promisify(exec);

// ── Module metadata ──────────────────────────────────────────────────────────

/**
 * Pubspec deps to remove per module when it is removed.
 * Note: sentry_flutter is shared with revenuecat/facebook and handled separately.
 */
const MODULE_DEPS = {
  analytics:  ['mixpanel_flutter'],
  facebook:   ['facebook_app_events', 'flutter_facebook_auth'],
  widget:     ['home_widget', 'background_fetch'],
  revenuecat: ['purchases_flutter'],
};

/**
 * Dart-define keys to remove from launch.json + Makefile per module.
 */
const MODULE_DEFINES = {
  sentry:     ['SENTRY_DSN'],
  analytics:  ['MIXPANEL_TOKEN'],
  revenuecat: ['RC_ANDROID_API_KEY', 'RC_IOS_API_KEY', 'RC_WEB_API_KEY'],
  llm_chat:   ['LLM_CHAT_ENDPOINT'],
};

/**
 * .env.example variable names to remove per module. We list both the new
 * test/prod split (RC_TEST_KEY / RC_IOS_PROD_KEY / RC_ANDROID_PROD_KEY) and
 * the legacy single-key vars so removal works on any project age.
 */
const MODULE_ENV_KEYS = {
  sentry:     ['SENTRY_DSN'],
  analytics:  ['MIXPANEL_TOKEN'],
  revenuecat: [
    'RC_TEST_KEY',
    'RC_IOS_PROD_KEY',
    'RC_ANDROID_PROD_KEY',
    'RC_WEB_API_KEY',
    // Legacy single-key format — only present in projects generated before
    // the test/prod split. Listed here so removal cleans them up too.
    'RC_ANDROID_API_KEY',
    'RC_IOS_API_KEY',
  ],
};

/**
 * Feature flags in features.dart per module (null = no flag).
 */
const MODULE_FEATURE_FLAGS = {
  onboarding: 'withOnboarding',
  web:        'withWeb',
  feedback:   'withFeedback',
  llm_chat:   'withLlmChat',
};

// ── Helpers ──────────────────────────────────────────────────────────────────

/**
 * Reconstruct active modules from kit_setup.json + features.dart + pubspec.yaml.
 * Extended version that also detects widget (not tracked in kit_setup).
 */
async function getActiveModules(kitSetup, projectDir) {
  const modules = [];
  if (kitSetup.useSentry) modules.push('sentry');
  if (kitSetup.analyticsProvider === 'mixpanel') modules.push('analytics');
  if (kitSetup.withFacebookPixel) modules.push('facebook');
  if (kitSetup.subscriptionModule) modules.push('revenuecat');
  if (kitSetup.withOnboarding) modules.push('onboarding');
  if (kitSetup.webCompat) modules.push('web');
  if (kitSetup.feedbackModule) modules.push('feedback');

  const featuresPath = path.join(projectDir, 'lib', 'core', 'config', 'features.dart');
  if (await fs.pathExists(featuresPath)) {
    const content = await fs.readFile(featuresPath, 'utf8');
    if (/const bool withLlmChat\s*=\s*true/.test(content)) modules.push('llm_chat');
  }

  if (await fs.pathExists(path.join(projectDir, '.github', 'workflows'))) modules.push('ci');

  // widget is not tracked in kit_setup.json — detect via pubspec.yaml
  const pubspecPath = path.join(projectDir, 'pubspec.yaml');
  if (await fs.pathExists(pubspecPath)) {
    const pubspec = await fs.readFile(pubspecPath, 'utf8');
    if (/^\s*home_widget\s*:/m.test(pubspec)) modules.push('widget');
  }

  return modules;
}

/**
 * Set a feature flag to false in features.dart. Only touches the specific flag.
 */
async function disableFeatureFlag(projectDir, flag) {
  const featuresPath = path.join(projectDir, 'lib', 'core', 'config', 'features.dart');
  if (!(await fs.pathExists(featuresPath))) return;
  let content = await fs.readFile(featuresPath, 'utf8');
  content = content.replace(
    new RegExp(`(const bool ${flag}\\s*=\\s*)true`, 'g'),
    '$1false'
  );
  await fs.outputFile(featuresPath, content, 'utf8');
}

/**
 * Remove specific dart-define entries from .vscode/launch.json args.
 */
async function removeDartDefinesFromLaunchJson(projectDir, keys) {
  if (!keys?.length) return;
  const launchPath = path.join(projectDir, '.vscode', 'launch.json');
  if (!(await fs.pathExists(launchPath))) return;

  const launch = await fs.readJson(launchPath);
  for (const config of launch.configurations || []) {
    if (!Array.isArray(config.args)) continue;
    config.args = config.args.filter(
      (arg) => !keys.some((key) => arg.startsWith(`--dart-define=${key}=`))
    );
  }
  await fs.outputFile(launchPath, JSON.stringify(launch, null, 2) + '\n', 'utf8');
}

/**
 * Remove specific dart-define lines from the project Makefile.
 * Repairs continuation backslashes after removal so `make` stays valid.
 */
async function removeDartDefinesFromMakefile(projectDir, keys) {
  if (!keys?.length) return;
  const makePath = path.join(projectDir, 'Makefile');
  if (!(await fs.pathExists(makePath))) return;

  let content = await fs.readFile(makePath, 'utf8');

  // Skip early if none of the keys are present
  if (!keys.some((k) => content.includes(`--dart-define=${k}=`))) return;

  // Remove each define line (leading whitespace + value + optional trailing backslash)
  for (const key of keys) {
    content = content.replace(
      new RegExp(`^[ \\t]*--dart-define=${key}=[^\\n]*\\n`, 'gm'),
      ''
    );
  }

  // Repair orphaned trailing backslashes:
  // A line ending with " \" followed immediately by a blank line or a top-level
  // directive is now the last define — strip the backslash.
  content = content.replace(/ \\\n(\n)/g, '\n$1');
  content = content.replace(/ \\\n([a-zA-Z#.])/g, '\n$1');

  // Remove empty DEFINES block left behind if all defines were removed
  content = content.replace(/^DEFINES\s*:=\s*\\\s*\n(\s*\n)*/m, '');
  content = content.replace(/^DEFINES\s*:=\s*\n(\s*\n)*/m, '');

  await fs.outputFile(makePath, content, 'utf8');
}

/**
 * Remove specific variable lines from a .env* file.
 */
async function removeEnvLines(projectDir, fileName, keys) {
  if (!keys?.length) return;
  const envPath = path.join(projectDir, fileName);
  if (!(await fs.pathExists(envPath))) return;

  let content = await fs.readFile(envPath, 'utf8');
  for (const key of keys) {
    content = content.replace(new RegExp(`^${key}=[^\\n]*\\n?`, 'gm'), '');
  }
  // Collapse runs of 3+ blank lines into 2
  content = content.replace(/\n{3,}/g, '\n\n');
  await fs.outputFile(envPath, content, 'utf8');
}

/**
 * Remove a single dependency from pubspec.yaml by name.
 */
async function removePubspecDep(projectDir, depName) {
  const pubspecPath = path.join(projectDir, 'pubspec.yaml');
  if (!(await fs.pathExists(pubspecPath))) return;
  let content = await fs.readFile(pubspecPath, 'utf8');
  content = content.replace(new RegExp(`^[ \\t]*${depName}:.*\\n`, 'gm'), '');
  await fs.outputFile(pubspecPath, content, 'utf8');
}

/**
 * Revert kit_setup.json flags for the removed module.
 */
function revertKitSetupFlag(config, module) {
  switch (module) {
    case 'sentry':
      config.useSentry = false;
      break;
    case 'analytics':
      config.analyticsProvider = null;
      break;
    case 'facebook':
      config.withFacebookPixel = false;
      break;
    case 'revenuecat':
      config.subscriptionModule = false;
      config.revenuecatWeb = false;
      break;
    case 'onboarding':
      config.withOnboarding = false;
      break;
    case 'web':
      config.webCompat = false;
      break;
    case 'feedback':
      config.feedbackModule = false;
      break;
    case 'llm_chat':
    case 'widget':
    case 'ci':
      // Controlled by features.dart / patch — no kit_setup flag to revert
      break;
    default:
      break;
  }
}

/**
 * Remove files and directories that were copied by the module's patch.
 * For modules whose stubs are regenerated (analytics, facebook, sentry),
 * no deletion is needed — the stub writer overwrites the file in-place.
 */
async function removeModuleFiles(projectDir, module, backend) {
  // Modules that have dedicated directories to remove
  const dirRemovals = {
    llm_chat:   [path.join('lib', 'features', 'llm_chat')],
    onboarding: [path.join('lib', 'features', 'onboarding')],
    widget:     [path.join('lib', 'core', 'home_widgets')],
    revenuecat: [path.join('lib', 'features', 'subscription')],
    // feedback: handled below (dir deleted, no-op stub recreated afterward)
    // ci: handled below (multiple top-level paths)
    // analytics/facebook/sentry: no dir — stub overwrites the file in place
  };

  if (module === 'feedback') {
    // Delete the full feedbacks dir; writeNoOpFeatureRequestRepository recreates
    // only the repository stub (the rest of the UI/api code is gone).
    const fullPath = path.join(projectDir, 'lib', 'features', 'feedbacks');
    if (await fs.pathExists(fullPath)) await fs.remove(fullPath);
    return;
  }

  if (module === 'ci') {
    // Remove only the specific CI paths added by the patch.
    // .github/ root is NOT deleted — the user may have CODEOWNERS, ISSUE_TEMPLATE, etc.
    // Only .github/workflows/ (added by Kasy) is removed.
    const ciPaths = [
      path.join(projectDir, '.github', 'workflows'),
      path.join(projectDir, '.gitlab'),
      path.join(projectDir, '.gitlab-ci.yml'),
      path.join(projectDir, 'codemagic.yaml'),
    ];
    for (const p of ciPaths) {
      if (await fs.pathExists(p)) await fs.remove(p);
    }
    return;
  }

  for (const relDir of dirRemovals[module] || []) {
    const fullPath = path.join(projectDir, relDir);
    if (await fs.pathExists(fullPath)) await fs.remove(fullPath);
  }

  // LLM Chat on Supabase: also remove the copied edge function
  if (module === 'llm_chat' && backend === 'supabase') {
    const edgeFnPath = path.join(projectDir, 'supabase', 'functions', 'llm-chat');
    if (await fs.pathExists(edgeFnPath)) await fs.remove(edgeFnPath);
  }
}

// ── Main command ─────────────────────────────────────────────────────────────

async function runRemove(module, options = {}) {
  const t = createTranslator(options.language || detectDefaultLanguage());
  const projectDir = path.resolve(options.directory || '.');

  if (!module) {
    printCompactHeader(t);
    ui.log.warn(t('remove.error.noModule'));
    return;
  }

  // 1. Must be inside a kasy project
  const kitSetupPath = path.join(projectDir, 'kit_setup.json');
  if (!(await fs.pathExists(kitSetupPath))) {
    throw new Error(t('remove.error.notKasyProject'));
  }

  const kitSetup = await fs.readJson(kitSetupPath);

  // 2. Validate module name
  const normalized = normalizeFeature(module);
  if (!normalized) {
    throw new Error(
      t('remove.error.unknownModule', {
        module,
        list: getVisibleFeatures({ audience: KASY_AUDIENCE }).map((f) => f.id).join(', '),
      })
    );
  }

  // 3. Check that the module is actually active
  const activeModules = await getActiveModules(kitSetup, projectDir);
  if (!activeModules.includes(normalized)) {
    printCompactHeader(t);
    ui.log.warn(t('remove.error.notActive', { module: normalized }));
    return;
  }

  printCompactHeader(t);
  ui.intro(t('remove.removing', { module: normalized }));
  const cancel = () => { ui.cancel(t('remove.cancelled')); process.exit(0); };

  // 4. Confirm before destructive action (skip with --yes)
  if (!options.yes) {
    const confirmed = await ui.confirm({
      message: t('remove.confirm', { module: normalized }),
      initialValue: false,
      onCancel: cancel,
    });
    if (!confirmed) {
      ui.outro(t('remove.cancelled'));
      return;
    }
  }

  // Build the module list that will be active AFTER removal (used for stub regen)
  const modulesAfterRemoval = activeModules.filter((m) => m !== normalized);

  // 5. Disable feature flag in features.dart (true → false)
  const featureFlag = MODULE_FEATURE_FLAGS[normalized];
  if (featureFlag) {
    await disableFeatureFlag(projectDir, featureFlag);
  }

  // 6. Remove dart-defines from .vscode/launch.json and Makefile
  const defineKeys = MODULE_DEFINES[normalized] || [];
  if (defineKeys.length > 0) {
    await removeDartDefinesFromLaunchJson(projectDir, defineKeys);
    await removeDartDefinesFromMakefile(projectDir, defineKeys);
  }

  // 7. Remove lines from .env.example and .env
  const envKeys = MODULE_ENV_KEYS[normalized] || [];
  if (envKeys.length > 0) {
    await removeEnvLines(projectDir, '.env.example', envKeys);
    await removeEnvLines(projectDir, '.env', envKeys);
  }

  // 8. Revert kit_setup.json flags
  revertKitSetupFlag(kitSetup, normalized);
  kitSetup.cliVersion = pkg.version;
  await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');

  // 9. Remove pubspec dependencies
  for (const dep of MODULE_DEPS[normalized] || []) {
    await removePubspecDep(projectDir, dep);
  }

  // 9b. Sentry shared-dep handling:
  //   sentry_flutter is also used by revenuecat and facebook (for tracking_api).
  //   Only remove it (and patch core files) when none of the three are active afterward.
  const sentryStillNeeded = modulesAfterRemoval.some((m) =>
    ['sentry', 'revenuecat', 'facebook'].includes(m)
  );
  if (['sentry', 'revenuecat', 'facebook'].includes(normalized) && !sentryStillNeeded) {
    await removePubspecDep(projectDir, 'sentry_flutter');
    await writeNoOpSentryUsages(projectDir).catch(() => {});
  } else if (normalized === 'sentry' && sentryStillNeeded) {
    ui.log.message(kleur.dim(t('remove.warn.sentry.shared')));
  }

  // 10. Remove files and directories added by the module's patch
  await removeModuleFiles(projectDir, normalized, kitSetup.backendProvider);

  // 11. Regenerate no-op stubs for the removed module
  const packageName = toPackageName(kitSetup.appName);

  if (normalized === 'analytics') {
    await writeNoOpAnalyticsApi(projectDir, packageName).catch(() => {});
  }
  if (normalized === 'facebook') {
    await writeNoOpTrackingApi(projectDir, packageName).catch(() => {});
  }
  if (normalized === 'feedback') {
    await writeNoOpFeatureRequestRepository(projectDir, packageName).catch(() => {});
  }
  if (normalized === 'widget') {
    await writeNoOpAdminHomeWidgets(projectDir).catch(() => {});
  }
  if (normalized === 'revenuecat') {
    await writeNoOpSubscriptionStubs(projectDir, packageName).catch(() => {});
  }

  // 12. Regenerate router.dart and main.dart with updated module list
  try {
    await writeRouter(projectDir, modulesAfterRemoval, packageName);
  } catch {
    // Non-fatal: router regeneration failure does not block pub get
  }
  try {
    await writeMainDart(projectDir, kitSetup.backendProvider, modulesAfterRemoval, packageName, {});
  } catch {
    // Non-fatal
  }

  // 13. flutter pub get
  {
    const spinner = ui.spinner();
    spinner.start(t('remove.pubGet'));
    try {
      await execAsync('flutter pub get', { cwd: projectDir, timeout: 300_000 });
      spinner.stop(t('remove.pubGetDone'));
    } catch {
      spinner.stop(`⚠ ${t('remove.pubGetFailed')}`);
    }
  }

  // 14. build_runner (only for modules whose removal changes codegen inputs)
  const needsBuildRunner = [
    'revenuecat', 'analytics', 'sentry', 'onboarding', 'llm_chat', 'feedback',
  ].includes(normalized);
  if (needsBuildRunner) {
    const spinner = ui.timedSpinner();
    spinner.start(t('remove.buildRunner'));
    try {
      await execAsync(
        'dart run build_runner build --delete-conflicting-outputs',
        { cwd: projectDir, timeout: 600_000 }
      );
      spinner.stop(t('remove.buildRunnerDone'));
    } catch {
      spinner.stop(`⚠ ${t('remove.buildRunnerFailed')}`);
    }
  }

  if (normalized === 'ci') {
    ui.log.message(kleur.dim(t('remove.warn.ci')));
  }

  ui.outro(t('remove.success', { module: normalized }));
}

module.exports = { runRemove };
