#!/usr/bin/env node
/**
 * Module Generator Script
 *
 * Generates the standard platform-service module structure:
 * types.ts → repository.ts → routes.ts → <name>.test.ts
 *
 * Also auto-patches:
 * - cosmos-init.ts — adds container definition
 * - server.ts — adds route import + registration
 *
 * Usage:
 *   pnpm gen:module --name tasks --fields "title:string,status:enum(pending,active,done),priority:number" --partition userId
 *   pnpm gen:module --name tasks --fields "title:string,done:boolean?" --partition userId --dry-run
 *
 * @module platform-service/scripts/gen-module
 */

/* eslint-disable no-console -- This generator is a CLI; console output is its user interface. */

import { promises as fs } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SRC_DIR = path.join(__dirname, '..', 'src');

// ── CLI ──────────────────────────────────────────────────────────────────────

interface Options {
  name: string;
  fields: string;
  partition: string;
  dryRun: boolean;
}

function parseArgs(): Options {
  const args = process.argv.slice(2);
  const options: Options = { name: '', fields: '', partition: 'userId', dryRun: false };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--name' || arg === '-n') options.name = args[++i];
    else if (arg === '--fields' || arg === '-f') options.fields = args[++i];
    else if (arg === '--partition' || arg === '-p') options.partition = args[++i];
    else if (arg === '--dry-run' || arg === '-d') options.dryRun = true;
    else if (arg === '--help' || arg === '-h') {
      showHelp();
      process.exit(0);
    }
  }

  if (!options.name || !options.fields) {
    console.error('Error: --name and --fields are required');
    showHelp();
    process.exit(1);
  }

  // Validate module name: lowercase, no special chars
  if (!/^[a-z][a-z0-9-]*$/.test(options.name)) {
    console.error('Error: --name must be lowercase alphanumeric with optional hyphens');
    process.exit(1);
  }

  return options;
}

function showHelp(): void {
  console.log(`
Module Generator — Create platform-service module scaffolding

Usage:
  pnpm gen:module --name <name> --fields "<field:type,...>" [--partition <key>] [--dry-run]

Options:
  --name, -n         Module name (e.g., "tasks", "alerts")
  --fields, -f       Comma-separated field definitions
  --partition, -p    Partition key field name (default: "userId")
  --dry-run, -d      Preview generated files without writing
  --help, -h         Show this help

Field Types:
  string             z.string()
  number             z.number()
  boolean            z.boolean()
  date               z.string().datetime()   (ISO string)
  datetime           z.string().datetime()   (ISO string with time)
  enum(a,b,c)        z.enum(["a","b","c"])
  string[]           z.array(z.string())
  number[]           z.array(z.number())

Append ? to make a field optional:  priority:number?

Examples:
  pnpm gen:module --name tasks \\
    --fields "title:string,status:enum(pending,active,done),priority:number?" \\
    --partition userId

  pnpm gen:module --name alerts \\
    --fields "message:string,level:enum(info,warn,error),read:boolean" \\
    --partition userId --dry-run
`);
}

// ── Field Parser ─────────────────────────────────────────────────────────────

interface ParsedField {
  name: string;
  type: string;
  optional: boolean;
  zodType: string;
  tsType: string;
  enumValues: string[] | null;
}

function splitFields(str: string): string[] {
  const parts: string[] = [];
  let depth = 0;
  let current = '';
  for (const ch of str) {
    if (ch === '(') depth++;
    if (ch === ')') depth--;
    if (ch === ',' && depth === 0) {
      parts.push(current.trim());
      current = '';
    } else {
      current += ch;
    }
  }
  if (current.trim()) parts.push(current.trim());
  return parts;
}

