#!/usr/bin/env node
/**
 * Extract Business Rules from SmartStack Domain Entities and Unit Tests
 *
 * Usage:
 *   node extract-business-rules.ts --module users --app-path "D:/01 - projets/SmartStack.app/02-Develop"
 *
 * Output: JSON array of DocBusinessRule objects
 */

import * as fs from 'fs';
import * as path from 'path';
import { glob } from 'glob';

interface DocBusinessRule {
  id: string;           // e.g., "BR-001"
  name: string;         // e.g., "Email requis"
  category: string;     // e.g., "Validation", "Sécurité", "Logique métier"
  statement: string;    // Human-readable business rule description
}

interface ExtractedRule {
  source: 'domain' | 'test';
  exception: string;     // Exception message
  context: string;       // Method or test name
  category?: string;
}

/**
 * Parse command line arguments
 */
function parseArgs(): { module: string; appPath: string } {
  const args = process.argv.slice(2);
  const moduleIndex = args.indexOf('--module');
  const appPathIndex = args.indexOf('--app-path');

  if (moduleIndex === -1 || appPathIndex === -1) {
    console.error('Usage: node extract-business-rules.ts --module <module-name> --app-path <path-to-app>');
    process.exit(1);
  }

  return {
    module: args[moduleIndex + 1],
    appPath: args[appPathIndex + 1],
  };
}

/**
 * Find domain entity file for given module
 * Tries both singular and plural forms (e.g., users → User.cs)
 */
async function findEntityFile(appPath: string, module: string): Promise<string | null> {
  const domainPath = path.join(appPath, 'src/SmartStack.Domain');

  // Try singular form first (most common: users → User.cs)
  const singularForm = capitalize(singularize(module));
  const singularPattern = `**/${singularForm}.cs`;

  try {
    let files = await glob(singularPattern, { cwd: domainPath, absolute: true });
    if (files.length > 0) {
      return files[0];
    }

    // Fallback to plural form
    const pluralForm = capitalize(module);
    const pluralPattern = `**/${pluralForm}.cs`;
    files = await glob(pluralPattern, { cwd: domainPath, absolute: true });
    return files.length > 0 ? files[0] : null;
  } catch (error) {
    console.error('Error finding entity:', error);
    return null;
  }
}

/**
 * Find unit test file for given entity
 * Tries both singular and plural forms (e.g., users → UserTests.cs)
 */
async function findTestFile(appPath: string, module: string): Promise<string | null> {
  const testsPath = path.join(appPath, 'tests/SmartStack.Tests.Unit/Domain');

  // Try singular form first (most common: users → UserTests.cs)
  const singularForm = capitalize(singularize(module));
  const singularPattern = `**/${singularForm}Tests.cs`;

  try {
    let files = await glob(singularPattern, { cwd: testsPath, absolute: true });
    if (files.length > 0) {
      return files[0];
    }

    // Fallback to plural form
    const pluralForm = capitalize(module);
    const pluralPattern = `**/${pluralForm}Tests.cs`;
    files = await glob(pluralPattern, { cwd: testsPath, absolute: true });
    return files.length > 0 ? files[0] : null;
  } catch (error) {
    console.error('Error finding test file:', error);
    return null;
  }
}

/**
 * Extract business rules from domain entity
 * Looks for:
 * - Guard clauses with thrown DomainExceptions
 * - Validation logic in Create/Update methods
 */
function extractFromDomainEntity(entityPath: string): ExtractedRule[] {
  const content = fs.readFileSync(entityPath, 'utf-8');
  const lines = content.split('\n');
  const rules: ExtractedRule[] = [];

  let currentMethod = '';

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];

    // Track current method for context
    const methodMatch = line.match(/public\s+(?:static\s+)?(?:\w+\??)\s+(\w+)\s*\(/);
    if (methodMatch) {
      currentMethod = methodMatch[1];
    }

    // Look for throw statements with DomainException
    // Example: throw new DomainException("Email is required");
    const throwMatch = line.match(/throw\s+new\s+(?:\w+\.)?DomainException\s*\(\s*"([^"]+)"\s*\)/);
    if (throwMatch) {
      const exception = throwMatch[1];
      rules.push({
        source: 'domain',
        exception,
        context: currentMethod,
        category: inferCategory(exception, currentMethod),
      });
      continue;
    }

    // Look for guard clauses (if ... throw pattern)
    // Example: if (string.IsNullOrWhiteSpace(email))
    const guardMatch = line.match(/if\s*\((.+)\)/);
    if (guardMatch && i + 1 < lines.length) {
      const nextLine = lines[i + 1].trim();
      const guardThrow = nextLine.match(/throw\s+new\s+(?:\w+\.)?DomainException\s*\(\s*"([^"]+)"\s*\)/);
      if (guardThrow) {
        const condition = guardMatch[1].trim();
        const exception = guardThrow[1];
        rules.push({
          source: 'domain',
          exception,
          context: `${currentMethod} (${condition})`,
          category: inferCategory(exception, currentMethod),
        });
      }
    }
  }

  return rules;
}

