/**
 * generate.js — Motor unificado de geração de projetos.
 *
 * Todos os backends (Firebase, Supabase, API) usam este motor.
 * Cada backend fornece dois hooks opcionais:
 *
 *   applyBackendSetup(targetDir, tokens, pathReplacements, options)
 *     → Chamado dentro do bloco project-setup, logo após copyWithTokens.
 *     → Responsável por: aplicar patch do backend, trocar pubspec.yaml,
 *       remover artefatos irrelevantes, escrever overrides de environment
 *       e copiar arquivos extras do backend (ex: supabase/migrations/).
 *     → Retorna: { detail?, extraAnswers?, kitSetupExtra?, returnExtra? }
 *         - detail       : string extra para o log do passo (ex: "12 patch files")
 *         - extraAnswers : campos extras para writeVsCodeLaunch / writeEnvExample
 *         - kitSetupExtra: campos extras para writeKitSetup
 *         - returnExtra  : campos extras no objeto de retorno da função
 *
 *   postBuild(targetDir, options, steps)
 *     → Chamado após build-runner e flutterfire configure.
 *     → Responsável por: deploy automático (Firebase), etc.
 *
 * Passos comuns executados para todos os backends:
 *   1.  Validação do template base (Firebase/)
 *   2.  buildTokens (tokens de substituição de texto)
 *   3.  copyWithTokens  (Firebase/ → targetDir)
 *   [hook: applyBackendSetup]
 *   3b. applyFeaturePatches  (features/ci/, features/web/, etc.)
 *   4.  writeVsCodeLaunch
 *   5.  writeEnvExample
 *   6.  writeFeaturesConfig
 *   7.  writeKitSetup
 *   8.  flutter pub get
 *   9.  dart run slang
 *   10. dart run build_runner build
 *   11. flutterfire configure  (Firebase para FCM + Remote Config — todos os backends)
 *   12. writeFirebaserc
 *   [hook: postBuild]
 */

const path = require('node:path');
const fs = require('fs-extra');
const { copyWithTokens, applyPatch } = require('./engine');
const { buildTokens } = require('./backends/firebase/tokens');
const { FEATURES_PATCH_DIR } = require('./catalog');
const {
  writeRouter,
  writeVsCodeLaunch,
  writeEnvExample,
  writeEnvFileIfMissing,
  writeFirebaserc,
  writeFeaturesConfig,
  writeKitSetup,
  writeMakefile,
  writeNoOpAnalyticsApi,
  writeNoOpTrackingApi,
  removeAndroidWidgetArtifacts,
  removeFacebookSigninFromAuthPages,
  removeAndroidFacebookMetadata,
  writeNoOpAdminHomeWidgets,
  writeNoOpFeatureRequestRepository,
  writeNoOpSubscriptionStubs,
  writeNoOpSentryUsages,
  writeMainDart,
  removeModuleDirs,
  stripPubspecDeps,
  removeDevelopmentTeam,
  localizeReleaseDocs,
} = require('./shared/generator-utils');
const { pubGet, slangGenerate, buildRunner, flutterfireConfigure, writeGoogleAuthOptions, writeGoogleIosUrlScheme, patchFirebaseServiceWorker } = require('./shared/post-build');
const { FIREBASE_SOURCE_DIR } = require('./shared/backend-config');

/**
 * Gera um projeto Flutter para o backend indicado.
 *
 * @param {string} targetDir - Caminho absoluto do diretório de destino.
 * @param {'firebase'|'supabase'|'api'} backend - Backend selecionado.
 * @param {object} options
 * @param {string}   options.appName
 * @param {string}   options.bundleId
 * @param {string}   options.firebaseProjectId
 * @param {string[]} [options.modules=[]]
 * @param {object}   [options.moduleAnswers={}]
 * @param {Function} [options.onProgress]  - Callback para atualizar spinner no CLI.
 * @param {object}   [options.deploy]      - Usado pelo hook postBuild do Firebase.
 * @param {object}   [hooks={}]
 * @param {Function|null} [hooks.applyBackendSetup]
 * @param {Function|null} [hooks.postBuild]
 * @returns {Promise<{steps: object[], packageName: string, appName: string, bundleId: string, firebaseProjectId: string}>}
 */
