/**
 * Generic copy + token-replacement engine.
 * Backend-agnostic: receives sourceDir, targetDir, and a tokens map.
 */

const path = require('node:path');
const fs = require('fs-extra');

// Directories / files that should NEVER be copied from any source template
const ALWAYS_EXCLUDE = new Set([
  // Flutter / Dart build artifacts
  'build',
  '.dart_tool',
  '.flutter-plugins',
  '.flutter-plugins-dependencies',
  'pubspec.lock',
  // Credentials — must NEVER end up in the client's project
  'firebase_key.json',
  '.env',
  'kit_setup.json',
  // VCS / OS
  '.git',
  '.DS_Store',
  // IDE & development tooling (developer-specific, not for the client)
  'node_modules',
  '.vscode',       // launch.json may contain developer credentials
  '.cursor',
  '.idea',
  '.claude',       // AI assistant instructions — not for the client
  // CI/CD configs — delivered only when the user selects the 'ci' module
  // (these files live in features/ci/ and are applied as a feature patch)
  '.github',
  '.gitlab',
  '.gitlab-ci.yml',
  'codemagic.yaml',
]);

// File basenames that should NEVER be copied, regardless of directory depth
const ALWAYS_EXCLUDE_BASENAMES = new Set([
  'Podfile.lock',           // iOS CocoaPods lock — regenerated by pod install
  'google-services.json',   // Firebase Android config — client must generate their own
  'GoogleService-Info.plist', // Firebase iOS config — client must generate their own
  'firebase_options_dev.dart', // Dev Firebase options — internal only
]);

// File extensions that may contain generated Dart code — skip these
const GENERATED_DART_EXTENSIONS = ['.g.dart', '.freezed.dart'];

// File extensions we try to replace text tokens in
const TEXT_EXTENSIONS = new Set([
  '.dart', '.yaml', '.json', '.xml', '.kt', '.kts', '.swift', '.m',
  '.gradle', '.plist', '.md', '.txt', '.html', '.js', '.ts',
  '.pbxproj', '.entitlements', '.xcscheme', '.strings', '.sh', '.sql',
]);

function isExcluded(relPath) {
  // relPath is always normalized to forward slashes before being passed here
  const top = relPath.split('/')[0];
  if (ALWAYS_EXCLUDE.has(top)) return true;
  if (ALWAYS_EXCLUDE_BASENAMES.has(path.basename(relPath))) return true;
  if (GENERATED_DART_EXTENSIONS.some((ext) => relPath.endsWith(ext))) return true;
  return false;
}

function isTextFile(filePath) {
  return TEXT_EXTENSIONS.has(path.extname(filePath).toLowerCase());
}

/**
 * Replace all occurrences of each token in content.
 * @param {string} content
 * @param {Record<string, string>} tokens - { 'OLD_VALUE': 'new_value', ... }
 */
function applyTokens(content, tokens) {
  let result = content;
  for (const [from, to] of Object.entries(tokens)) {
    result = result.replaceAll(from, to);
  }
  return result;
}

/**
 * Apply path replacements to a relative path.
 * Used to rewrite Android Kotlin package paths (e.g. com/appfirebase/app → com/example/myapp).
 * @param {string} relPath - Path with forward slashes
 * @param {Record<string, string>} pathReplacements - { 'old/segment': 'new/segment', ... }
 */
function applyPathReplacements(relPath, pathReplacements) {
  if (!pathReplacements || Object.keys(pathReplacements).length === 0) {
    return relPath;
  }
  let result = relPath.replace(/\\/g, '/');
  for (const [from, to] of Object.entries(pathReplacements)) {
    result = result.replaceAll(from.replace(/\\/g, '/'), to.replace(/\\/g, '/'));
  }
  return result;
}

/**
 * Copy a source directory to a target directory, skipping excluded paths
 * and applying token replacement in text files.
 *
 * @param {string} sourceDir - Absolute path to source
 * @param {string} targetDir - Absolute path to destination (will be created)
 * @param {Record<string,string>} tokens - Token replacement map
 * @param {string[]} [extraExcludes] - Additional top-level names to skip
 * @param {Record<string,string>} [pathReplacements] - Path segment replacements (e.g. {'com/appfirebase/app':'com/example/myapp'})
 * @returns {Promise<{filesCopied: number, filesSkipped: number}>}
 */