/**
 * Extract business rules from unit tests
 * Looks for test methods that assert DomainExceptions
 */
function extractFromTests(testPath: string): ExtractedRule[] {
  const content = fs.readFileSync(testPath, 'utf-8');
  const lines = content.split('\n');
  const rules: ExtractedRule[] = [];

  let currentTestName = '';

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];

    // Track current test method
    // Example: public void Create_WithEmptyEmail_ThrowsDomainException()
    const testMatch = line.match(/public\s+(?:void|Task)\s+(\w+)\s*\(/);
    if (testMatch) {
      currentTestName = testMatch[1];
    }

    // Look for assertion patterns
    // Example: .Should().Throw<DomainException>().WithMessage("*email*")
    const assertMatch = line.match(/Should\(\)\.Throw<DomainException>\(\)(?:\.WithMessage\("([^"]+)"\))?/);
    if (assertMatch && currentTestName) {
      const message = assertMatch[1] || '';
      const exception = extractExceptionFromTestName(currentTestName, message);

      rules.push({
        source: 'test',
        exception,
        context: currentTestName,
        category: inferCategoryFromTestName(currentTestName),
      });
    }
  }

  return rules;
}

/**
 * Infer business rule exception message from test name
 * Example: Create_WithEmptyEmail_ThrowsDomainException → "Email is required"
 */
function extractExceptionFromTestName(testName: string, messageHint: string): string {
  // Parse test name pattern: Method_With{Condition}_{ExpectedBehavior}
  const parts = testName.split('_');

  if (parts.length >= 3) {
    const condition = parts[1].replace(/^With/, '');

    // Handle common patterns
    if (condition.includes('Empty')) {
      const field = condition.replace('Empty', '').toLowerCase();
      return `${capitalize(field)} is required`;
    }

    if (condition.includes('Invalid')) {
      const field = condition.replace('Invalid', '').toLowerCase();
      return `Invalid ${field}`;
    }

    if (condition.includes('Duplicate')) {
      const field = condition.replace('Duplicate', '').toLowerCase();
      return `${capitalize(field)} must be unique`;
    }
  }

  // Fallback to message hint or test name
  return messageHint.replace(/\*/g, '') || testName;
}

/**
 * Infer category from exception message or method context
 */
function inferCategory(exception: string, context: string): string {
  const lower = exception.toLowerCase();

  if (lower.includes('required') || lower.includes('empty') || lower.includes('null')) {
    return 'Validation';
  }

  if (lower.includes('unique') || lower.includes('duplicate') || lower.includes('exist')) {
    return 'Validation';
  }

  if (lower.includes('password') || lower.includes('token') || lower.includes('auth')) {
    return 'Sécurité';
  }

  if (lower.includes('permission') || lower.includes('role') || lower.includes('access')) {
    return 'Autorisation';
  }

  if (lower.includes('tenant') || lower.includes('isolation')) {
    return 'Multi-tenant';
  }

  if (lower.includes('email') || lower.includes('notification')) {
    return 'Communication';
  }

  if (lower.includes('audit') || lower.includes('log') || lower.includes('track')) {
    return 'Audit';
  }

  if (context.includes('Create') || context.includes('Update') || context.includes('Delete')) {
    return 'Logique métier';
  }

  return 'Autre';
}

/**
 * Infer category from test name
 */