function parseFields(fieldsStr: string): ParsedField[] {
  const fields: ParsedField[] = [];
  const fieldDefs = splitFields(fieldsStr);

  for (const raw of fieldDefs) {
    const def = raw.trim();
    if (!def) continue;

    const optional = def.endsWith('?');
    const cleaned = optional ? def.slice(0, -1) : def;
    const colonIdx = cleaned.indexOf(':');
    if (colonIdx === -1) throw new Error(`Invalid field (missing type): ${def}`);

    const name = cleaned.slice(0, colonIdx).trim();
    const type = cleaned.slice(colonIdx + 1).trim();

    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
      throw new Error(`Invalid field name: ${name}`);
    }

    let zodType: string;
    let tsType: string;
    let enumValues: string[] | null = null;

    if (type.startsWith('enum(') && type.endsWith(')')) {
      enumValues = type
        .slice(5, -1)
        .split(',')
        .map(v => v.trim())
        .filter(Boolean);
      if (enumValues.length === 0) throw new Error(`Empty enum: ${def}`);
      zodType = `z.enum([${enumValues.map(v => `'${v}'`).join(', ')}])`;
      tsType = enumValues.map(v => `'${v}'`).join(' | ');
    } else {
      const MAP: Record<string, { zod: string; ts: string }> = {
        string: { zod: 'z.string().min(1)', ts: 'string' },
        number: { zod: 'z.number()', ts: 'number' },
        boolean: { zod: 'z.boolean()', ts: 'boolean' },
        date: { zod: 'z.string().datetime()', ts: 'string' },
        datetime: { zod: 'z.string().datetime()', ts: 'string' },
        'string[]': { zod: 'z.array(z.string())', ts: 'string[]' },
        'number[]': { zod: 'z.array(z.number())', ts: 'number[]' },
      };
      const mapping = MAP[type];
      if (!mapping) throw new Error(`Unknown field type "${type}" in: ${def}`);
      zodType = mapping.zod;
      tsType = mapping.ts;
    }

    fields.push({ name, type, optional, zodType, tsType, enumValues });
  }

  if (fields.length === 0) throw new Error('No fields defined');
  return fields;
}

// ── Helpers ──────────────────────────────────────────────────────────────────

function pascal(s: string): string {
  return s.replace(/(^|-)([a-z])/g, (_, __, c: string) => c.toUpperCase());
}
function camel(s: string): string {
  const p = pascal(s);
  return p.charAt(0).toLowerCase() + p.slice(1);
}

function sampleValue(f: ParsedField): string {
  if (f.enumValues) return `'${f.enumValues[0]}'`;
  if (f.tsType === 'string') return `'Test ${f.name}'`;
  if (f.tsType === 'number') return '42';
  if (f.tsType === 'boolean') return 'true';
  if (f.tsType === 'string[]') return `['a']`;
  if (f.tsType === 'number[]') return '[1]';
  return `'2026-01-01T00:00:00.000Z'`;
}

// ── Template: types.ts ───────────────────────────────────────────────────────

function genTypes(name: string, fields: ParsedField[], partition: string): string {
  const P = pascal(name);
  const createFields = fields
    .map(f => `  ${f.name}: ${f.zodType}${f.optional ? '.optional()' : ''},`)
    .join('\n');
  const updateFields = fields.map(f => `  ${f.name}: ${f.zodType}.optional(),`).join('\n');
  const docFields = fields.map(f => `  ${f.name}${f.optional ? '?' : ''}: ${f.tsType};`).join('\n');

  return `/**
 * ${P} module — types and schemas.
 */

import { z } from 'zod';

// ── Document ─────────────────────────────────────────────────
export interface ${P}Doc {
  id: string;
  productId: string;
  ${partition}: string;
${docFields}
  createdAt: string;
  updatedAt: string;
}

// ── Schemas ──────────────────────────────────────────────────
export const Create${P}Schema = z.object({
${createFields}
});

export const Update${P}Schema = z.object({
${updateFields}
});

export type Create${P}Input = z.infer<typeof Create${P}Schema>;
export type Update${P}Input = z.infer<typeof Update${P}Schema>;
`;
}

// ── Template: repository.ts ──────────────────────────────────────────────────

