'use strict';

const { generateConstraintName, deriveCompositeShortName, shortenIdentifier } = require('../naming');
const {
  IS_DELETED_COLUMN,
  DELETED_AT_COLUMN,
  DELETED_BY_COLUMN,
  isSoftDeleteEnabled,
  assertSoftDeletePostgresOnly,
  softDeleteCheckName
} = require('../soft-delete-constants');

const COLUMN_INDENT = '    ';

const OP_TO_SQL = {
  in: 'IN',
  gt: '>',
  gte: '>=',
  lt: '<',
  lte: '<=',
  eq: '=',
  neq: '!='
};

function emitCreateTable(ir, dialect) {
  const { tableName, fields, primaryKey, uniques, relations, checks } = ir;

  // Guard Fase 1: soft-delete hanya didukung PostgreSQL. Non-PG + softDelete → ERROR
  // (jangan emit bentuk boolean VARCHAR salah diam-diam).
  assertSoftDeletePostgresOnly(ir, dialect);

  const lines = [];

  for (const [fieldName, field] of Object.entries(fields)) {
    lines.push(emitColumn(fieldName, field, dialect));
  }

  if (Array.isArray(primaryKey) && primaryKey.length > 0) {
    lines.push(emitPrimaryKey(tableName, primaryKey, dialect));
  }

  for (const [fieldName, field] of Object.entries(fields)) {
    if (field.unique === true) {
      lines.push(emitUniqueSingle(tableName, fieldName, dialect));
    }
  }

  for (const uq of uniques) {
    lines.push(emitUniqueComposite(tableName, uq.columns, dialect));
  }

  for (const [relName, rel] of Object.entries(relations)) {
    if (rel.type !== 'belongsTo') continue;
    // `_auto` suffix marker dari IR builder = name collision; single-model validator should error before reach emitter
    if (relName.endsWith('_auto')) continue;
    lines.push(emitForeignKey(tableName, relName, rel, dialect));
  }

  // Gabungkan CHECK eksplisit (dari SDF) dengan CHECK boolean yang di-derive
  // otomatis untuk dialect non-native-boolean (mysql/oracle/sqlite). CHECK ini
  // menjadi penanda deterministik agar introspeksi dapat mengenali kembali kolom
  // VARCHAR sebagai boolean (lihat database-introspector.getBooleanColumns).
  const allChecks = (Array.isArray(checks) ? checks : []).concat(deriveBooleanChecks(fields, dialect));
  const checkNames = resolveCheckNames(tableName, allChecks, dialect.maxIdentifierLength);
  for (let i = 0; i < allChecks.length; i++) {
    const check = allChecks[i];
    if (check._invalid) continue;
    lines.push(emitCheck(check, checkNames[i], dialect));
  }

  // CHECK konsistensi soft-delete (R8). Artifact IMPLISIT: tidak pernah ditulis di SDF,
  // selalu diturunkan dari `softDelete.enabled` (pola sama dengan deriveBooleanChecks).
  // Compound multi-field, jadi TIDAK lewat jalur emitCheck generik (single-field).
  if (isSoftDeleteEnabled(ir)) {
    lines.push(emitSoftDeleteCheck(tableName, dialect));
  }

  return `CREATE TABLE ${dialect.formatTableIdentifier(ir)} (\n${lines.join(',\n')}\n)`;
}

/**
 * Emit CHECK konsistensi tiga kolom soft-delete (R8). Bentuk PostgreSQL (Fase 1,
 * boolean native):
 *
 *   CONSTRAINT <name> CHECK (
 *       (is_deleted = TRUE  AND deleted_at IS NOT NULL AND deleted_by IS NOT NULL)
 *    OR (is_deleted = FALSE AND deleted_at IS NULL     AND deleted_by IS NULL)
 *   )
 *
 * Nama deterministik via softDeleteCheckName (sumber tunggal di soft-delete-constants,
 * bukan auto-name DB) agar Phase 03 (diff/introspect) mencocokkan dengan derivasi nama
 * yang sama. Identifier kolom lewat dialect.quoteIdentifier (PostgreSQL: tanpa quote
 * untuk identifier snake_case).
 *
 * @param {string} tableName - nama tabel tak-qualified (untuk nama constraint)
 * @param {Object} dialect - dialect aktif (dijamin PostgreSQL oleh guard di pemanggil)
 * @returns {string} satu baris constraint ber-indent siap join ke body CREATE TABLE
 */
function emitSoftDeleteCheck(tableName, dialect) {
  const name = softDeleteCheckName(tableName, dialect.maxIdentifierLength);
  const isDeleted = dialect.quoteIdentifier(IS_DELETED_COLUMN);
  const deletedAt = dialect.quoteIdentifier(DELETED_AT_COLUMN);
  const deletedBy = dialect.quoteIdentifier(DELETED_BY_COLUMN);
  const expr =
    `(${isDeleted} = TRUE AND ${deletedAt} IS NOT NULL AND ${deletedBy} IS NOT NULL)` +
    ` OR (${isDeleted} = FALSE AND ${deletedAt} IS NULL AND ${deletedBy} IS NULL)`;
  return `${COLUMN_INDENT}CONSTRAINT ${dialect.quoteIdentifier(name)} CHECK (${expr})`;
}

/**
 * Derive CHECK constraint penanda boolean untuk dialect yang menyimpan boolean
 * sebagai VARCHAR (mysql/oracle/sqlite). Bentuk: `col IN ('true','false')`.
 *
 * PostgreSQL (nativeBoolean === true) memakai tipe BOOLEAN native sehingga tidak
 * memerlukan penanda dan dikecualikan. CHECK ini TIDAK ditulis user di SDF;
 * kata kunci `boolean` di field sudah cukup, generator yang menyisipkannya.
 *
 * @param {Object} fields - Map fieldName -> field IR (punya .type)
 * @param {Object} dialect - Dialect aktif
 * @returns {Array<{field:string, op:string, value:string[]}>}
 */
