'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,
  BASE_COMPONENT_FILES,
  CORE_FILES,
  CORE_SOURCE_DIR,
  FEATURES_PATCH_DIR,
  normalizeFeature,
} = require('../scaffold/catalog');
const { applyPatch } = require('../scaffold/engine');
const { buildTokens } = require('../scaffold/backends/firebase/tokens');
const { localizeReleaseDocs } = require('../scaffold/shared/generator-utils');

const execAsync = promisify(exec);

const CHANGELOG_PATH = path.join(__dirname, '..', 'scaffold', 'CHANGELOG.json');

const NEEDS_BUILD_RUNNER = [
  'revenuecat', 'analytics', 'sentry', 'onboarding', 'llm_chat', 'feedback',
];
const COMPONENTS_UPDATE_TARGET = 'components';
const CORE_UPDATE_TARGET = 'core';
const IOS_RELEASE_UPDATE_TARGET = 'ios-release';

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

function isNewer(a, b) {
  const parse = (v) => String(v || '0.0.0').replace(/^v/, '').split('.').map(Number);
  const [aMaj, aMin, aPat] = parse(a);
  const [bMaj, bMin, bPat] = parse(b);
  if (aMaj !== bMaj) return aMaj > bMaj;
  if (aMin !== bMin) return aMin > bMin;
  return aPat > bPat;
}

/**
 * Collect changelog entries newer than `fromVersion` that affect the given modules.
 * Returns { module: ['v1.2.0: description', ...] }
 */
function getChangesSince(changelog, fromVersion, modules, language) {
  const lang = (language || 'en').replace(/-.*/, '').toLowerCase();
  const changes = {};
  for (const [version, entry] of Object.entries(changelog)) {
    if (!isNewer(version, fromVersion || '0.0.0')) continue;
    for (const [mod, description] of Object.entries(entry.modules || {})) {
      if (modules.includes(mod)) {
        const text = typeof description === 'object'
          ? (description[lang] || description.en || description.pt || Object.values(description)[0])
          : description;
        if (!changes[mod]) changes[mod] = [];
        changes[mod].push({ version, description: text });
      }
    }
  }
  return changes;
}

function normalizeUpdateTarget(value) {
  if (!value) {
    return null;
  }
  const normalized = String(value).trim().toLowerCase().replace(/[-\s]/g, '_');
  const componentAliases = new Set([
    'component',
    'components',
    'base_component',
    'base_components',
    'uikit',
    'ui_kit',
  ]);
  if (componentAliases.has(normalized)) {
    return COMPONENTS_UPDATE_TARGET;
  }
  const coreAliases = new Set(['core', 'core_files', 'corefiles']);
  if (coreAliases.has(normalized)) {
    return CORE_UPDATE_TARGET;
  }
  const iosAliases = new Set(['ios_release', 'iosrelease']);
  if (iosAliases.has(normalized) || normalized === 'ios-release') {
    return IOS_RELEASE_UPDATE_TARGET;
  }
  return null;
}

async function applyFileList(fileList, projectDir) {
  let filesApplied = 0;
  for (const relativePath of fileList) {
    const sourcePath = path.join(CORE_SOURCE_DIR, relativePath);
    if (!(await fs.pathExists(sourcePath))) {
      continue;
    }
    const destinationPath = path.join(projectDir, relativePath);
    await fs.ensureDir(path.dirname(destinationPath));
    await fs.copy(sourcePath, destinationPath);
    filesApplied += 1;
  }
  return filesApplied;
}

function applyBaseComponents(projectDir) {
  return applyFileList(BASE_COMPONENT_FILES, projectDir);
}

function applyCoreFiles(projectDir) {
  return applyFileList(CORE_FILES, projectDir);
}

/** Same detection logic used by add.js and remove.js. */
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');
  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;
}

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

