#!/usr/bin/env node

/**
 * Version bumping and publishing script for Less.js monorepo
 * 
 * This script:
 * 1. Determines the next version (patch increment or explicit)
 * 2. Updates all package.json files to the same version
 * 3. Creates a git tag
 * 4. Commits version changes
 * 5. Publishes all packages to NPM
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const semver = require('semver');

const ROOT_DIR = path.resolve(__dirname, '..');
const PACKAGES_DIR = path.join(ROOT_DIR, 'packages');

// Get all package.json files
function getPackageFiles() {
  const packages = [];
  
  // Root package.json
  const rootPkgPath = path.join(ROOT_DIR, 'package.json');
  if (fs.existsSync(rootPkgPath)) {
    packages.push(rootPkgPath);
  }
  
  // Package directories
  const packageDirs = fs.readdirSync(PACKAGES_DIR, { withFileTypes: true })
    .filter(dirent => dirent.isDirectory())
    .map(dirent => path.join(PACKAGES_DIR, dirent.name));
  
  for (const pkgDir of packageDirs) {
    const pkgPath = path.join(pkgDir, 'package.json');
    if (fs.existsSync(pkgPath)) {
      packages.push(pkgPath);
    }
  }
  
  return packages;
}

// Read package.json
function readPackage(pkgPath) {
  return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
}

// Write package.json
function writePackage(pkgPath, pkg) {
  const content = JSON.stringify(pkg, null, '\t') + '\n';
  fs.writeFileSync(pkgPath, content, 'utf8');
}

// Parse version string
function parseVersion(version) {
  const parts = version.split('.');
  return {
    major: parseInt(parts[0], 10),
    minor: parseInt(parts[1], 10),
    patch: parseInt(parts[2], 10),
    prerelease: parts[3] || null
  };
}

// Increment patch version
function incrementPatch(version) {
  const parsed = parseVersion(version);
  return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
}

// Get current version from main package
function getCurrentVersion() {
  const lessPkgPath = path.join(PACKAGES_DIR, 'less', 'package.json');
  const pkg = readPackage(lessPkgPath);
  return pkg.version;
}

// Check if version was explicitly set (via environment variable or git commit message)
function getExplicitVersion() {
  // Check for explicit version in environment
  if (process.env.EXPLICIT_VERSION) {
    return process.env.EXPLICIT_VERSION;
  }
  
  // Check git commit message for version bump instruction
  try {
    const commitMsg = execSync('git log -1 --pretty=%B', { encoding: 'utf8' });
    const versionMatch = commitMsg.match(/version[:\s]+v?(\d+\.\d+\.\d+(?:-[a-z]+\.\d+)?)/i);
    if (versionMatch) {
      return versionMatch[1];
    }
  } catch (e) {
    // Ignore errors
  }
  
  return null;
}

// Update all package.json files with new version
function updateAllVersions(newVersion) {
  const packageFiles = getPackageFiles();
  const updated = [];
  
  for (const pkgPath of packageFiles) {
    const pkg = readPackage(pkgPath);
    if (pkg.version) {
      pkg.version = newVersion;
      writePackage(pkgPath, pkg);
      updated.push(pkgPath);
    }
  }
  
  return updated;
}

// Get packages that should be published (not private)
function getPublishablePackages() {
  const packageFiles = getPackageFiles();
  const publishable = [];
  
  for (const pkgPath of packageFiles) {
    const pkg = readPackage(pkgPath);
    // Skip root package and private packages
    if (!pkg.private && pkg.name && pkg.name !== '@less/root') {
      publishable.push({
        path: pkgPath,
        name: pkg.name,
        dir: path.dirname(pkgPath)
      });
    }
  }
  
  return publishable;
}

// Main function
function main() {
  const dryRun = process.env.DRY_RUN === 'true' || process.argv.includes('--dry-run');
  const branch = process.env.GITHUB_REF_NAME || execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
  const isAlpha = branch === 'alpha';
  const isMaster = branch === 'master';
  
  if (dryRun) {
    console.log(`🧪 DRY RUN MODE - No changes will be committed or published\n`);
  }
  
  // Enforce branch restrictions - only allow publishing from master or alpha branches
  if (!isMaster && !isAlpha) {
    console.error(`❌ ERROR: Publishing is only allowed from 'master' or 'alpha' branches`);
    console.error(`   Current branch: ${branch}`);
    console.error(`   Please switch to 'master' or 'alpha' branch before publishing`);
    process.exit(1);
  }
  
  console.log(`🚀 Starting publish process for branch: ${branch}`);
  
  // Get current version
  let currentVersion = getCurrentVersion();
  console.log(`📦 Current version: ${currentVersion}`);
  
  // Protection: If on alpha branch and version was overwritten by a merge from master
  if (isAlpha && !currentVersion.includes('-alpha.')) {
    console.log(`\n⚠️  WARNING: Alpha branch version (${currentVersion}) doesn't contain '-alpha.'`);
    console.log(`   This likely happened due to merging master into alpha.`);
    console.log(`   Attempting to restore alpha version...`);
    
    // Try to find the last alpha version from alpha branch history
    let restoredVersion = null;
    try {
      // Get recent commits on alpha that modified package.json
      const commits = execSync(
        'git log alpha --oneline -20 -- packages/less/package.json',
        { cwd: ROOT_DIR, encoding: 'utf8' }
      ).trim().split('\n');
      
      // Search through commits to find the last alpha version
      for (const commitLine of commits) {
        const commitHash = commitLine.split(' ')[0];
        try {
          const pkgContent = execSync(
            `git show ${commitHash}:packages/less/package.json 2>/dev/null`,
            { cwd: ROOT_DIR, encoding: 'utf8' }
          );
          const pkg = JSON.parse(pkgContent);
          if (pkg.version && pkg.version.includes('-alpha.')) {
            restoredVersion = pkg.version;
            console.log(`   Found previous alpha version in commit ${commitHash}: ${restoredVersion}`);
            break;
          }
        } catch (e) {
          // Continue to next commit
        }
      }
      
      if (restoredVersion) {
        // Increment the alpha number from the restored version
        const alphaMatch = restoredVersion.match(/^(\d+\.\d+\.\d+)-alpha\.(\d+)$/);
        if (alphaMatch) {
          const alphaNum = parseInt(alphaMatch[2], 10);
          const newAlphaVersion = `${alphaMatch[1]}-alpha.${alphaNum + 1}`;
          console.log(`   Restoring and incrementing to: ${newAlphaVersion}`);
          currentVersion = newAlphaVersion;
          updateAllVersions(newAlphaVersion);
        } else {
          console.log(`   Restoring to: ${restoredVersion}`);
          currentVersion = restoredVersion;
          updateAllVersions(restoredVersion);
        }
      } else {
        // No previous alpha version found, create one from current version
        const parsed = parseVersion(currentVersion);
        const nextMajor = parsed.major + 1;
        const newAlphaVersion = `${nextMajor}.0.0-alpha.1`;
        console.log(`   No previous alpha version found. Creating new: ${newAlphaVersion}`);
        currentVersion = newAlphaVersion;
        updateAllVersions(newAlphaVersion);
      }
    } catch (e) {
      // If we can't find previous version, create a new alpha version
      const parsed = parseVersion(currentVersion);
      const nextMajor = parsed.major + 1;
      const newAlphaVersion = `${nextMajor}.0.0-alpha.1`;
      console.log(`   Could not find previous alpha version. Creating: ${newAlphaVersion}`);
      currentVersion = newAlphaVersion;
      updateAllVersions(newAlphaVersion);
    }
    
    console.log(`✅ Restored/created alpha version: ${currentVersion}\n`);
  }
  
  // Determine next version
  const explicitVersion = getExplicitVersion();
  let nextVersion;
  
  if (explicitVersion) {
    nextVersion = explicitVersion;
    console.log(`✨ Using explicit version: ${nextVersion}`);
  } else if (isAlpha) {
    // For alpha branch, use alpha versions
    const parsed = parseVersion(currentVersion);
    if (parsed.prerelease) {
      // Already an alpha, increment alpha number
      const alphaMatch = currentVersion.match(/^(\d+\.\d+\.\d+)-alpha\.(\d+)$/);
      if (alphaMatch) {
        const alphaNum = parseInt(alphaMatch[2], 10);
        nextVersion = `${alphaMatch[1]}-alpha.${alphaNum + 1}`;
      } else {
        // Other prerelease format, determine base version and start alpha.1
        const baseVersion = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
        nextVersion = `${baseVersion}-alpha.1`;
      }
    } else {
      // Not an alpha version, determine next major and start alpha.1
      const parsed = parseVersion(currentVersion);
      const nextMajor = parsed.major + 1;
      nextVersion = `${nextMajor}.0.0-alpha.1`;
    }
    console.log(`🔢 Auto-incrementing alpha version: ${nextVersion}`);
  } else {
    // For master, increment patch
    nextVersion = incrementPatch(currentVersion);
    console.log(`🔢 Auto-incrementing patch version: ${nextVersion}`);
  }
  
  // Update all package.json files
  console.log(`📝 Updating all package.json files to version ${nextVersion}...`);
  const updated = updateAllVersions(nextVersion);
  console.log(`✅ Updated ${updated.length} package.json files`);
  
  // Get publishable packages
  const publishable = getPublishablePackages();
  console.log(`📦 Found ${publishable.length} publishable packages:`);
  publishable.forEach(pkg => console.log(`   - ${pkg.name}`));
  
  // Stage changes
  console.log(`📌 Staging version changes...`);
  if (!dryRun) {
    execSync('git add package.json packages/*/package.json', { cwd: ROOT_DIR, stdio: 'inherit' });
  } else {
    console.log(`   [DRY RUN] Would stage: package.json packages/*/package.json`);
  }
  
  // Commit
  console.log(`💾 Committing version bump...`);
  if (!dryRun) {
    try {
      execSync(`git commit -m "chore: bump version to ${nextVersion}"`, { 
        cwd: ROOT_DIR, 
        stdio: 'inherit' 
      });
    } catch (e) {
      // Commit might fail if nothing changed, that's okay
      console.log(`⚠️  Commit skipped (no changes or already committed)`);
    }
  } else {
    console.log(`   [DRY RUN] Would commit: "chore: bump version to ${nextVersion}"`);
  }
  
  // Create tag
  const tagName = `v${nextVersion}`;
  console.log(`🏷️  Creating git tag: ${tagName}...`);
  if (!dryRun) {
    try {
      execSync(`git tag -a "${tagName}" -m "Release ${tagName}"`, { 
        cwd: ROOT_DIR, 
        stdio: 'inherit' 
      });
    } catch (e) {
      console.log(`⚠️  Tag might already exist, continuing...`);
    }
  } else {
    console.log(`   [DRY RUN] Would create tag: ${tagName}`);
  }
  
  // Push commit and tag
  console.log(`📤 Pushing to ${branch}...`);
  if (!dryRun) {
    try {
      execSync(`git push origin ${branch}`, { cwd: ROOT_DIR, stdio: 'inherit' });
      execSync(`git push origin "${tagName}"`, { cwd: ROOT_DIR, stdio: 'inherit' });
    } catch (e) {
      console.log(`⚠️  Push failed, but continuing with publish...`);
    }
  } else {
    console.log(`   [DRY RUN] Would push to: origin ${branch}`);
    console.log(`   [DRY RUN] Would push tag: origin ${tagName}`);
  }
  
  // Validate alpha branch requirements
  if (isAlpha) {
    console.log(`\n🔍 Validating alpha branch requirements...`);
    
    // Validation 1: Version must contain 'alpha'
    if (!nextVersion.includes('-alpha.')) {
      console.error(`❌ ERROR: Alpha branch version must contain '-alpha.'`);
      console.error(`   Generated version: ${nextVersion}`);
      console.error(`   Expected format: X.Y.Z-alpha.N`);
      process.exit(1);
    }
    console.log(`✅ Version contains 'alpha' suffix: ${nextVersion}`);
    
    // Validation 2: Must publish with 'alpha' tag
    // (This is enforced in the code below, but we log it for clarity)
    console.log(`✅ Will publish with 'alpha' tag (enforced)`);
    
    // Validation 3: Check if alpha is behind master
    try {
      execSync('git fetch origin master:master 2>/dev/null || true', { cwd: ROOT_DIR });
      const masterCommits = execSync('git rev-list --count alpha..master 2>/dev/null || echo "0"', { 
        cwd: ROOT_DIR, 
        encoding: 'utf8' 
      }).trim();
      
      if (parseInt(masterCommits, 10) > 0) {
        console.error(`❌ ERROR: Alpha branch is behind master by ${masterCommits} commit(s)`);
        console.error(`   Alpha branch must include all commits from master before publishing`);
        console.error(`   Please merge master into alpha first`);
        process.exit(1);
      }
      console.log(`✅ Alpha branch is up to date with master`);
    } catch (e) {
      console.log(`⚠️  Could not verify master sync status, continuing...`);
    }
    
    // Validation 4: Alpha base version must be >= master version
    try {
      const masterVersionStr = execSync('git show master:packages/less/package.json 2>/dev/null', { 
        cwd: ROOT_DIR, 
        encoding: 'utf8' 
      });
      const masterPkg = JSON.parse(masterVersionStr);
      const masterVersion = masterPkg.version;
      
      // Extract base version from alpha version (remove -alpha.X)
      const alphaBase = nextVersion.replace(/-alpha\.\d+$/, '');
      
      // Semver comparison using semver library
      const isGreaterOrEqual = semver.gte(alphaBase, masterVersion);
      
      if (!isGreaterOrEqual) {
        console.error(`❌ ERROR: Alpha base version (${alphaBase}) is lower than master version (${masterVersion})`);
        console.error(`   According to semver, alpha base version must be >= master version`);
        process.exit(1);
      }
      console.log(`✅ Alpha base version (${alphaBase}) is >= master version (${masterVersion})`);
    } catch (e) {
      console.log(`⚠️  Could not compare with master version, continuing...`);
    }
  }
  
  // Determine NPM tag based on branch and version
  const npmTag = isAlpha ? 'alpha' : 'latest';
  const isAlphaVersion = nextVersion.includes('-alpha.');
  
  // Validation: Alpha versions must use 'alpha' tag, non-alpha versions must use 'latest' tag
  if (isAlphaVersion && npmTag !== 'alpha') {
    console.error(`❌ ERROR: Alpha version (${nextVersion}) must be published with 'alpha' tag, not '${npmTag}'`);
    console.error(`   Alpha versions cannot be published to 'latest' tag`);
    process.exit(1);
  }
  
  if (!isAlphaVersion && npmTag === 'alpha') {
    console.error(`❌ ERROR: Non-alpha version (${nextVersion}) cannot be published with 'alpha' tag`);
    console.error(`   Only versions containing '-alpha.' can be published to 'alpha' tag`);
    process.exit(1);
  }
  
  // Enforce alpha tag for alpha branch
  if (isAlpha && npmTag !== 'alpha') {
    console.error(`❌ ERROR: Alpha branch must publish with 'alpha' tag, not '${npmTag}'`);
    process.exit(1);
  }
  
  console.log(`\n📦 Publishing packages to NPM with tag: ${npmTag}...`);
  
  const publishErrors = [];
  
  for (const pkg of publishable) {
    console.log(`\n📤 Publishing ${pkg.name}...`);
    if (dryRun) {
      console.log(`   [DRY RUN] Would publish: ${pkg.name}@${nextVersion} with tag: ${npmTag}`);
      console.log(`   [DRY RUN] Command: npm publish --tag ${npmTag}`);
    } else {
      try {
        // For scoped packages, ensure access is set correctly
        const publishCmd = `npm publish --tag ${npmTag} --access public`;
        execSync(publishCmd, { 
          cwd: pkg.dir, 
          stdio: 'inherit',
          env: { ...process.env, NODE_AUTH_TOKEN: process.env.NPM_TOKEN }
        });
        console.log(`✅ Successfully published ${pkg.name}@${nextVersion}`);
      } catch (e) {
        const errorMsg = e.message || String(e);
        console.error(`❌ Failed to publish ${pkg.name}: ${errorMsg}`);
        publishErrors.push({ name: pkg.name, error: errorMsg });
        // Continue with other packages instead of exiting immediately
      }
    }
  }
  
  // Report any publish errors at the end
  if (publishErrors.length > 0) {
    console.error(`\n❌ Publishing completed with ${publishErrors.length} error(s):`);
    publishErrors.forEach(({ name, error }) => {
      console.error(`   - ${name}: ${error}`);
    });
    console.error(`\n⚠️  Note: Version bump and commit were successful.`);
    console.error(`   Some packages failed to publish. You may need to publish them manually.`);
    process.exit(1);
  }
  
  if (dryRun) {
    console.log(`\n🧪 DRY RUN COMPLETE - No changes were made`);
    console.log(`   Would publish version: ${nextVersion}`);
    console.log(`   Would create tag: ${tagName}`);
    console.log(`   Would use NPM tag: ${npmTag}`);
  } else {
    console.log(`\n🎉 Successfully published all packages!`);
    console.log(`   Version: ${nextVersion}`);
    console.log(`   Tag: ${tagName}`);
    console.log(`   NPM Tag: ${npmTag}`);
  }
  
  // Output version for GitHub Actions
  if (process.env.GITHUB_OUTPUT) {
    const fs = require('fs');
    fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${nextVersion}\n`);
    fs.appendFileSync(process.env.GITHUB_OUTPUT, `tag=${tagName}\n`);
  }
  
  return { version: nextVersion, tag: tagName };
}

// Run if called directly
if (require.main === module) {
  main();
}

module.exports = { main };
