#!/usr/bin/env tsx
/**
 * Generate TypeScript types from OpenAPI spec
 *
 * This script uses openapi-typescript to generate proper TypeScript types
 * from the OpenAPI spec that NestJS/Swagger generates.
 *
 * Usage:
 *   1. First generate the OpenAPI spec: npm run api:generate-spec
 *   2. Then generate types: npm run api:generate-types
 *
 * Or use: npm run api:sync-types (runs both)
 */

import { createHash } from 'crypto';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

async function sha256OfFile(filePath: string): Promise<string> {
  const buffer = await fs.readFile(filePath);
  return createHash('sha256').update(buffer).digest('hex');
}

async function generateTypes() {
  const openapiPath = path.join(__dirname, '../apps/api/openapi.json');
  const outputDir = path.join(__dirname, '../apps/website/lib/types/generated');

  console.log('🔄 Generating TypeScript types from OpenAPI spec...');

  // Check if OpenAPI spec exists
  try {
    await fs.access(openapiPath);
  } catch {
    console.error(`❌ OpenAPI spec not found at: ${openapiPath}`);
    console.error('Restore the committed OpenAPI contract or run "npm run api:generate-spec" to regenerate it.');
    process.exit(1);
  }

  // Ensure output directory exists
  await fs.mkdir(outputDir, { recursive: true });

  try {
    const specSha256 = await sha256OfFile(openapiPath);

    // Generate individual DTO files + barrel index for deterministic imports
    await generateIndividualDtoFiles(openapiPath, outputDir, specSha256);
  } catch (error) {
    console.error('❌ Failed to generate types:', error);
    process.exit(1);
  }
}

async function generateIndividualDtoFiles(openapiPath: string, outputDir: string, specSha256: string) {
  console.log('📝 Generating individual DTO files...');

  const specContent = await fs.readFile(openapiPath, 'utf-8');
  const spec = JSON.parse(specContent);
  const schemas = spec.components?.schemas || {};

  const schemaNames = Object.keys(schemas).sort((a: string, b: string) => a.localeCompare(b));
  
  // Get existing files in output directory
  let existingFiles: string[] = [];
  try {
    existingFiles = await fs.readdir(outputDir);
    existingFiles = existingFiles.filter(f => f.endsWith('.ts'));
  } catch (error) {
    // Directory doesn't exist yet
  }
  
  // Generate individual files for each schema
  const generatedFileNames: string[] = [];
  for (const schemaName of schemaNames) {
    const schema = schemas[schemaName];

    // File name should match the schema name exactly
    const fileName = `${schemaName}.ts`;
    const filePath = path.join(outputDir, fileName);

    const fileContent = generateDtoFileContent(schemaName, schema, schemas, specSha256);
    await fs.writeFile(filePath, fileContent);
    console.log(`  ✅ Generated ${fileName}`);
    generatedFileNames.push(fileName);
  }

  const indexFileName = 'index.ts';
  const indexFilePath = path.join(outputDir, indexFileName);
  const indexFileContent = generateIndexFileContent(schemaNames, specSha256);
  await fs.writeFile(indexFilePath, indexFileContent);
  console.log(`  ✅ Generated ${indexFileName}`);
  generatedFileNames.push(indexFileName);

  // Clean up files that are no longer in the spec
  const filesToRemove = existingFiles.filter(f => !generatedFileNames.includes(f));
  for (const file of filesToRemove) {
    const filePath = path.join(outputDir, file);
    await fs.unlink(filePath);
    console.log(`  🗑️  Removed obsolete file: ${file}`);
  }

  console.log(`✅ Generated ${schemaNames.length} individual DTO files at: ${outputDir}`);
  if (filesToRemove.length > 0) {
    console.log(`🧹 Cleaned up ${filesToRemove.length} obsolete files`);
  }
}

function generateIndexFileContent(schemaNames: string[], specSha256: string): string {
  let content = `/**
 * Auto-generated barrel for API DTO types.
 * Spec SHA256: ${specSha256}
 * This file is generated by scripts/generate-api-types.ts
 * Do not edit manually - regenerate using: npm run api:generate-types
 */
`;
  for (const schemaName of schemaNames) {
    content += `\nexport type { ${schemaName} } from './${schemaName}';`;
  }
  content += '\n';
  return content;
}

