#!/usr/bin/env node

// eslint-env node, es2021

const { exec } = require('child_process');
const { promisify } = require('util');
const fs = require('fs').promises;
const path = require('path');

const execAsync = promisify(exec);

// Colors for console output
const colors = {
  reset: '\x1b[0m',
  bright: '\x1b[1m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  red: '\x1b[31m',
  cyan: '\x1b[36m',
};

function log(message, color = '') {
  console.log(`${color}${message}${colors.reset}`);
}

async function pathExists(p) {
  try {
    await fs.access(p);
    return true;
  } catch {
    return false;
  }
}

async function getPlugins() {
  const pluginDevDir = path.join(__dirname, 'plugin-dev');
  const entries = await fs.readdir(pluginDevDir, { withFileTypes: true });

  const plugins = [];

  // Build sync-core before shared-schema. shared-schema re-exports generic
  // vector-clock algorithms from sync-core, while sync-core must not import
  // shared-schema; the sync core stays domain-agnostic.
  plugins.push({
    name: 'sync-core',
    path: 'packages/sync-core',
    buildCommand: 'npm run build',
    skipCopy: true,
  });

  // Provider contracts and implementations depend on public sync-core exports
  // but must stay outside the core package boundary.
  plugins.push({
    name: 'sync-providers',
    path: 'packages/sync-providers',
    buildCommand: 'npm run build',
    skipCopy: true,
  });

  // Add shared-schema after sync-core as it's needed for app/server type resolution.
  plugins.push({
    name: 'shared-schema',
    path: 'packages/shared-schema',
    buildCommand: 'npm run build',
    skipCopy: true,
  });

  // Add plugin-api as it's a dependency for plugins
  plugins.push({
    name: 'plugin-api',
    path: 'packages/plugin-api',
    buildCommand: 'npm run build',
    skipCopy: true,
  });

  // Add vite-plugin as it's a build dependency
  plugins.push({
    name: 'vite-plugin',
    path: 'packages/vite-plugin',
    buildCommand: 'npm run build',
    skipCopy: true,
  });

  for (const entry of entries) {
    if (entry.isDirectory()) {
      const pluginPath = path.join(pluginDevDir, entry.name);
      const packageJsonPath = path.join(pluginPath, 'package.json');

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

        // Skip boilerplate
        if (entry.name === 'boilerplate-solid-js') continue;

        const buildScript = packageJson.scripts && packageJson.scripts.build;
        // Check if it's a real build script or just a placeholder
        const hasRealBuildScript =
          buildScript && !buildScript.includes("echo 'No build needed");

        // Read manifest.json to check for additional files (e.g., config schema)
        const files = ['manifest.json', 'plugin.js', 'icon.svg'];
        const requiredFiles = [...files];
        // manifest.json may live at the root or inside src/
        const manifestCandidates = [
          path.join(pluginPath, 'manifest.json'),
          path.join(pluginPath, 'src', 'manifest.json'),
        ];
        let manifestRead = false;
        for (const manifestPath of manifestCandidates) {
          try {
            const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
            manifestRead = true;
            if (manifest.iFrame !== false) {
              files.push('index.html');
              requiredFiles.push('index.html');
            }
            if (manifest.jsonSchemaCfg) {
              files.push(manifest.jsonSchemaCfg);
              requiredFiles.push(manifest.jsonSchemaCfg);
            }
            if (manifest.i18n?.languages) {
              for (const lang of manifest.i18n.languages) {
                requiredFiles.push(`i18n/${lang}.json`);
              }
            }
            break;
          } catch {
            // Try next candidate
          }
        }
        if (!manifestRead) {
          // No manifest.json found — assume index.html is needed
          files.push('index.html');
          requiredFiles.push('index.html');
        }

        plugins.push({
          name: entry.name,
          path: `packages/plugin-dev/${entry.name}`,
          buildCommand: hasRealBuildScript ? 'npm run build' : undefined,
          files,
          requiredFiles,
          sourcePath: hasRealBuildScript ? 'dist' : undefined,
        });
      } catch (e) {
        // Not a package, skip
      }
    }
  }

  return plugins;
}

async function ensureDir(dir) {
  try {
    await fs.mkdir(dir, { recursive: true });
  } catch (error) {
    // Directory might already exist
  }
}

async function copyFile(src, dest) {
  try {
    // console.log(`Copying ${src} to ${dest}`);
    await fs.copyFile(src, dest);
    return true;
  } catch (error) {
    if (error.code === 'ENOENT') {
      log(`  ❌ Missing expected file: ${path.basename(src)} (path: ${src})`, colors.red);
      // We want to fail if a file is missing
      throw new Error(`Missing expected file: ${path.basename(src)}`);
    } else {
      log(`  ❌ Failed to copy ${path.basename(src)}: ${error.message}`, colors.red);
      throw error;
    }
  }
}