async function runUpdate(module, options = {}) {
  const t = createTranslator(options.language || detectDefaultLanguage());
  const projectDir = path.resolve(options.directory || '.');
  const cancel = () => { ui.cancel(t('update.cancelled')); process.exit(0); };

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

  const kitSetup = await fs.readJson(kitSetupPath);
  const projectVersion = kitSetup.cliVersion || null;
  const currentVersion = pkg.version;

  // 2. Load changelog
  let changelog = {};
  if (await fs.pathExists(CHANGELOG_PATH)) {
    changelog = await fs.readJson(CHANGELOG_PATH);
  }

  // 3. Detect active modules
  const activeModules = await getActiveModules(kitSetup, projectDir);

  // ── Mode A: update a specific module ────────────────────────────────────────
  if (module) {
    const normalizedTarget = normalizeUpdateTarget(module);
    if (normalizedTarget === COMPONENTS_UPDATE_TARGET) {
      printCompactHeader(t);
      ui.intro(t('update.applyingComponents'));
      if (!options.yes) {
        ui.log.warn(t('update.warn.commitComponents'));
        const confirmed = await ui.confirm({
          message: t('update.confirmComponents'),
          initialValue: false,
          onCancel: cancel,
        });
        if (!confirmed) {
          ui.outro(t('update.cancelled'));
          return;
        }
      }

      const spinner = ui.spinner();
      spinner.start(t('update.applyingComponents'));
      try {
        const filesApplied = await applyBaseComponents(projectDir);
        if (filesApplied === 0) {
          spinner.stop(`⚠ ${t('update.noComponentFiles')}`);
          ui.outro(t('update.cancelled'));
          return;
        }
        spinner.stop(t('update.appliedComponents', { count: filesApplied }));
      } catch (err) {
        spinner.error(t('update.applyComponentsFailed'));
        throw err;
      }

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

      kitSetup.cliVersion = currentVersion;
      await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
      ui.outro(t('update.componentsSuccess'));
      return;
    }

    if (normalizedTarget === CORE_UPDATE_TARGET) {
      printCompactHeader(t);
      ui.intro(t('update.applyingCore'));
      if (!options.yes) {
        ui.log.warn(t('update.warn.commitComponents'));
        const confirmed = await ui.confirm({
          message: t('update.confirmCore'),
          initialValue: false,
          onCancel: cancel,
        });
        if (!confirmed) {
          ui.outro(t('update.cancelled'));
          return;
        }
      }

      const spinner = ui.spinner();
      spinner.start(t('update.applyingCore'));
      try {
        const filesApplied = await applyCoreFiles(projectDir);
        if (filesApplied === 0) {
          spinner.stop(`⚠ ${t('update.noComponentFiles')}`);
          ui.outro(t('update.cancelled'));
          return;
        }
        spinner.stop(t('update.appliedCore', { count: filesApplied }));
      } catch (err) {
        spinner.error(t('update.applyComponentsFailed'));
        throw err;
      }

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

      kitSetup.cliVersion = currentVersion;
      await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
      ui.outro(t('update.coreSuccess'));
      return;
    }

    if (normalizedTarget === IOS_RELEASE_UPDATE_TARGET) {
      const patchDir = path.join(FEATURES_PATCH_DIR, 'ios-release');
      if (!(await fs.pathExists(patchDir))) {
        throw new Error(t('update.error.noIosReleasePatch'));
      }
      printCompactHeader(t);
      ui.intro(t('update.applying', { module: IOS_RELEASE_UPDATE_TARGET }));
      if (!options.yes) {
        ui.log.warn(t('update.warn.commit', { module: IOS_RELEASE_UPDATE_TARGET }));
        const confirmed = await ui.confirm({
          message: t('update.confirm', { module: IOS_RELEASE_UPDATE_TARGET }),
          initialValue: false,
          onCancel: cancel,
        });
        if (!confirmed) {
          ui.outro(t('update.cancelled'));
          return;
        }
      }
      const spinner = ui.spinner();
      spinner.start(t('update.applying', { module: IOS_RELEASE_UPDATE_TARGET }));
      try {
        const { tokens, pathReplacements } = buildTokens({
          appName: kitSetup.appName,
          bundleId: kitSetup.bundleId,
        });
        await applyPatch(patchDir, projectDir, tokens, pathReplacements);
        await localizeReleaseDocs(projectDir, options.language || detectDefaultLanguage());
        spinner.stop(t('update.iosRelease.success'));
      } catch (err) {
        spinner.error(t('update.applyFailed', { module: IOS_RELEASE_UPDATE_TARGET }));
        throw err;
      }
      kitSetup.cliVersion = currentVersion;
      await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
      ui.outro(t('update.iosRelease.success'));
      return;
    }

    const normalized = normalizeFeature(module);
    if (!normalized) {
      throw new Error(
        t('update.error.unknownTarget', {
          module,
          list: [...AVAILABLE_FEATURES, COMPONENTS_UPDATE_TARGET, CORE_UPDATE_TARGET, IOS_RELEASE_UPDATE_TARGET].join(', '),
        })
      );
    }
    if (!activeModules.includes(normalized)) {
      printCompactHeader(t);
      ui.log.warn(t('update.error.notActive', { module: normalized }));
      return;
    }

    const patchDir = path.join(FEATURES_PATCH_DIR, normalized);
    if (!(await fs.pathExists(patchDir))) {
      printCompactHeader(t);
      ui.log.message(kleur.dim(t('update.noPatch', { module: normalized })));
      return;
    }

    printCompactHeader(t);
    ui.intro(t('update.applying', { module: normalized }));

    // Warn and confirm
    if (!options.yes) {
      ui.log.warn(t('update.warn.commit', { module: normalized }));
      const confirmed = await ui.confirm({
        message: t('update.confirm', { module: normalized }),
        initialValue: false,
        onCancel: cancel,
      });
      if (!confirmed) {
        ui.outro(t('update.cancelled'));
        return;
      }
    }

    // Re-apply patch
    {
      const spinner = ui.spinner();
      spinner.start(t('update.applying', { module: normalized }));
      try {
        const { tokens, pathReplacements } = buildTokens({
          appName: kitSetup.appName,
          bundleId: kitSetup.bundleId,
        });
        await applyPatch(patchDir, projectDir, tokens, pathReplacements);
        spinner.stop(t('update.applied', { module: normalized }));
      } catch (err) {
        spinner.error(t('update.applyFailed', { module: normalized }));
        throw err;
      }
    }

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

    // build_runner (only for modules that generate code)
    if (NEEDS_BUILD_RUNNER.includes(normalized)) {
      const spinner = ui.timedSpinner();
      spinner.start(t('update.buildRunner'));
      try {
        await execAsync(
          'dart run build_runner build --delete-conflicting-outputs',
          { cwd: projectDir, timeout: 600_000 }
        );
        spinner.stop(t('update.buildRunnerDone'));
      } catch {
        spinner.stop(`⚠ ${t('update.buildRunnerFailed')}`);
      }
    }

    // Update cliVersion
    kitSetup.cliVersion = currentVersion;
    await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');

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

  // ── Mode B: show status ──────────────────────────────────────────────────────
  const alreadyUpToDate = projectVersion && !isNewer(currentVersion, projectVersion);
  const modulesForChangelog = [...new Set([...activeModules, COMPONENTS_UPDATE_TARGET, CORE_UPDATE_TARGET])];
  const changes = getChangesSince(changelog, projectVersion, modulesForChangelog, options.language);

  // Modules in this project that have patch dirs (can be re-applied)
  const patchableModules = [];
  for (const m of activeModules) {
    if (await fs.pathExists(path.join(FEATURES_PATCH_DIR, m))) {
      patchableModules.push(m);
    }
  }

  if (alreadyUpToDate && Object.keys(changes).length === 0) {
    printCompactHeader(t);
    ui.outro(t('update.alreadyUpToDate', { version: currentVersion }));
    return;
  }

  printCompactHeader(t);
  const fromLabel = projectVersion || '?';
  ui.intro(t('update.status', { from: fromLabel, to: currentVersion }));

  // Modules/components with actual changelog entries — show what improved + the command
  const modulesWithChanges = Object.keys(changes);
  if (modulesWithChanges.length > 0) {
    const lines = [kleur.bold(t('update.changesTitle'))];
    for (const mod of modulesWithChanges) {
      for (const { description } of changes[mod]) {
        lines.push(`${kleur.cyan('✦')} ${kleur.bold(mod)}  ${kleur.dim('→')}  ${kleur.cyan(`kasy update ${mod}`)}`);
        lines.push(`  ${kleur.dim(description)}`);
      }
    }
    ui.log.message(lines.join('\n'));
  }

  // Modules without changelog entries that can still be re-applied (advanced / recovery)
  const modulesWithoutChanges = patchableModules.filter((m) => !modulesWithChanges.includes(m));
  const hasComponentChanges = modulesWithChanges.includes(COMPONENTS_UPDATE_TARGET);

  if (modulesWithChanges.length === 0 && !hasComponentChanges) {
    // Nothing new — show everything available
    const lines = [];
    if (patchableModules.length > 0) {
      lines.push(t('update.howToUpdate'));
      for (const m of patchableModules) {
        lines.push(kleur.cyan(`  kasy update ${m}`));
      }
      lines.push('');
    }
    lines.push(t('update.howToUpdateComponents'));
    lines.push(kleur.cyan(`  kasy update ${COMPONENTS_UPDATE_TARGET}`));
    ui.log.message(lines.join('\n'));
  } else if (modulesWithoutChanges.length > 0) {
    // Some modules have no new changes — show as secondary info
    const lines = [kleur.dim(t('update.reapplyTitle'))];
    for (const m of modulesWithoutChanges) {
      lines.push(kleur.dim(`  kasy update ${m}`));
    }
    if (!hasComponentChanges) {
      lines.push(kleur.dim(`  kasy update ${COMPONENTS_UPDATE_TARGET}`));
    }
    ui.log.message(lines.join('\n'));
  }
  ui.outro('');
}

module.exports = { runUpdate };