function genRepo(name: string, fields: ParsedField[], partition: string): string {
  const P = pascal(name);
  const prefix = name.slice(0, 3);

  return `/**
 * ${P} repository — Cosmos DB CRUD.
 */

import { getRegisteredContainer } from '@bytelyst/cosmos';
import crypto from 'node:crypto';
import type { ${P}Doc, Create${P}Input, Update${P}Input } from './types.js';

function getContainer() {
  return getRegisteredContainer('${name}');
}

export async function create${P}(
  productId: string,
  ${partition}: string,
  input: Create${P}Input,
): Promise<${P}Doc> {
  const now = new Date().toISOString();
  const doc: ${P}Doc = {
    id: \`${prefix}_\${crypto.randomUUID()}\`,
    productId,
    ${partition},
    ...input,
    createdAt: now,
    updatedAt: now,
  };
  await getContainer().items.create(doc);
  return doc;
}

export async function get${P}(
  id: string,
  ${partition}: string,
): Promise<${P}Doc | null> {
  try {
    const { resource } = await getContainer().item(id, ${partition}).read<${P}Doc>();
    return resource ?? null;
  } catch {
    return null;
  }
}

export async function list${P}(
  productId: string,
  ${partition}: string,
): Promise<${P}Doc[]> {
  const { resources } = await getContainer()
    .items.query<${P}Doc>({
      query: \`SELECT * FROM c WHERE c.productId = @pid AND c.${partition} = @pk ORDER BY c.createdAt DESC\`,
      parameters: [
        { name: '@pid', value: productId },
        { name: '@pk', value: ${partition} },
      ],
    })
    .fetchAll();
  return resources;
}

export async function update${P}(
  id: string,
  ${partition}: string,
  updates: Update${P}Input,
): Promise<${P}Doc | null> {
  const existing = await get${P}(id, ${partition});
  if (!existing) return null;

  const updated: ${P}Doc = {
    ...existing,
    ...updates,
    updatedAt: new Date().toISOString(),
  };
  await getContainer().item(id, ${partition}).replace(updated);
  return updated;
}

export async function delete${P}(
  id: string,
  ${partition}: string,
): Promise<boolean> {
  try {
    await getContainer().item(id, ${partition}).delete();
    return true;
  } catch {
    return false;
  }
}
`;
}

// ── Template: routes.ts ──────────────────────────────────────────────────────

function genRoutes(name: string, _fields: ParsedField[], _partition: string): string {
  const P = pascal(name);
  const c = camel(name);

  return `/**
 * ${P} routes.
 */

import type { FastifyInstance } from 'fastify';
import { UnauthorizedError, NotFoundError } from '../../lib/errors.js';
import { getRequestProductId } from '../../lib/request-context.js';
import { Create${P}Schema, Update${P}Schema } from './types.js';
import { create${P}, get${P}, list${P}, update${P}, delete${P} } from './repository.js';

function requireAuth(req: { jwtPayload?: { sub: string; role?: string } }): string {
  if (!req.jwtPayload?.sub) throw new UnauthorizedError('Authentication required');
  return req.jwtPayload.sub;
}

export async function ${c}Routes(app: FastifyInstance): Promise<void> {
  // ── List ────────────────────────────────────────────────────
  app.get('/${name}', async (req) => {
    const userId = requireAuth(req);
    const productId = getRequestProductId(req);
    return list${P}(productId, userId);
  });

  // ── Get by ID ──────────────────────────────────────────────
  app.get<{ Params: { id: string } }>('/${name}/:id', async (req) => {
    const userId = requireAuth(req);
    const item = await get${P}(req.params.id, userId);
    if (!item) throw new NotFoundError('${P} not found');
    return item;
  });

  // ── Create ─────────────────────────────────────────────────
  app.post('/${name}', async (req, reply) => {
    const userId = requireAuth(req);
    const productId = getRequestProductId(req);
    const input = Create${P}Schema.parse(req.body);
    const item = await create${P}(productId, userId, input);
    reply.status(201);
    return item;
  });

  // ── Update ─────────────────────────────────────────────────
  app.patch<{ Params: { id: string } }>('/${name}/:id', async (req) => {
    const userId = requireAuth(req);
    const updates = Update${P}Schema.parse(req.body);
    const item = await update${P}(req.params.id, userId, updates);
    if (!item) throw new NotFoundError('${P} not found');
    return item;
  });

  // ── Delete ─────────────────────────────────────────────────
  app.delete<{ Params: { id: string } }>('/${name}/:id', async (req, reply) => {
    const userId = requireAuth(req);
    const ok = await delete${P}(req.params.id, userId);
    if (!ok) throw new NotFoundError('${P} not found');
    reply.status(204);
  });
}
`;
}

// ── Template: test ───────────────────────────────────────────────────────────