async function buildPlugin(plugin) {
  const startTime = Date.now();
  log(`\n📦 Processing ${plugin.name}...`, colors.cyan);

  try {
    // Check if plugin directory exists
    const pluginPath = path.resolve(plugin.path);
    try {
      await fs.access(pluginPath);
    } catch {
      throw new Error(`Plugin directory not found: ${plugin.path}`);
    }

    // Run build command if specified
    if (plugin.buildCommand) {
      log(`  Building...`, colors.yellow);
      const packageJsonPath = path.join(pluginPath, 'package.json');

      // Check if package.json exists and install dependencies if needed
      try {
        await fs.access(packageJsonPath);
        const nodeModulesPath = path.join(pluginPath, 'node_modules');
        // Check if dependencies are actually installed by looking for a real package
        // (not just .vite cache folders)
        let needsInstall = false;
        try {
          await fs.access(nodeModulesPath);
          // Check if at least one actual package exists (not a dot-folder)
          const entries = await fs.readdir(nodeModulesPath);
          const hasRealPackages = entries.some((e) => !e.startsWith('.'));
          needsInstall = !hasRealPackages;
        } catch {
          needsInstall = true;
        }
        if (needsInstall) {
          log(`  Installing dependencies...`, colors.yellow);
          await execAsync(`cd ${pluginPath} && npm install`);
        }
      } catch {
        // No package.json, skip install
      }

      await execAsync(`cd ${pluginPath} && ${plugin.buildCommand}`);
    }

    // Copy files to assets if not skipped
    if (!plugin.skipCopy) {
      const targetDir = path.join('src/assets/bundled-plugins', plugin.name);
      await fs.rm(targetDir, { recursive: true, force: true });
      await ensureDir(targetDir);

      const filesToCopy = plugin.files || [];

      // Prefer dist if it exists, otherwise fall back to plugin root (for simple plugins)
      const preferredSourcePath = plugin.sourcePath
        ? path.join(pluginPath, plugin.sourcePath)
        : path.join(pluginPath, 'dist');
      const distExists = await pathExists(preferredSourcePath);
      const sourcePath = distExists ? preferredSourcePath : pluginPath;

      const requiredFiles = plugin.requiredFiles || filesToCopy;
      const missingFiles = [];

      if (distExists) {
        // Copy the full dist to preserve all chunks/assets
        await fs.cp(sourcePath, targetDir, { recursive: true });
      } else {
        // Simple plugins: copy listed files from the root
        for (const file of filesToCopy) {
          const src = path.join(sourcePath, file);
          const dest = path.join(targetDir, file);
          try {
            if (!(await copyFile(src, dest))) {
              missingFiles.push(file);
            }
          } catch (e) {
            throw e;
          }
        }

        // Copy i18n directory if it exists
        const i18nSrc = path.join(sourcePath, 'i18n');
        if (await pathExists(i18nSrc)) {
          const i18nDest = path.join(targetDir, 'i18n');
          await fs.cp(i18nSrc, i18nDest, { recursive: true });
        }
      }

      // Validate required files exist after copy
      for (const file of requiredFiles) {
        const dest = path.join(targetDir, file);
        if (!(await pathExists(dest))) {
          missingFiles.push(file);
        }
      }

      if (missingFiles.length) {
        throw new Error(
          `Missing required asset(s) for ${plugin.name}: ${missingFiles.join(', ')}`,
        );
      }

      if (distExists) {
        log(`  ✅ Copied dist to assets (${plugin.name})`, colors.green);
      } else {
        log(
          `  ✅ Copied ${filesToCopy.length}/${filesToCopy.length} files to assets`,
          colors.green,
        );
      }
    }

    const duration = ((Date.now() - startTime) / 1000).toFixed(1);
    log(`✅ ${plugin.name} completed (${duration}s)`, colors.green);

    return { plugin: plugin.name, success: true, duration };
  } catch (error) {
    const duration = ((Date.now() - startTime) / 1000).toFixed(1);
    log(`❌ ${plugin.name} failed: ${error.message} (${duration}s)`, colors.red);
    return { plugin: plugin.name, success: false, error: error.message, duration };
  }
}

async function buildAll() {
  log('🚀 Building all packages...', colors.bright);
  const startTime = Date.now();

  const plugins = await getPlugins();

  // Build plugins sequentially to avoid conflicts
  const results = [];
  for (const plugin of plugins) {
    const result = await buildPlugin(plugin);
    results.push(result);
  }

  // Summary
  const totalDuration = ((Date.now() - startTime) / 1000).toFixed(1);
  const successful = results.filter((r) => r.success).length;
  const failed = results.filter((r) => !r.success).length;

  log('\n📊 Build Summary:', colors.bright);
  log(`  Total packages: ${plugins.length}`);
  log(`  Successful: ${successful}`, colors.green);
  if (failed > 0) {
    log(`  Failed: ${failed}`, colors.red);
    results
      .filter((r) => !r.success)
      .forEach((r) => {
        log(`    - ${r.plugin}: ${r.error}`, colors.red);
      });
  }
  log(`  Total time: ${totalDuration}s`);

  if (failed > 0) {
    process.exit(1);
  }
}

// Run if called directly
if (require.main === module) {
  buildAll().catch((error) => {
    log(`\n❌ Build failed: ${error.message}`, colors.red);
    process.exit(1);
  });
}

module.exports = { buildAll };