async function copyWithTokens(sourceDir, targetDir, tokens = {}, extraExcludes = [], pathReplacements = {}) {
  const extraSet = new Set(extraExcludes);
  let filesCopied = 0;
  let filesSkipped = 0;

  async function walk(src) {
    const entries = await fs.readdir(src, { withFileTypes: true });

    for (const entry of entries) {
      const srcPath = path.join(src, entry.name);
      const rel = path.relative(sourceDir, srcPath).replace(/\\/g, '/');
      const top = rel.split('/')[0];

      if (isExcluded(rel) || extraSet.has(top) || entry.name.startsWith('.DS_Store')) {
        filesSkipped++;
        continue;
      }

      if (entry.isDirectory()) {
        await walk(srcPath);
      } else if (entry.isFile()) {
        const adjustedRel = applyPathReplacements(rel, pathReplacements);
        const destPath = path.join(targetDir, adjustedRel);

        if (isTextFile(srcPath) && Object.keys(tokens).length > 0) {
          try {
            const raw = await fs.readFile(srcPath, 'utf8');
            const replaced = applyTokens(raw, tokens);
            await fs.outputFile(destPath, replaced, 'utf8');
          } catch {
            await fs.copy(srcPath, destPath);
          }
        } else {
          await fs.ensureDir(path.dirname(destPath));
          await fs.copy(srcPath, destPath);
        }
        filesCopied++;
      }
    }
  }

  await walk(sourceDir);
  return { filesCopied, filesSkipped };
}

/**
 * Apply a patch directory over an existing target.
 * Copies files from patchDir to targetDir, overwriting existing files.
 * Applies token replacement to text files.
 *
 * @param {string} patchDir - Absolute path to patch directory (e.g. backends/supabase/patch)
 * @param {string} targetDir - Absolute path to project (e.g. generated project root)
 * @param {Record<string,string>} tokens - Token replacement map
 * @param {Record<string,string>} [pathReplacements] - Path segment replacements
 * @returns {Promise<{filesApplied: number}>}
 */
async function applyPatch(patchDir, targetDir, tokens = {}, pathReplacements = {}) {
  if (!(await fs.pathExists(patchDir))) {
    return { filesApplied: 0 };
  }

  let filesApplied = 0;

  async function walk(src) {
    const entries = await fs.readdir(src, { withFileTypes: true });

    for (const entry of entries) {
      const srcPath = path.join(src, entry.name);
      const rel = path.relative(patchDir, srcPath).replace(/\\/g, '/');

      // Skip OS junk files only — do NOT skip .github, .gitlab, etc.
      if (entry.name === '.DS_Store' || entry.name.startsWith('._')) continue;
      if (GENERATED_DART_EXTENSIONS.some((ext) => rel.endsWith(ext))) continue;

      if (entry.isDirectory()) {
        await walk(srcPath);
      } else if (entry.isFile()) {
        const adjustedRel = applyPathReplacements(rel, pathReplacements);
        const destPath = path.join(targetDir, adjustedRel);
        await fs.ensureDir(path.dirname(destPath));

        if (isTextFile(srcPath) && Object.keys(tokens).length > 0) {
          const raw = await fs.readFile(srcPath, 'utf8');
          const replaced = applyTokens(raw, tokens);
          await fs.outputFile(destPath, replaced, 'utf8');
        } else {
          await fs.copy(srcPath, destPath);
        }
        filesApplied++;
      }
    }
  }

  await walk(patchDir);
  return { filesApplied };
}

/**
 * Remove files from target that are no longer needed (e.g. Firebase-specific when using Supabase).
 * @param {string} targetDir - Project root
 * @param {string[]} relativePaths - Paths relative to targetDir (e.g. ['lib/core/data/api/firestore_retry.dart'])
 */
async function removeFiles(targetDir, relativePaths) {
  for (const rel of relativePaths) {
    const p = path.join(targetDir, rel);
    if (await fs.pathExists(p)) {
      await fs.remove(p).catch((err) => {
        throw new Error(`Failed to remove ${rel}: ${err.message}`);
      });
    }
  }
}

module.exports = {
  copyWithTokens,
  applyTokens,
  applyPatch,
  removeFiles,
  isExcluded,
  isTextFile,
};