function genTest(name: string, fields: ParsedField[]): string {
  const P = pascal(name);

  const requiredFields = fields.filter(f => !f.optional);
  const validPayload = requiredFields.map(f => `    ${f.name}: ${sampleValue(f)},`).join('\n');

  return `/**
 * ${P} module — schema tests.
 */

import { describe, it, expect } from 'vitest';
import { Create${P}Schema, Update${P}Schema } from './types.js';

describe('Create${P}Schema', () => {
  it('accepts valid input', () => {
    const result = Create${P}Schema.parse({
${validPayload}
    });
    expect(result).toBeDefined();
${requiredFields.map(f => `    expect(result.${f.name}).toBe(${sampleValue(f)});`).join('\n')}
  });

  it('rejects empty object', () => {
    expect(() => Create${P}Schema.parse({})).toThrow();
  });

${requiredFields
  .map(
    f => `  it('rejects missing ${f.name}', () => {
    const input = {
${requiredFields
  .filter(r => r.name !== f.name)
  .map(r => `      ${r.name}: ${sampleValue(r)},`)
  .join('\n')}
    };
    expect(() => Create${P}Schema.parse(input)).toThrow();
  });
`
  )
  .join('\n')}});

describe('Update${P}Schema', () => {
  it('accepts empty object (all fields optional)', () => {
    const result = Update${P}Schema.parse({});
    expect(result).toEqual({});
  });

  it('accepts partial update', () => {
    const result = Update${P}Schema.parse({ ${requiredFields[0]?.name ?? 'id'}: ${sampleValue(requiredFields[0] ?? fields[0])} });
    expect(result).toBeDefined();
  });
});
`;
}

// ── Auto-patch: cosmos-init.ts ───────────────────────────────────────────────

async function patchCosmosInit(name: string, partition: string, dryRun: boolean): Promise<boolean> {
  const filePath = path.join(SRC_DIR, 'lib', 'cosmos-init.ts');
  let content: string;
  try {
    content = await fs.readFile(filePath, 'utf-8');
  } catch {
    console.log('   ⚠️  cosmos-init.ts not found — skipping auto-patch');
    return false;
  }

  // Check if container already exists
  if (content.includes(`'${name}'`) || content.includes(`"${name}"`)) {
    console.log(`   ℹ️  Container '${name}' already in cosmos-init.ts`);
    return false;
  }

  // Find the CONTAINER_DEFS object — insert before the closing };
  // Pattern: look for the last container entry before a closing brace
  const insertLine = `  ${name}: { partitionKeyPath: '/${partition}' },`;

  // Find a good insertion point: before the first comment line after container entries,
  // or before the closing `};` of the CONTAINER_DEFS
  const lines = content.split('\n');
  let insertIdx = -1;
  let inDefs = false;

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i].trim();
    // Detect we're inside CONTAINER_DEFS
    if (line.includes('CONTAINER_DEFS') || line.includes('containerDefs')) inDefs = true;
    // Look for a line with }: or }; that ends the object
    if (inDefs && (line === '};' || line === '} as const;' || line === '} satisfies')) {
      insertIdx = i;
      break;
    }
  }

  if (insertIdx === -1) {
    console.log('   ⚠️  Could not find insertion point in cosmos-init.ts — add manually');
    console.log(`       ${insertLine}`);
    return false;
  }

  if (dryRun) {
    console.log(`   📝 Would add to cosmos-init.ts line ${insertIdx + 1}:`);
    console.log(`       ${insertLine}`);
    return true;
  }

  lines.splice(insertIdx, 0, insertLine);
  await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
  console.log(`   ✅ cosmos-init.ts — added container '${name}'`);
  return true;
}

// ── Auto-patch: server.ts ────────────────────────────────────────────────────

