import type { Schema } from 'genson-js/dist/types';

import type { Event, Method } from '@/pages/api/generateCode';

export interface GeneratedCode {
  jsCode: string;
  sqlCode: string;
}

interface Column {
  name: string;
  sql: string;
}

function sanitizeTableName(tableName: string): string {
  // Convert to PascalCase
  let pascalCaseTableName = tableName
    // Replace special characters with underscores
    .replace(/[^a-zA-Z0-9_]/g, '_')
    // Makes first letter and any letters following an underscore upper case
    .replace(/^([a-zA-Z])|_([a-zA-Z])/g, (match: string) => match.toUpperCase())
    // Removes all underscores
    .replace(/_/g, '');

  // Add underscore if first character is a number
  if (/^[0-9]/.test(pascalCaseTableName)) {
    pascalCaseTableName = '_' + pascalCaseTableName;
  }

  return pascalCaseTableName;
}

const createColumn = (columnName: string, schema: Schema): Column => {
  let type: string;
  switch (schema.type) {
    case 'string':
      type = 'TEXT';
      break;
    case 'integer':
      type = 'INT';
      break;
    case 'number':
      type = 'FLOAT';
      break;
    case 'boolean':
      type = 'BOOLEAN';
      break;
    case 'array':
      type = 'TEXT[]';
      break;
    case 'object':
      type = 'JSONB';
      break;
    default:
      type = 'TEXT';
  }
  return { name: columnName, sql: `"${columnName}" ${type}` };
};

export class WizardCodeGenerator {
  constructor(private contractFilter: string, private selectedMethods: Method[], private selectedEvents?: Event[]) {}

  private getColumns(method: Method): Column[] {
    if (!method.schema.properties) {
      return [];
    }
    return Object.entries(method.schema.properties).map(([k, v]) => createColumn(k, v));
  }

  private getTableName(method: Method): { contextDbName: string; tableName: string } {
    const tableName = `calls_to_${method.method_name}`;
    return { tableName, contextDbName: sanitizeTableName(tableName) };
  }

  private generateSQLForMethod(method: Method): string {
    if (!method.schema.properties) {
      return '';
    }
    const { tableName } = this.getTableName(method);
    const columns = this.getColumns(method);

    // TODO: add NULLABLE for optional fields
    return `
CREATE TABLE ${tableName}
(
  "block_height"    INT,
  "block_timestamp" TIMESTAMP,
  "signer_id"       TEXT,
  "receipt_id"      TEXT,
${columns.map((c) => `  ${c.sql},`).join('\n')}
  PRIMARY KEY ("receipt_id")
);
-- Consider adding an index (https://www.postgresql.org/docs/14/sql-createindex.html) on a frequently queried column, e.g.:
${columns.map((c) => `-- CREATE INDEX "${tableName}_${c.name}_key" ON "${tableName}" ("${c.name}" ASC);`).join('\n')}
    `;
  }

  private generateJSForMethod(method: Method): string {
    const columnNames = this.getColumns(method).map((c) => c.name);
    const primaryKeys = ['receipt_id'];
    const { contextDbName } = this.getTableName(method);
    const methodName = method.method_name;
    return `
  // Extract and upsert ${methodName} function calls
  const callsTo${methodName} = extractFunctionCallEntity("${this.contractFilter}", "${methodName}", ${JSON.stringify(
      columnNames,
    )});
  try {
    await context.db.${contextDbName}.upsert(callsTo${methodName}, ${JSON.stringify(primaryKeys)}, ${JSON.stringify(
      columnNames,
    )});
  } catch(e) {
    context.error(\`Unable to upsert ${methodName} function calls: \$\{e.message\}\`);
  }
`;
  }

  private generateJSCode(): string {
    return `
  function extractFunctionCallEntity(contractFilter, methodName, argsToInclude) {
    const jsonify = (v) => {
      if ((typeof v === "object" && v !== null) || Array.isArray(v))
        return JSON.stringify(v);
      return v;
    };
    return block
      .functionCallsToReceiver(contractFilter, methodName)
      .map((fc) => {
        let fcArgs = {};
        try {
          fcArgs = fc.argsAsJSON();
        } catch (e) {
          console.log(
            \`Failed to parse args \$\{fc.args\} into JSON for \$\{fc.methodName\}\`
          );
        }

        const extractedArgs = Object.fromEntries(
          Object.entries(fcArgs)
            .filter(([k]) => argsToInclude.includes(k))
            .map(([k, v]) => [k, jsonify(v)])
        );
        return {
          block_height: block.blockHeight,
          block_timestamp: block.timestamp,
          signer_id: fc.signerId,
          receipt_id: fc.receiptId,
          ...extractedArgs,
        };
      });
  }
  ${this.selectedMethods.map((m) => this.generateJSForMethod(m)).join('\n')}
`;
  }

  public generateCode(): GeneratedCode {
    const jsCode = this.generateJSCode();
    const sqlCode = this.selectedMethods.map((m) => this.generateSQLForMethod(m)).join('\n');
    return { jsCode, sqlCode };
  }
}