function deriveBooleanChecks(fields, dialect) {
  if (dialect.nativeBoolean === true) return [];
  const derived = [];
  for (const [fieldName, field] of Object.entries(fields)) {
    if (field && field.type === 'boolean') {
      derived.push({ field: fieldName, op: 'in', value: ['true', 'false'] });
    }
  }
  return derived;
}

function emitColumn(fieldName, field, dialect) {
  const parts = [`${COLUMN_INDENT}${dialect.quoteIdentifier(fieldName)}`, dialect.mapType(field)];

  if (field.notnull === true) {
    parts.push('NOT NULL');
  }

  // autoUpdate is a runtime marker, not a DDL feature, so DEFAULT must not be emitted
  if (field.default !== undefined && field.autoUpdate !== true) {
    parts.push(`DEFAULT ${dialect.formatDefault(field.default)}`);
  }

  return parts.join(' ');
}

function emitPrimaryKey(tableName, primaryKey, dialect) {
  const name = generateConstraintName('pk', tableName, '', dialect.maxIdentifierLength);
  const cols = primaryKey.map((c) => dialect.quoteIdentifier(c)).join(', ');
  return `${COLUMN_INDENT}CONSTRAINT ${dialect.quoteIdentifier(name)} PRIMARY KEY (${cols})`;
}

function emitUniqueSingle(tableName, fieldName, dialect) {
  const name = generateConstraintName('uq', tableName, fieldName, dialect.maxIdentifierLength);
  return `${COLUMN_INDENT}CONSTRAINT ${dialect.quoteIdentifier(name)} UNIQUE (${dialect.quoteIdentifier(fieldName)})`;
}

function emitUniqueComposite(tableName, columns, dialect) {
  const shortName = deriveCompositeShortName(columns, dialect.maxIdentifierLength);
  const name = generateConstraintName('uq', tableName, shortName, dialect.maxIdentifierLength);
  const cols = columns.map((c) => dialect.quoteIdentifier(c)).join(', ');
  return `${COLUMN_INDENT}CONSTRAINT ${dialect.quoteIdentifier(name)} UNIQUE (${cols})`;
}

function emitForeignKey(tableName, relName, rel, dialect) {
  const name = generateConstraintName('fk', tableName, relName, dialect.maxIdentifierLength);
  const local = dialect.quoteIdentifier(rel.localKey);
  const target = dialect.formatTableIdentifier(getTargetIr(rel));
  const ref = dialect.quoteIdentifier(rel.references);
  let line = `${COLUMN_INDENT}CONSTRAINT ${dialect.quoteIdentifier(name)} FOREIGN KEY (${local}) REFERENCES ${target} (${ref})`;
  if (rel.onDelete) line += ` ON DELETE ${dialect.formatReferentialAction(rel.onDelete)}`;
  if (rel.onUpdate) line += ` ON UPDATE ${dialect.formatReferentialAction(rel.onUpdate)}`;
  return line;
}

// Cross-model-validator augments rel with `_targetSchemaName` + `_targetTableName`.
// Single-model emit (no validator run) falls back to splitting `rel.target` on dot
// so unit tests can exercise the FK emitter without wiring a full Map.
function getTargetIr(rel) {
  if (rel._targetTableName) {
    return { schemaName: rel._targetSchemaName || null, tableName: rel._targetTableName };
  }
  const target = rel.target;
  if (typeof target === 'string' && target.includes('.')) {
    const idx = target.indexOf('.');
    return { schemaName: target.slice(0, idx), tableName: target.slice(idx + 1) };
  }
  return { schemaName: null, tableName: target };
}

function resolveCheckNames(tableName, checks, maxLength) {
  const opCountByField = {};
  for (const c of checks) {
    if (c._invalid) continue;
    if (!opCountByField[c.field]) opCountByField[c.field] = new Set();
    opCountByField[c.field].add(c.op);
  }

  const names = [];
  for (const c of checks) {
    if (c._invalid) {
      names.push(null);
      continue;
    }
    if (c.name) {
      names.push(shortenIdentifier(c.name, maxLength));
      continue;
    }
    const needsOpSuffix = opCountByField[c.field] && opCountByField[c.field].size > 1;
    const suffix = needsOpSuffix ? `${c.field}_${c.op}` : c.field;
    names.push(generateConstraintName('chk', tableName, suffix, maxLength));
  }
  return names;
}

function emitCheck(check, name, dialect) {
  const sqlOp = OP_TO_SQL[check.op];
  if (!sqlOp) {
    throw new Error(`Unknown check operator: '${check.op}'`);
  }
  const col = dialect.quoteIdentifier(check.field);
  const value = formatCheckValue(check.value, dialect);
  return `${COLUMN_INDENT}CONSTRAINT ${dialect.quoteIdentifier(name)} CHECK (${col} ${sqlOp} ${value})`;
}

function formatCheckValue(value, dialect) {
  if (typeof value === 'boolean') {
    return dialect.formatDefault({ kind: 'literal', value });
  }
  if (typeof value === 'number') {
    return String(value);
  }
  if (typeof value === 'string') {
    return `'${value.replace(/'/g, "''")}'`;
  }
  if (Array.isArray(value)) {
    const parts = value.map((v) => {
      if (typeof v === 'number') return String(v);
      if (typeof v === 'string') return `'${v.replace(/'/g, "''")}'`;
      throw new Error(`Unsupported check array element type: '${typeof v}'`);
    });
    return `(${parts.join(', ')})`;
  }
  throw new Error(`Unsupported check value type: '${typeof value}'`);
}

module.exports = { emitCreateTable };