async function patchServerTs(name: string, dryRun: boolean): Promise<boolean> {
  const filePath = path.join(SRC_DIR, 'server.ts');
  let content: string;
  try {
    content = await fs.readFile(filePath, 'utf-8');
  } catch {
    console.log('   ⚠️  server.ts not found — skipping auto-patch');
    return false;
  }

  const c = camel(name);
  const routeFn = `${c}Routes`;

  // Check if already registered
  if (content.includes(routeFn)) {
    console.log(`   ℹ️  ${routeFn} already in server.ts`);
    return false;
  }

  const importLine = `import { ${routeFn} } from './modules/${name}/routes.js';`;
  const registerLine = `await app.register(${routeFn}, { prefix: '/api' });`;

  if (dryRun) {
    console.log('   📝 Would add to server.ts:');
    console.log(`       Import:   ${importLine}`);
    console.log(`       Register: ${registerLine}`);
    return true;
  }

  const lines = content.split('\n');

  // Find last import line to insert import after it
  let lastImportIdx = -1;
  for (let i = 0; i < lines.length; i++) {
    if (lines[i].startsWith('import ')) lastImportIdx = i;
  }

  if (lastImportIdx === -1) {
    console.log('   ⚠️  Could not find import section in server.ts — add manually');
    return false;
  }

  // Insert import after last import
  lines.splice(lastImportIdx + 1, 0, importLine);

  // Find last app.register line to insert registration after it
  let lastRegisterIdx = -1;
  for (let i = 0; i < lines.length; i++) {
    if (lines[i].includes('app.register(') && lines[i].includes("prefix: '/api'")) {
      lastRegisterIdx = i;
    }
  }

  if (lastRegisterIdx === -1) {
    // Fall back: insert before the listen/start call
    for (let i = 0; i < lines.length; i++) {
      if (lines[i].includes('app.listen') || lines[i].includes('startService')) {
        lastRegisterIdx = i - 1;
        break;
      }
    }
  }

  if (lastRegisterIdx === -1) {
    console.log('   ⚠️  Could not find registration point in server.ts — add manually');
    console.log(`       ${registerLine}`);
    return false;
  }

  lines.splice(lastRegisterIdx + 1, 0, registerLine);
  await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
  console.log(`   ✅ server.ts — added import + registration for ${routeFn}`);
  return true;
}

// ── Main ─────────────────────────────────────────────────────────────────────

async function main(): Promise<void> {
  const { name, fields, partition, dryRun } = parseArgs();

  console.log(`\n🚀 Generating module: ${name}`);
  console.log(`   Fields:    ${fields}`);
  console.log(`   Partition: /${partition}`);
  if (dryRun) console.log('   Mode:      DRY RUN — no files will be written\n');

  const parsedFields = parseFields(fields);
  console.log(`   Parsed ${parsedFields.length} field(s)\n`);

  const moduleDir = path.join(SRC_DIR, 'modules', name);

  // Check if module already exists
  try {
    await fs.access(moduleDir);
    console.error(`❌ Module directory already exists: ${moduleDir}`);
    console.error('   Delete it first or choose a different name.');
    process.exit(1);
  } catch {
    // Good — directory doesn't exist
  }

  const files: Record<string, string> = {
    'types.ts': genTypes(name, parsedFields, partition),
    'repository.ts': genRepo(name, parsedFields, partition),
    'routes.ts': genRoutes(name, parsedFields, partition),
    [`${name}.test.ts`]: genTest(name, parsedFields),
  };

  if (dryRun) {
    console.log('📄 Generated files (preview):\n');
    for (const [filename, content] of Object.entries(files)) {
      console.log(`── ${filename} ──────────────────────────────────────`);
      console.log(content);
    }
    console.log('── Auto-patches ──────────────────────────────────────');
    await patchCosmosInit(name, partition, true);
    await patchServerTs(name, true);
    console.log('\n✨ Dry run complete. Re-run without --dry-run to write files.');
    return;
  }

  // Write module files
  await fs.mkdir(moduleDir, { recursive: true });
  for (const [filename, content] of Object.entries(files)) {
    await fs.writeFile(path.join(moduleDir, filename), content, 'utf-8');
    console.log(`   ✅ ${filename}`);
  }

  // Auto-patch cosmos-init.ts and server.ts
  console.log('');
  await patchCosmosInit(name, partition, false);
  await patchServerTs(name, false);

  console.log(`\n✨ Module '${name}' created at src/modules/${name}/`);
  console.log(`\n   Verify: pnpm --filter @lysnrai/platform-service test`);
  console.log(`   Typecheck: pnpm --filter @lysnrai/platform-service exec tsc --noEmit`);
}

main().catch(err => {
  console.error('\n❌ Error:', err instanceof Error ? err.message : String(err));
  process.exit(1);
});