function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Record<string, any>, specSha256: string): string {
  // Collect dependencies (referenced DTOs)
  const dependencies = new Set<string>();
  collectDependencies(schema, dependencies, allSchemas);
  dependencies.delete(schemaName); // Remove self-reference

  const sortedDependencies = Array.from(dependencies).sort((a, b) => a.localeCompare(b));

  let content = `/**
 * Auto-generated DTO from OpenAPI spec
 * Spec SHA256: ${specSha256}
 * This file is generated by scripts/generate-api-types.ts
 * Do not edit manually - regenerate using: npm run api:generate-types
 */

`;

  // Add imports for dependencies (sorted for deterministic output)
  for (const dep of sortedDependencies) {
    content += `import type { ${dep} } from './${dep}';\n`;
  }

  if (sortedDependencies.length > 0) {
    content += '\n';
  }

  // Generate interface - use the schema name directly
  content += `export interface ${schemaName} {\n`;
  
  const properties = schema.properties || {};
  const required = new Set(schema.required || []);
  
  for (const [propName, propSchema] of Object.entries(properties)) {
    const isRequired = required.has(propName);
    const optionalMark = isRequired ? '' : '?';
    const typeStr = schemaToTypeString(propSchema as any, allSchemas);
    
    // Add JSDoc comment for format
    if ((propSchema as any).format) {
      content += `  /** Format: ${(propSchema as any).format} */\n`;
    }
    
    content += `  ${propName}${optionalMark}: ${typeStr};\n`;
  }
  
  content += '}\n';
  
  return content;
}

function collectDependencies(schema: any, deps: Set<string>, allSchemas: Record<string, any>): void {
  if (!schema) return;
  
  if (schema.$ref) {
    const refName = schema.$ref.split('/').pop();
    if (refName && allSchemas[refName]) {
      deps.add(refName);
    }
    return;
  }
  
  if (schema.type === 'array' && schema.items) {
    collectDependencies(schema.items, deps, allSchemas);
    return;
  }
  
  if (schema.properties) {
    for (const propSchema of Object.values(schema.properties)) {
      collectDependencies(propSchema, deps, allSchemas);
    }
  }

  if (schema.additionalProperties) {
    collectDependencies(schema.additionalProperties, deps, allSchemas);
  }
  
  if (schema.oneOf) {
    for (const subSchema of schema.oneOf) {
      collectDependencies(subSchema, deps, allSchemas);
    }
  }
  
  if (schema.anyOf) {
    for (const subSchema of schema.anyOf) {
      collectDependencies(subSchema, deps, allSchemas);
    }
  }
  
  if (schema.allOf) {
    for (const subSchema of schema.allOf) {
      collectDependencies(subSchema, deps, allSchemas);
    }
  }
}

function schemaToTypeString(schema: any, allSchemas: Record<string, any>): string {
  if (!schema) return 'unknown';

  if (schema.$ref) {
    const refName = schema.$ref.split('/').pop();
    if (!refName) return 'unknown';
    return allSchemas[refName] ? refName : 'unknown';
  }

  if (schema.type === 'array') {
    const itemType = schemaToTypeString(schema.items, allSchemas);
    return `${itemType}[]`;
  }

  if (schema.type === 'object') {
    if (schema.additionalProperties) {
      const valueType = schemaToTypeString(schema.additionalProperties, allSchemas);
      return `Record<string, ${valueType}>`;
    }

    if (schema.properties) {
      // Inline object type
      const props = Object.entries(schema.properties)
        .map(([key, val]) => `${key}: ${schemaToTypeString(val as any, allSchemas)}`)
        .join('; ');
      return `{ ${props} }`;
    }
    return 'Record<string, unknown>';
  }

  if (schema.oneOf) {
    return schema.oneOf.map((s: any) => schemaToTypeString(s, allSchemas)).join(' | ');
  }

  if (schema.anyOf) {
    return schema.anyOf.map((s: any) => schemaToTypeString(s, allSchemas)).join(' | ');
  }

  if (schema.enum) {
    return schema.enum.map((v: any) => JSON.stringify(v)).join(' | ');
  }
  
  switch (schema.type) {
    case 'string':
      return 'string';
    case 'number':
    case 'integer':
      return 'number';
    case 'boolean':
      return 'boolean';
    case 'null':
      return 'null';
    default:
      return 'unknown';
  }
}

generateTypes().catch(console.error);