function inferCategoryFromTestName(testName: string): string {
  if (testName.includes('Empty') || testName.includes('Invalid') || testName.includes('Required')) {
    return 'Validation';
  }

  if (testName.includes('Password') || testName.includes('Token')) {
    return 'Sécurité';
  }

  if (testName.includes('Permission') || testName.includes('Role')) {
    return 'Autorisation';
  }

  if (testName.includes('Unique') || testName.includes('Duplicate')) {
    return 'Validation';
  }

  return 'Logique métier';
}

/**
 * Merge and deduplicate rules from domain and tests
 */
function mergeRules(domainRules: ExtractedRule[], testRules: ExtractedRule[]): ExtractedRule[] {
  const merged: ExtractedRule[] = [...domainRules];
  const existingExceptions = new Set(domainRules.map((r) => r.exception.toLowerCase()));

  for (const testRule of testRules) {
    // Only add test rule if not already covered by domain rule
    if (!existingExceptions.has(testRule.exception.toLowerCase())) {
      merged.push(testRule);
      existingExceptions.add(testRule.exception.toLowerCase());
    }
  }

  return merged;
}

/**
 * Convert extracted rules to DocBusinessRule format
 */
function formatAsBusinessRules(rules: ExtractedRule[]): DocBusinessRule[] {
  return rules.map((rule, index) => {
    // Generate BR-XXX ID
    const id = `BR-${String(index + 1).padStart(3, '0')}`;

    // Generate short name from exception
    const name = generateRuleName(rule.exception);

    // Generate full statement
    const statement = rule.exception;

    return {
      id,
      name,
      category: rule.category || 'Autre',
      statement,
    };
  });
}

/**
 * Generate short rule name from exception message
 * Example: "Email is required" → "Email requis"
 * Example: "Password must be at least 8 characters" → "Mot de passe fort"
 */
function generateRuleName(exception: string): string {
  const lower = exception.toLowerCase();

  if (lower.includes('required') || lower.includes('is required')) {
    const field = exception.split(' ')[0];
    return `${field} requis`;
  }

  if (lower.includes('unique') || lower.includes('must be unique')) {
    const field = exception.split(' ')[0];
    return `Unicité ${field.toLowerCase()}`;
  }

  if (lower.includes('password') && (lower.includes('least') || lower.includes('strong'))) {
    return 'Mot de passe fort';
  }

  if (lower.includes('email') && lower.includes('valid')) {
    return 'Email valide';
  }

  // Fallback: Take first 3-4 words
  const words = exception.split(' ').slice(0, 4).join(' ');
  return words.length > 50 ? words.substring(0, 47) + '...' : words;
}

/**
 * Capitalize first letter
 */
function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

/**
 * Simple singularize function for common English plurals
 */
function singularize(str: string): string {
  if (str.endsWith('ies')) {
    return str.slice(0, -3) + 'y';
  }
  if (str.endsWith('sses') || str.endsWith('shes') || str.endsWith('ches') || str.endsWith('xes')) {
    return str.slice(0, -2);
  }
  if (str.endsWith('s') && !str.endsWith('ss')) {
    return str.slice(0, -1);
  }
  return str;
}

/**
 * Main execution
 */
async function main() {
  const { module, appPath } = parseArgs();

  console.error(`Extracting business rules for module: ${module}`);
  console.error(`App path: ${appPath}`);

  // Find entity file
  const entityPath = await findEntityFile(appPath, module);
  if (!entityPath) {
    console.error(`Entity not found for module: ${module}`);
    process.exit(1);
  }

  console.error(`Found entity: ${entityPath}`);

  // Extract rules from domain
  const domainRules = extractFromDomainEntity(entityPath);
  console.error(`Extracted ${domainRules.length} rules from domain entity`);

  // Find and extract from tests (optional)
  const testPath = await findTestFile(appPath, module);
  let testRules: ExtractedRule[] = [];

  if (testPath) {
    console.error(`Found test file: ${testPath}`);
    testRules = extractFromTests(testPath);
    console.error(`Extracted ${testRules.length} rules from unit tests`);
  } else {
    console.error(`No test file found for ${module}`);
  }

  // Merge and format
  const merged = mergeRules(domainRules, testRules);
  const businessRules = formatAsBusinessRules(merged);

  console.error(`Total business rules: ${businessRules.length}`);

  // Output JSON to stdout
  console.log(JSON.stringify(businessRules, null, 2));
}

main().catch((error) => {
  console.error('Fatal error:', error);
  process.exit(1);
});