async function generateProject(targetDir, backend, options, hooks = {}) {
  const {
    appName,
    bundleId,
    firebaseProjectId,
    modules = [],
    moduleAnswers = {},
    onProgress = () => {},
    includeWeb = true,
    language = 'en',
  } = options;

  const { applyBackendSetup = null, postBuild = null } = hooks;

  if (!(await fs.pathExists(FIREBASE_SOURCE_DIR))) {
    throw new Error(
      `Firebase template not found: ${FIREBASE_SOURCE_DIR}\n` +
      'Make sure the Firebase/ folder exists at the kit root.'
    );
  }

  const { tokens, packageName, pathReplacements } = buildTokens({
    appName,
    bundleId,
    fbAppId: moduleAnswers.fbAppId,
    fbToken: moduleAnswers.fbToken,
    defaultPaywall: moduleAnswers.defaultPaywall || 'basic',
    functionsRegion: options.functionsRegion || 'us-central1',
  });

  const steps = [];
  let extraAnswers = {};
  let kitSetupExtra = {};
  let returnExtra = {};

  // Track whether we created the target directory so we can clean it up on failure
  const targetDirExistedBefore = await fs.pathExists(targetDir);

  // ── 1. Project setup ───────────────────────────────────────────────────────
  onProgress('project-setup');
  try {
    const { filesCopied } = await copyWithTokens(
      FIREBASE_SOURCE_DIR,
      targetDir,
      tokens,
      [],
      pathReplacements
    );

    // Copy README in user's language (README.md = pt, README.en.md, README.es.md)
    const readmeLang = ['en', 'pt', 'es'].includes(language) ? language : 'en';
    if (readmeLang !== 'pt') {
      const langReadme = path.join(targetDir, `README.${readmeLang}.md`);
      if (await fs.pathExists(langReadme)) {
        await fs.copy(langReadme, path.join(targetDir, 'README.md'));
      }
    }
    for (const lang of ['en', 'es']) {
      const p = path.join(targetDir, `README.${lang}.md`);
      if (await fs.pathExists(p)) await fs.remove(p);
    }

    await localizeReleaseDocs(targetDir, readmeLang);

    let setupDetail = `${filesCopied} files`;

    // Remove the web/ folder when the web module was NOT selected.
    // The base template always has web/, but clients that want native-only
    // should not receive it.
    if (!modules.includes('web')) {
      const webDir = path.join(targetDir, 'web');
      if (await fs.pathExists(webDir)) {
        await fs.remove(webDir);
      }
    }

    // Apply feature patches BEFORE backend setup so the backend patch can
    // override any Firebase-specific files introduced by the feature patch
    // (e.g. feedbacks/api/ uses Firestore in the feature patch but Supabase/API
    // backends have their own version that must win).
    let featurePatchCount = 0;
    for (const module of modules) {
      const patchDir = path.join(FEATURES_PATCH_DIR, module);
      if (await fs.pathExists(patchDir)) {
        const { filesApplied } = await applyPatch(patchDir, targetDir, tokens, pathReplacements);
        featurePatchCount += filesApplied;
      }
    }
    if (featurePatchCount > 0) setupDetail += ` + ${featurePatchCount} feature files`;

    if (applyBackendSetup) {
      const hookResult = (await applyBackendSetup(targetDir, tokens, pathReplacements, options)) ?? {};
      if (hookResult.detail) setupDetail += ` + ${hookResult.detail}`;
      if (hookResult.extraAnswers) extraAnswers = hookResult.extraAnswers;
      if (hookResult.kitSetupExtra) kitSetupExtra = hookResult.kitSetupExtra;
      if (hookResult.returnExtra) returnExtra = hookResult.returnExtra;
    }

    // CI templates: swap to user's language (PULL_REQUEST_TEMPLATE, BUG, feature_request)
    if (modules.includes('ci')) {
      const githubDir = path.join(targetDir, '.github');
      const issueDir = path.join(githubDir, 'ISSUE_TEMPLATE');
      const ciFiles = [
        ['PULL_REQUEST_TEMPLATE', path.join(githubDir, 'PULL_REQUEST_TEMPLATE.md')],
        ['BUG', path.join(issueDir, 'BUG.md')],
        ['feature_request', path.join(issueDir, 'feature_request.md')],
      ];
      const readmeLang = ['en', 'pt', 'es'].includes(language) ? language : 'en';
      for (const [base, destPath] of ciFiles) {
        const dir = path.dirname(destPath);
        if (readmeLang !== 'en') {
          const langPath = path.join(dir, `${base}.${readmeLang}.md`);
          if (await fs.pathExists(langPath)) {
            await fs.copy(langPath, destPath);
          }
        }
        for (const lang of ['pt', 'es']) {
          const p = path.join(dir, `${base}.${lang}.md`);
          if (await fs.pathExists(p)) await fs.remove(p);
        }
      }
    }

    const answers = { ...moduleAnswers, ...extraAnswers, functionsRegion: options.functionsRegion || 'us-central1' };
    await writeVsCodeLaunch(targetDir, appName, backend, modules, answers, language);
    await writeEnvExample(targetDir, modules, answers, language);
    await writeEnvFileIfMissing(targetDir);
    await writeFeaturesConfig(targetDir, modules, answers, language);
    await writeRouter(targetDir, modules, packageName, moduleAnswers.defaultPaywall || 'basic');
    await writeMakefile(targetDir, language, backend, modules, answers);
    await writeKitSetup(targetDir, {
      appName,
      bundleId,
      backend,
      modules,
      firebaseProjectId,
      moduleAnswers: answers,
      ...kitSetupExtra,
    });

    // ── Phase B: decouple optional modules ───────────────────────────────────
    // Remove directories of modules that were NOT selected
    await removeModuleDirs(targetDir, modules);

    // Write no-op analytics_api.dart if analytics not selected
    if (!modules.includes('analytics')) {
      await writeNoOpAnalyticsApi(targetDir, packageName);
    }

    // Write no-op tracking_api.dart if facebook not selected
    if (!modules.includes('facebook')) {
      await writeNoOpTrackingApi(targetDir, packageName);
      // Remove Facebook SDK meta-data from AndroidManifest so the SDK doesn't
      // initialize on app start and spam logs with OAuthException error 190.
      await removeAndroidFacebookMetadata(targetDir);
      await removeFacebookSigninFromAuthPages(targetDir);
    }

    // Write no-op feature_request_repository.dart if feedback not selected but revenuecat IS selected.
    // Only premium_page_provider.dart (subscription module) imports this — when revenuecat is not
    // selected the subscription module is removed entirely, so the stub is not needed.
    if (!modules.includes('feedback') && modules.includes('revenuecat')) {
      await writeNoOpFeatureRequestRepository(targetDir, packageName);
    }

    // Write no-op subscription stubs if revenuecat not selected
    // (user_repository, home_page, admin_paywalls, onboarding_page reference subscription types)
    if (!modules.includes('revenuecat')) {
      await writeNoOpSubscriptionStubs(targetDir, packageName);
    }

    // Write no-op admin_home_widgets.dart if widget not selected
    // (imports home_widget_mywidget_service which only exists when widget is selected)
    if (!modules.includes('widget')) {
      await writeNoOpAdminHomeWidgets(targetDir);
      // Remove Android native widget files and unregister the receiver from the manifest
      // so the widget does not appear in the Android widget picker when not selected
      await removeAndroidWidgetArtifacts(targetDir, bundleId);
    }

    // Patch core files that import sentry_flutter when sentry, revenuecat, and facebook
    // are all not selected — removes sentry import so the dep can be stripped from pubspec.
    if (!modules.includes('sentry') && !modules.includes('revenuecat') && !modules.includes('facebook')) {
      await writeNoOpSentryUsages(targetDir);
    }

    // Generate main.dart based on backend + selected modules
    await writeMainDart(targetDir, backend, modules, packageName, {
      supabaseUrl: extraAnswers.supabaseUrl,
      supabaseAnonKey: extraAnswers.supabaseAnonKey,
    });

    // Strip unused deps from pubspec.yaml
    await stripPubspecDeps(targetDir, modules);

    // Remove internal DEVELOPMENT_TEAM from iOS project — clients use their own Apple team
    await removeDevelopmentTeam(targetDir);
    // ── End Phase B ──────────────────────────────────────────────────────────

    steps.push({ name: 'project-setup', ok: true, detail: setupDetail });
  } catch (err) {
    steps.push({ name: 'project-setup', ok: false, detail: err.message });
    // Clean up partial output only if we created the directory — never delete pre-existing dirs
    if (!targetDirExistedBefore && await fs.pathExists(targetDir)) {
      await fs.remove(targetDir).catch(() => {});
    }
    return { steps };
  }

  // ── 2. Post-build comum ────────────────────────────────────────────────────
  onProgress('pub-get');
  const pubGetResult = await pubGet(targetDir);
  steps.push({ name: 'pub-get', ok: pubGetResult.ok, detail: pubGetResult.ok ? null : pubGetResult.error });
  if (!pubGetResult.ok) return { steps };

  onProgress('slang');
  const slangResult = await slangGenerate(targetDir);
  steps.push({ name: 'slang', ok: slangResult.ok, detail: slangResult.ok ? null : slangResult.error });
  if (!slangResult.ok) return { steps };

  onProgress('build-runner');
  const buildRunnerResult = await buildRunner(targetDir);
  steps.push({
    name: 'build-runner',
    ok: buildRunnerResult.ok,
    detail: buildRunnerResult.ok ? null : buildRunnerResult.error,
  });
  if (!buildRunnerResult.ok) return { steps };

  onProgress('flutterfire');
  const ffResult = await flutterfireConfigure(targetDir, firebaseProjectId, { includeWeb });
  steps.push({ name: 'flutterfire', ok: ffResult.ok, detail: ffResult.ok ? null : ffResult.error });

  // After flutterfire: patch Android, iOS and Web config files.
  if (ffResult.ok) {
    // Android: populate kGoogleWebClientId in lib/google_auth_options.dart.
    // Skip for Supabase — it defines kGoogleIosClientId too, handled separately
    // in new.js after credentials are resolved (readSupabaseGoogleCredentials).
    if (backend !== 'supabase') {
      const gaResult = await writeGoogleAuthOptions(targetDir);
      steps.push({
        name: 'google-auth-options',
        ok: gaResult.ok,
        detail: gaResult.ok ? null : gaResult.error,
      });
    }

    // iOS: register REVERSED_CLIENT_ID as a URL scheme in ios/Runner/Info.plist
    const iosSchemeResult = await writeGoogleIosUrlScheme(targetDir);
    steps.push({
      name: 'google-ios-url-scheme',
      ok: iosSchemeResult.ok,
      detail: iosSchemeResult.ok ? null : iosSchemeResult.error,
    });

    // Web: patch firebase-messaging-sw.js with real config values (only when web module is selected)
    if (modules.includes('web')) {
      const swResult = await patchFirebaseServiceWorker(targetDir);
      steps.push({
        name: 'firebase-messaging-sw',
        ok: swResult.ok,
        detail: swResult.ok ? null : swResult.error,
      });
    }
  }

  await writeFirebaserc(targetDir, firebaseProjectId);

  // ── 3. Post-build específico do backend ────────────────────────────────────
  if (postBuild) {
    try {
      await postBuild(targetDir, options, steps);
    } catch (err) {
      steps.push({ name: 'post-build', ok: false, detail: err.message });
    }
  }

  return { steps, packageName, appName, bundleId, firebaseProjectId, ...returnExtra };
}

module.exports = { generateProject };
