#!/usr/bin/env tsx
/**
 * Contract Compatibility Verification Script
 * 
 * This script verifies that the API contracts are compatible with the website
 * by running the type generation and then checking for breaking changes.
 */

import { execSync } from 'child_process';
import * as fs from 'fs/promises';
import * as path from 'path';
import { glob } from 'glob';

interface OpenAPISchema {
  type?: string;
  format?: string;
  $ref?: string;
  items?: OpenAPISchema;
  properties?: Record<string, OpenAPISchema>;
  required?: string[];
  enum?: string[];
  nullable?: boolean;
  description?: string;
}

interface OpenAPISpec {
  openapi: string;
  info: {
    title: string;
    description: string;
    version: string;
  };
  paths: Record<string, any>;
  components: {
    schemas: Record<string, OpenAPISchema>;
  };
}

interface ContractChange {
  type: 'added' | 'removed' | 'modified' | 'breaking';
  dto: string;
  property?: string;
  details: string;
}

const colors = {
  reset: '\x1b[0m',
  green: '\x1b[32m',
  red: '\x1b[31m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  dim: '\x1b[2m'
};

async function runContractCompatibilityCheck(): Promise<void> {
  console.log(`${colors.blue}🔍 Running Contract Compatibility Check...${colors.reset}\n`);

  const apiRoot = path.join(__dirname, '../apps/api');
  const websiteRoot = path.join(__dirname, '../apps/website');
  const openapiPath = path.join(apiRoot, 'openapi.json');
  const generatedTypesDir = path.join(websiteRoot, 'lib/types/generated');
  const backupDir = path.join(__dirname, '../.backup/contract-types');

  // Step 1: Generate current OpenAPI spec
  console.log(`${colors.yellow}1. Generating OpenAPI spec...${colors.reset}`);
  try {
    execSync('npm run api:generate-spec', { stdio: 'inherit' });
  } catch (error) {
    console.error(`${colors.red}❌ Failed to generate OpenAPI spec${colors.reset}`);
    process.exit(1);
  }

  // Step 2: Backup current generated types
  console.log(`${colors.yellow}2. Backing up current generated types...${colors.reset}`);
  await fs.mkdir(backupDir, { recursive: true });
  try {
    const files = await fs.readdir(generatedTypesDir);
    for (const file of files) {
      if (file.endsWith('.ts')) {
        const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');
        await fs.writeFile(path.join(backupDir, file), content);
      }
    }
  } catch (error) {
    console.log(`${colors.yellow}⚠️  No existing types to backup${colors.reset}`);
  }

  // Step 3: Generate new types
  console.log(`${colors.yellow}3. Generating new types...${colors.reset}`);
  try {
    execSync('npm run api:generate-types', { stdio: 'inherit' });
  } catch (error) {
    console.error(`${colors.red}❌ Failed to generate types${colors.reset}`);
    process.exit(1);
  }

  // Step 4: Compare and detect changes
  console.log(`${colors.yellow}4. Analyzing contract changes...${colors.reset}`);
  const changes = await detectContractChanges(backupDir, generatedTypesDir, openapiPath);

  // Step 5: Report results
  console.log(`${colors.yellow}5. Reporting changes...${colors.reset}\n`);
  await reportChanges(changes);

  // Step 6: Clean up backup
  console.log(`${colors.yellow}6. Cleaning up...${colors.reset}`);
  await fs.rm(backupDir, { recursive: true, force: true });

  console.log(`\n${colors.green}✅ Contract compatibility check completed!${colors.reset}`);
}

async function detectContractChanges(
  backupDir: string,
  currentDir: string,
  openapiPath: string
): Promise<ContractChange[]> {
  const changes: ContractChange[] = [];

  // Read OpenAPI spec
  const specContent = await fs.readFile(openapiPath, 'utf-8');
  const spec: OpenAPISpec = JSON.parse(specContent);
  const schemas = spec.components.schemas;

  // Get current and backup files
  const currentFiles = await fs.readdir(currentDir);
  const backupFiles = await fs.readdir(backupDir);
  
  const currentDTOs = currentFiles.filter(f => f.endsWith('.ts')).map(f => f.replace('.ts', ''));
  const backupDTOs = backupFiles.filter(f => f.endsWith('.ts')).map(f => f.replace('.ts', ''));

  // Check for removed DTOs
  for (const backupDTO of backupDTOs) {
    if (!currentDTOs.includes(backupDTO)) {
      changes.push({
        type: 'removed',
        dto: backupDTO,
        details: `DTO ${backupDTO} was removed`
      });
    }
  }

  // Check for added and modified DTOs
  for (const currentDTO of currentDTOs) {
    const currentPath = path.join(currentDir, `${currentDTO}.ts`);
    const backupPath = path.join(backupDir, `${currentDTO}.ts`);

    const currentContent = await fs.readFile(currentPath, 'utf-8');
    const backupExists = backupDTOs.includes(currentDTO);

    if (!backupExists) {
      changes.push({
        type: 'added',
        dto: currentDTO,
        details: `New DTO ${currentDTO} was added`
      });
    } else {
      const backupContent = await fs.readFile(backupPath, 'utf-8');
      
      // Check for property changes
      const schema = schemas[currentDTO];
      if (schema && schema.properties) {
        const currentProps = extractProperties(currentContent);
        const backupProps = extractProperties(backupContent);

        // Check for removed properties
        for (const [propName, backupProp] of Object.entries(backupProps)) {
          if (!currentProps[propName]) {
            const isRequired = schema.required?.includes(propName);
            changes.push({
              type: isRequired ? 'breaking' : 'modified',
              dto: currentDTO,
              property: propName,
              details: `Property ${propName} was removed${isRequired ? ' (BREAKING)' : ''}`
            });
          }
        }

        // Check for added properties
        for (const [propName, currentProp] of Object.entries(currentProps)) {
          if (!backupProps[propName]) {
            const isRequired = schema.required?.includes(propName);
            changes.push({
              type: isRequired ? 'breaking' : 'added',
              dto: currentDTO,
              property: propName,
              details: `Property ${propName} was added${isRequired ? ' (potentially breaking)' : ''}`
            });
          }
        }

        // Check for type changes
        for (const [propName, currentProp] of Object.entries(currentProps)) {
          if (backupProps[propName]) {
            const backupProp = backupProps[propName];
            if (currentProp.type !== backupProp.type) {
              changes.push({
                type: 'breaking',
                dto: currentDTO,
                property: propName,
                details: `Property ${propName} type changed from ${backupProp.type} to ${currentProp.type} (BREAKING)`
              });
            }
          }
        }
      }
    }
  }

  return changes;
}

function extractProperties(content: string): Record<string, { type: string; optional: boolean }> {
  const properties: Record<string, { type: string; optional: boolean }> = {};
  
  // Match property lines: propertyName?: type;
  const propertyRegex = /^\s*(\w+)(\??):\s*([^;]+);/gm;
  let match;
  
  while ((match = propertyRegex.exec(content)) !== null) {
    const [, name, optional, type] = match;
    properties[name] = {
      type: type.trim(),
      optional: !!optional
    };
  }
  
  return properties;
}

async function reportChanges(changes: ContractChange[]): Promise<void> {
  if (changes.length === 0) {
    console.log(`${colors.green}✅ No changes detected - contracts are stable${colors.reset}`);
    return;
  }

  const breaking = changes.filter(c => c.type === 'breaking');
  const modified = changes.filter(c => c.type === 'modified');
  const added = changes.filter(c => c.type === 'added');
  const removed = changes.filter(c => c.type === 'removed');

  if (breaking.length > 0) {
    console.log(`${colors.red}🚨 BREAKING CHANGES DETECTED:${colors.reset}`);
    breaking.forEach(change => {
      console.log(`  ${colors.red}• ${change.dto}${change.property ? '.' + change.property : ''}: ${change.details}${colors.reset}`);
    });
    console.log('');
  }

  if (removed.length > 0) {
    console.log(`${colors.red}❌ REMOVED:${colors.reset}`);
    removed.forEach(change => {
      console.log(`  ${colors.red}• ${change.dto}: ${change.details}${colors.reset}`);
    });
    console.log('');
  }

  if (modified.length > 0) {
    console.log(`${colors.yellow}⚠️  MODIFIED:${colors.reset}`);
    modified.forEach(change => {
      console.log(`  ${colors.yellow}• ${change.dto}.${change.property}: ${change.details}${colors.reset}`);
    });
    console.log('');
  }

  if (added.length > 0) {
    console.log(`${colors.green}➕ ADDED:${colors.reset}`);
    added.forEach(change => {
      console.log(`  ${colors.green}• ${change.dto}${change.property ? '.' + change.property : ''}: ${change.details}${colors.reset}`);
    });
    console.log('');
  }

  const totalChanges = changes.length;
  console.log(`${colors.blue}📊 Summary: ${totalChanges} total changes (${breaking.length} breaking, ${removed.length} removed, ${modified.length} modified, ${added.length} added)${colors.reset}`);

  if (breaking.length > 0) {
    console.log(`\n${colors.red}❌ Contract compatibility check FAILED due to breaking changes${colors.reset}`);
    process.exit(1);
  }
}

// Run if called directly
if (require.main === module) {
  runContractCompatibilityCheck().catch(error => {
    console.error(`${colors.red}❌ Error running contract compatibility check:${colors.reset}`, error);
    process.exit(1);
  });
}