import {
  escapeQuotes,
  parseDefault,
  exportFieldComment,
} from "../exportSQL/shared";
import { DB } from "../../data/constants";
import { databases } from "../../data/databases";
import { dbToTypes } from "../../data/datatypes";

function getQuote(db) {
  if (db === DB.MYSQL || db === DB.MARIADB) return (s) => `\`${s}\``;
  if (db === DB.MSSQL) return (s) => `[${s}]`;
  return (s) => `"${s}"`;
}

function parseType(field, db) {
  let res = field.type;
  const meta = dbToTypes[db]?.[field.type];
  if (field.type === "SET" || field.type === "ENUM") {
    res += field.values
      ? `(${field.values.map((v) => `'${v}'`).join(", ")})`
      : "";
  } else if (meta?.isSized || meta?.hasPrecision) {
    res += field.size ? `(${field.size})` : "";
  }
  return res;
}

function columnDefinition(field, db) {
  const meta =
    dbToTypes[db]?.[field.type] || dbToTypes[DB.GENERIC]?.[field.type];

  let typeStr = field.type;
  if (db === DB.MYSQL || db === DB.MARIADB) {
    typeStr = parseType(field, db);
  } else if (meta?.isSized || meta?.hasPrecision) {
    typeStr += field.size ? `(${field.size})` : "";
  }

  let sql = typeStr;

  if (db === DB.MYSQL || db === DB.MARIADB) {
    if (meta?.signed && field.unsigned) sql += " UNSIGNED";
  }
  if (field.notNull) sql += " NOT NULL";
  if (field.unique) sql += " UNIQUE";
  if (field.primary) sql += " PRIMARY KEY";

  if (field.increment) {
    if (db === DB.POSTGRES) sql += " GENERATED BY DEFAULT AS IDENTITY";
    else if (db === DB.MYSQL || db === DB.MARIADB) sql += " AUTO_INCREMENT";
    else if (db === DB.MSSQL) sql += " IDENTITY";
    else if (db === DB.ORACLESQL) sql += " GENERATED BY DEFAULT AS IDENTITY";
    else if (db === DB.SQLITE) sql += " AUTOINCREMENT";
  }

  if (field.default != null && field.default !== "") {
    sql += ` DEFAULT ${parseDefault(field, db)}`;
  }
  if (field.check && meta?.hasCheck) sql += ` CHECK(${field.check})`;
  if ((db === DB.MYSQL || db === DB.MARIADB) && field.comment?.trim()) {
    sql += ` COMMENT '${escapeQuotes(field.comment)}'`;
  }
  if (db === DB.POSTGRES && field.isArray) sql += " ARRAY";

  return sql;
}

function toTable(table, db) {
  const q = getQuote(db);
  const meta = (f) =>
    dbToTypes[db]?.[f.type] || dbToTypes[DB.GENERIC]?.[f.type];

  let fieldDefs;
  if (db === DB.POSTGRES) {
    fieldDefs = table.fields
      .map(
        (f) =>
          `${exportFieldComment(f.comment)}\t${q(f.name)} ${f.type}${f.size ? `(${f.size})` : ""}${f.isArray ? " ARRAY" : ""}${f.notNull ? " NOT NULL" : ""}${f.unique ? " UNIQUE" : ""}${f.increment ? " GENERATED BY DEFAULT AS IDENTITY" : ""}${f.default?.trim() ? ` DEFAULT ${parseDefault(f, db)}` : ""}${f.check && meta(f)?.hasCheck ? ` CHECK(${f.check})` : ""}`,
      )
      .join(",\n");
  } else if (db === DB.MYSQL || db === DB.MARIADB) {
    fieldDefs = table.fields
      .map(
        (f) =>
          `\t${q(f.name)} ${parseType(f, db)}${db === DB.MYSQL && meta(f)?.signed && f.unsigned ? " UNSIGNED" : ""}${f.notNull ? " NOT NULL" : ""}${f.increment ? " AUTO_INCREMENT" : ""}${f.unique ? " UNIQUE" : ""}${f.default !== "" ? ` DEFAULT ${parseDefault(f, db)}` : ""}${f.check && meta(f)?.hasCheck ? ` CHECK(${f.check})` : ""}${f.comment ? ` COMMENT '${escapeQuotes(f.comment)}'` : ""}`,
      )
      .join(",\n");
  } else if (db === DB.SQLITE) {
    fieldDefs = table.fields
      .map(
        (f) =>
          `\t${q(f.name)} ${f.type}${f.notNull ? " NOT NULL" : ""}${f.unique ? " UNIQUE" : ""}${f.default !== "" ? ` DEFAULT ${parseDefault(f, db)}` : ""}${f.check && meta(f)?.hasCheck ? ` CHECK(${f.check})` : ""}`,
      )
      .join(",\n");
  } else if (db === DB.MSSQL) {
    const m = (f) => dbToTypes[DB.MSSQL]?.[f.type.toUpperCase()];
    fieldDefs = table.fields
      .map((f) => {
        const isSized = m(f)?.isSized || m(f)?.hasPrecision;
        return `\t${q(f.name)} ${f.type}${f.size && isSized ? `(${f.size})` : ""}${f.notNull ? " NOT NULL" : ""}${f.increment ? " IDENTITY" : ""}${f.unique ? " UNIQUE" : ""}${f.default !== "" ? ` DEFAULT ${parseDefault(f, db)}` : ""}${f.check && meta(f)?.hasCheck ? ` CHECK(${f.check})` : ""}`;
      })
      .join(",\n");
  } else if (db === DB.ORACLESQL) {
    fieldDefs = table.fields
      .map(
        (f) =>
          `\t${q(f.name)} ${f.type}${f.size !== undefined && f.size !== "" ? `(${f.size})` : ""}${f.notNull ? " NOT NULL" : ""}${f.increment ? " GENERATED BY DEFAULT AS IDENTITY" : ""}${f.unique ? " UNIQUE" : ""}${f.default !== "" ? ` DEFAULT ${parseDefault(f, db)}` : ""}${f.check && meta(f)?.hasCheck ? ` CHECK(${f.check})` : ""}`,
      )
      .join(",\n");
  }

  const pk = table.fields.filter((f) => f.primary);
  const pkClause =
    pk.length > 0
      ? `,\n\tPRIMARY KEY(${pk.map((f) => q(f.name)).join(", ")})`
      : "";

  let inheritsClause = "";
  if (
    db === DB.POSTGRES &&
    Array.isArray(table.inherits) &&
    table.inherits.length > 0
  ) {
    inheritsClause = `\n) INHERITS (${table.inherits.map((p) => q(p)).join(", ")})`;
  } else {
    inheritsClause = "\n)";
  }

  let create = "";
  if (db === DB.POSTGRES || db === DB.MYSQL || db === DB.SQLITE) {
    create = `CREATE TABLE IF NOT EXISTS ${q(table.name)} (\n${fieldDefs}${pkClause}${inheritsClause};`;
  } else if (db === DB.MARIADB) {
    create = `CREATE OR REPLACE TABLE ${q(table.name)} (\n${fieldDefs}${pkClause}\n)`;
  } else if (db === DB.MSSQL) {
    create = `CREATE TABLE ${q(table.name)} (\n${fieldDefs}${pkClause}\n);\nGO`;
  } else if (db === DB.ORACLESQL) {
    create = `CREATE TABLE ${q(table.name)} (\n${fieldDefs}${pkClause}\n)`;
  }

  if (db === DB.MARIADB || db === DB.ORACLESQL) create += ";";

  if ((db === DB.MYSQL || db === DB.MARIADB) && table.comment?.trim()) {
    create = create.replace(
      ");",
      `) COMMENT='${escapeQuotes(table.comment)}';`,
    );
  }

  let extra = "";
  if (db === DB.POSTGRES || db === DB.ORACLESQL) {
    const t = table.comment?.trim()
      ? `COMMENT ON TABLE ${q(table.name)} IS '${escapeQuotes(table.comment)}';`
      : "";
    const cols = table.fields
      .filter((f) => f.comment?.trim())
      .map(
        (f) =>
          `COMMENT ON COLUMN ${q(table.name)}.${q(f.name)} IS '${escapeQuotes(f.comment)}'`,
      )
      .join(";\n");
    extra = [t, cols].filter(Boolean).join("\n");
  } else if (db === DB.MSSQL && table.comment?.trim()) {
    extra = `
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'${escapeQuotes(table.comment).replace(/'/g, "''")}',
  @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'${table.name}';
GO`;
  }

  const indexStmts = (table.indices || [])
    .map((i) => {
      const ifNotExists = db === DB.SQLITE ? "IF NOT EXISTS " : "";
      const idx = `CREATE ${i.unique ? "UNIQUE " : ""}INDEX ${ifNotExists}${q(i.name)}\nON ${q(table.name)} (${(i.fields || []).map((c) => q(c)).join(", ")});`;
      return db === DB.MSSQL ? idx + "\nGO" : idx;
    })
    .join("\n");

  const prefix =
    db === DB.SQLITE && table.comment?.trim()
      ? `/* ${escapeQuotes(table.comment)} */\n`
      : "";
  return [prefix, create, extra, indexStmts].filter(Boolean).join("\n");
}

function resolveRel(diagram, rel) {
  const tables = diagram?.tables || [];
  const startT = tables.find((t) => t.id === rel.startTableId);
  const endT = tables.find((t) => t.id === rel.endTableId);
  if (!startT || !endT) return null;
  const startF = (startT.fields || []).find((f) => f.id === rel.startFieldId);
  const endF = (endT.fields || []).find((f) => f.id === rel.endFieldId);
  if (!startF || !endF) return null;
  return {
    startTableName: startT.name,
    startFieldName: startF.name,
    endTableName: endT.name,
    endFieldName: endF.name,
  };
}

function normalizeFkAction(s) {
  return (s || "No action").toString().toUpperCase();
}

function toTypeDefinition(type, database, q) {
  if (!type || !databases[database]?.hasTypes) return null;
  const fields = (type.fields || [])
    .map((f) => `\t${q(f.name)} ${f.type}`)
    .join(",\n");
  let s = `CREATE TYPE ${q(type.name)} AS (\n${fields}\n);`;
  if (type.comment?.trim()) {
    s += `\nCOMMENT ON TYPE ${q(type.name)} IS '${escapeQuotes(type.comment)}';`;
  }
  return s;
}

function toEnumDefinition(e, database, q) {
  if (!e || !databases[database]?.hasEnums) return null;
  const values = (e.values || [])
    .map((v) => `\t'${escapeQuotes(String(v))}'`)
    .join(",\n");
  return `CREATE TYPE ${q(e.name)} AS ENUM (\n${values}\n);`;
}

export const generateMigrationSQL = (
  diff,
  database = DB.POSTGRES,
  diagrams,
) => {
  const q = getQuote(database);
  let up = [];
  let down = [];

  for (const [path, change] of Object.entries(diff)) {
    const keys = path.split("#");
    const bracketStart = keys[0].indexOf("[");
    const element =
      bracketStart >= 0 ? keys[0].substring(0, bracketStart) : keys[0];
    const nameStart = keys[0].indexOf("name=");
    const name =
      nameStart >= 0
        ? keys[0].substring(nameStart + 5, keys[0].indexOf("]", nameStart))
        : "";

    switch (element) {
      case "tables": {
        if (keys.length === 1 && change.from && !change.to) {
          up.push(`DROP TABLE ${q(change.from.name)};`);
          down.push(toTable(change.from, database));
        }
        if (keys.length === 1 && change.to && !change.from) {
          up.push(toTable(change.to, database));
          down.push(`DROP TABLE ${q(change.to.name)};`);
        }

        if (keys.length > 1) {
          const childPart = keys[1];

          if (childPart.startsWith("fields")) {
            const columnName = childPart.substring(
              childPart.indexOf("name=") + 5,
              childPart.indexOf(",type="),
            );
            const columnType = childPart.substring(
              childPart.indexOf("type=") + 5,
              childPart.indexOf("]"),
            );

            const property = keys[2];

            if (!property) {
              if (change.from && !change.to) {
                up.push(`ALTER TABLE ${q(name)} DROP COLUMN ${q(columnName)};`);
                const addCol =
                  database === DB.MSSQL
                    ? `ADD ${q(columnName)} ${columnDefinition(change.from, database)}`
                    : database === DB.ORACLESQL
                      ? `ADD (${q(columnName)} ${columnDefinition(change.from, database)})`
                      : `ADD COLUMN ${q(columnName)} ${columnDefinition(change.from, database)}`;
                down.push(`ALTER TABLE ${q(name)} ${addCol};`);
              }
              if (change.to && !change.from) {
                const addCol =
                  database === DB.MSSQL
                    ? `ADD ${q(columnName)} ${columnDefinition(change.to, database)}`
                    : database === DB.ORACLESQL
                      ? `ADD (${q(columnName)} ${columnDefinition(change.to, database)})`
                      : `ADD COLUMN ${q(columnName)} ${columnDefinition(change.to, database)}`;
                up.push(`ALTER TABLE ${q(name)} ${addCol};`);
                down.push(
                  `ALTER TABLE ${q(name)} DROP COLUMN ${q(columnName)};`,
                );
              }
            }

            switch (property) {
              case "type": {
                if (database === DB.SQLITE) {
                  break;
                }
                if (database === DB.MYSQL || database === DB.MARIADB) {
                  up.push(
                    `ALTER TABLE ${q(name)} MODIFY COLUMN ${q(columnName)} ${change.to};`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} MODIFY COLUMN ${q(columnName)} ${change.from};`,
                  );
                } else if (database === DB.ORACLESQL) {
                  up.push(
                    `ALTER TABLE ${q(name)} MODIFY (${q(columnName)} ${change.to});`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} MODIFY (${q(columnName)} ${change.from});`,
                  );
                } else {
                  up.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} TYPE ${change.to};`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} TYPE ${change.from};`,
                  );
                }
                break;
              }

              case "notNull": {
                if (database === DB.SQLITE) {
                  break;
                } else {
                  const nn =
                    database === DB.MYSQL || database === DB.MARIADB
                      ? (v) =>
                          `ALTER TABLE ${q(name)} MODIFY COLUMN ${q(columnName)} ${columnType}${v ? " NOT NULL" : ""};`
                      : (v) =>
                          `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} ${v ? "SET NOT NULL" : "DROP NOT NULL"};`;
                  if (change.to) {
                    up.push(nn(true));
                    down.push(nn(false));
                  } else {
                    up.push(nn(false));
                    down.push(nn(true));
                  }
                }
                break;
              }

              case "default": {
                const defVal = (v) =>
                  parseDefault(
                    { default: v, type: columnType || "VARCHAR" },
                    database,
                  );
                if (change.to != null && change.to !== "") {
                  up.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} SET DEFAULT ${defVal(change.to)};`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} DROP DEFAULT;`,
                  );
                } else {
                  up.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} DROP DEFAULT;`,
                  );
                  if (change.from != null && change.from !== "") {
                    down.push(
                      `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} SET DEFAULT ${defVal(change.from)};`,
                    );
                  }
                }
                break;
              }

              case "check": {
                const cn = `${name}_${columnName}_check`;
                if (change.to && change.to !== "") {
                  up.push(
                    `ALTER TABLE ${q(name)} ADD CONSTRAINT ${q(cn)} CHECK (${change.to});`,
                  );
                  down.push(`ALTER TABLE ${q(name)} DROP CONSTRAINT ${q(cn)};`);
                } else {
                  up.push(`ALTER TABLE ${q(name)} DROP CONSTRAINT ${q(cn)};`);
                  down.push(
                    `ALTER TABLE ${q(name)} ADD CONSTRAINT ${q(cn)} CHECK (${change.from});`,
                  );
                }
                break;
              }

              case "increment": {
                if (change.to === true) {
                  up.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} ADD GENERATED BY DEFAULT AS IDENTITY;`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} DROP IDENTITY;`,
                  );
                } else {
                  up.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} DROP IDENTITY;`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} ADD GENERATED BY DEFAULT AS IDENTITY;`,
                  );
                }
                break;
              }

              case "isArray": {
                if (database !== DB.POSTGRES) break;

                if (change.to === true) {
                  up.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} TYPE ${columnType}[] USING ${q(columnName)}::${columnType}[];`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} TYPE ${columnType} USING ${q(columnName)}[1];`,
                  );
                } else {
                  up.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} TYPE ${columnType} USING ${q(columnName)}[1];`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} TYPE ${columnType}[] USING ${q(columnName)}::${columnType}[];`,
                  );
                }
                break;
              }

              case "comment": {
                if (database === DB.MYSQL || database === DB.MARIADB) {
                  up.push(
                    `ALTER TABLE ${q(name)} MODIFY COLUMN ${q(columnName)} ${columnType} COMMENT '${escapeQuotes(String(change.to ?? ""))}';`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} MODIFY COLUMN ${q(columnName)} ${columnType} COMMENT '${escapeQuotes(String(change.from ?? ""))}';`,
                  );
                } else if (database === DB.MSSQL) {
                  up.push(
                    `EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'${escapeQuotes(String(change.to ?? "")).replace(/'/g, "''")}', @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'${name}', @level2type=N'COLUMN',@level2name=N'${columnName}';`,
                  );
                  down.push(
                    `EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'${escapeQuotes(String(change.from ?? "")).replace(/'/g, "''")}', @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'${name}', @level2type=N'COLUMN',@level2name=N'${columnName}';`,
                  );
                } else if (database === DB.SQLITE) {
                  up.push(
                    `-- ${escapeQuotes(String(change.to ?? ""))};`,
                  );
                  down.push(
                    `-- ${escapeQuotes(String(change.from ?? ""))};`,
                  );
                } else {
                  up.push(
                    `COMMENT ON COLUMN ${q(name)}.${q(columnName)} IS '${escapeQuotes(String(change.to ?? ""))}';`,
                  );
                  down.push(
                    `COMMENT ON COLUMN ${q(name)}.${q(columnName)} IS '${escapeQuotes(String(change.from ?? ""))}';`,
                  );
                }
                break;
              }

              case "name": {
                if (database === DB.MSSQL) {
                  up.push(
                    `EXEC sp_rename '${name}.${change.from}', '${change.to}', 'COLUMN';`,
                  );
                  down.push(
                    `EXEC sp_rename '${name}.${change.to}', '${change.from}', 'COLUMN';`,
                  );
                } else {
                  up.push(
                    `ALTER TABLE ${q(name)} RENAME COLUMN ${q(change.from)} TO ${q(change.to)};`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} RENAME COLUMN ${q(change.to)} TO ${q(change.from)};`,
                  );
                }
                break;
              }

              case "unique": {
                const cu = `${name}_${columnName}_unique`;
                if (change.to) {
                  up.push(
                    `ALTER TABLE ${q(name)} ADD CONSTRAINT ${q(cu)} UNIQUE (${q(columnName)});`,
                  );
                  down.push(`ALTER TABLE ${q(name)} DROP CONSTRAINT ${q(cu)};`);
                } else {
                  up.push(`ALTER TABLE ${q(name)} DROP CONSTRAINT ${q(cu)};`);
                  down.push(
                    `ALTER TABLE ${q(name)} ADD CONSTRAINT ${q(cu)} UNIQUE (${q(columnName)});`,
                  );
                }
                break;
              }

              case "primary": {
                const cp = `${name}_pkey`;
                if (change.to) {
                  up.push(
                    `ALTER TABLE ${q(name)} ADD CONSTRAINT ${q(cp)} PRIMARY KEY (${q(columnName)});`,
                  );
                  down.push(`ALTER TABLE ${q(name)} DROP CONSTRAINT ${q(cp)};`);
                } else {
                  up.push(`ALTER TABLE ${q(name)} DROP CONSTRAINT ${q(cp)};`);
                  down.push(
                    `ALTER TABLE ${q(name)} ADD CONSTRAINT ${q(cp)} PRIMARY KEY (${q(columnName)});`,
                  );
                }
                break;
              }

              case "size": {
                if (database === DB.SQLITE) break;

                if (database === DB.MYSQL || database === DB.MARIADB) {
                  up.push(
                    `ALTER TABLE ${q(name)} MODIFY COLUMN ${q(columnName)} ${columnType}(${change.to});`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} MODIFY COLUMN ${q(columnName)} ${columnType}(${change.from});`,
                  );
                } else if (database === DB.ORACLESQL) {
                  up.push(
                    `ALTER TABLE ${q(name)} MODIFY (${q(columnName)} ${columnType}(${change.to}));`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} MODIFY (${q(columnName)} ${columnType}(${change.from}));`,
                  );
                } else {
                  up.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} TYPE ${columnType}(${change.to});`,
                  );
                  down.push(
                    `ALTER TABLE ${q(name)} ALTER COLUMN ${q(columnName)} TYPE ${columnType}(${change.from});`,
                  );
                }
                break;
              }

              default:
                break;
            }
          }

          if (childPart.startsWith("indices")) {
            const property = keys[2];
            const idxFields = (arr) => (arr || []).map((f) => q(f)).join(", ");

            if (!property) {
              if (change.from && !change.to) {
                const dropIdx =
                  database === DB.MYSQL || database === DB.MARIADB
                    ? `DROP INDEX ${q(change.from.name)} ON ${q(name)};`
                    : `DROP INDEX ${q(change.from.name)};`;
                up.push(dropIdx);
                const ifNotExists =
                  database === DB.SQLITE ? "IF NOT EXISTS " : "";
                down.push(
                  `CREATE ${change.from.unique ? "UNIQUE " : ""}INDEX ${ifNotExists}${q(change.from.name)} ON ${q(name)} (${idxFields(change.from.fields)});`,
                );
              }
              if (change.to && !change.from) {
                const ifNotExists =
                  database === DB.SQLITE ? "IF NOT EXISTS " : "";
                up.push(
                  `CREATE ${change.to.unique ? "UNIQUE " : ""}INDEX ${ifNotExists}${q(change.to.name)} ON ${q(name)} (${idxFields(change.to.fields)});`,
                );
                const dropIdx =
                  database === DB.MYSQL || database === DB.MARIADB
                    ? `DROP INDEX ${q(change.to.name)} ON ${q(name)};`
                    : `DROP INDEX ${q(change.to.name)};`;
                down.push(dropIdx);
              }
            }

            if (property === "name") {
              if (database === DB.POSTGRES || database === DB.ORACLESQL) {
                up.push(
                  `ALTER INDEX ${q(change.from)} RENAME TO ${q(change.to)};`,
                );
                down.push(
                  `ALTER INDEX ${q(change.to)} RENAME TO ${q(change.from)};`,
                );
              } else {
                up.push(
                  `-- Rename index: DROP ${q(change.from)} then CREATE with new name`,
                );
                down.push(
                  `-- Rename index: DROP ${q(change.to)} then CREATE with old name`,
                );
              }
            }

            if (property === "fields") {
              const ns = childPart.indexOf("name=");
              const indexName =
                ns >= 0
                  ? childPart.substring(ns + 5, childPart.indexOf("]", ns))
                  : "";
              if (
                indexName &&
                Array.isArray(change.from) &&
                Array.isArray(change.to)
              ) {
                const dropIdx =
                  database === DB.MYSQL || database === DB.MARIADB
                    ? `DROP INDEX ${q(indexName)} ON ${q(name)};`
                    : `DROP INDEX ${q(indexName)};`;
                const ifNotExists =
                  database === DB.SQLITE ? "IF NOT EXISTS " : "";
                const mkCreate = (arr, unique = false) =>
                  `CREATE ${unique ? "UNIQUE " : ""}INDEX ${ifNotExists}${q(indexName)} ON ${q(name)} (${idxFields(arr)});`;
                up.push(dropIdx);
                up.push(mkCreate(change.to));
                down.push(dropIdx);
                down.push(mkCreate(change.from));
              }
            }
          }

          if (childPart === "inherits" && keys[2] !== undefined) {
            if (database !== DB.POSTGRES) break;

            const parent = change.to || change.from;
            if (parent) {
              if (change.to && !change.from) {
                up.push(`ALTER TABLE ${q(name)} INHERIT ${q(parent)};`);
                down.push(`ALTER TABLE ${q(name)} NO INHERIT ${q(parent)};`);
              } else if (change.from && !change.to) {
                up.push(`ALTER TABLE ${q(name)} NO INHERIT ${q(parent)};`);
                down.push(`ALTER TABLE ${q(name)} INHERIT ${q(parent)};`);
              } else if (change.from && change.to) {
                up.push(`ALTER TABLE ${q(name)} NO INHERIT ${q(change.from)};`);
                up.push(`ALTER TABLE ${q(name)} INHERIT ${q(change.to)};`);
                down.push(`ALTER TABLE ${q(name)} NO INHERIT ${q(change.to)};`);
                down.push(`ALTER TABLE ${q(name)} INHERIT ${q(change.from)};`);
              }
            }
          }

          if (childPart === "comment") {
            if (database === DB.POSTGRES || database === DB.ORACLESQL) {
              up.push(
                `COMMENT ON TABLE ${q(name)} IS '${escapeQuotes(String(change.to ?? ""))}';`,
              );
              down.push(
                `COMMENT ON TABLE ${q(name)} IS '${escapeQuotes(String(change.from ?? ""))}';`,
              );
            } else if (database === DB.MYSQL || database === DB.MARIADB) {
              up.push(
                `ALTER TABLE ${q(name)} COMMENT='${escapeQuotes(String(change.to ?? ""))}';`,
              );
              down.push(
                `ALTER TABLE ${q(name)} COMMENT='${escapeQuotes(String(change.from ?? ""))}';`,
              );
            } else if (database === DB.MSSQL) {
              up.push(
                `EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'${escapeQuotes(String(change.to ?? "")).replace(/'/g, "''")}', @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'${name}';`,
              );
              down.push(
                `EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'${escapeQuotes(String(change.from ?? "")).replace(/'/g, "''")}', @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'${name}';`,
              );
            } else if (database === DB.SQLITE) {
              up.push(
                `-- TABLE COMMENT: ${escapeQuotes(String(change.to ?? ""))}`,
              );
              down.push(
                `-- TABLE COMMENT: ${escapeQuotes(String(change.from ?? ""))}`,
              );
            }
          }

          if (childPart === "name") {
            if (database === DB.MYSQL || database === DB.MARIADB) {
              up.push(`RENAME TABLE ${q(change.from)} TO ${q(change.to)};`);
              down.push(`RENAME TABLE ${q(change.to)} TO ${q(change.from)};`);
            } else if (database === DB.MSSQL) {
              up.push(`EXEC sp_rename '${change.from}', '${change.to}';`);
              down.push(`EXEC sp_rename '${change.to}', '${change.from}';`);
            } else {
              up.push(
                `ALTER TABLE ${q(change.from)} RENAME TO ${q(change.to)};`,
              );
              down.push(
                `ALTER TABLE ${q(change.to)} RENAME TO ${q(change.from)};`,
              );
            }
          }
        }

        break;
      }
      case "relationships": {
        if (database === DB.SQLITE) break;

        const getRels = (d) => d?.relationships || d?.references || [];

        if (keys.length === 1) {
          if (change.to && !change.from) {
            const resolved = resolveRel(diagrams.to, change.to);
            if (!resolved) break;

            const addFk = `ALTER TABLE ${q(resolved.startTableName)} ADD CONSTRAINT ${q(change.to.name)} FOREIGN KEY (${q(resolved.startFieldName)}) REFERENCES ${q(resolved.endTableName)} (${q(resolved.endFieldName)}) ON UPDATE ${normalizeFkAction(change.to.updateConstraint)} ON DELETE ${normalizeFkAction(change.to.deleteConstraint)};`;
            up.push(addFk);
            const dropFk =
              database === DB.MYSQL || database === DB.MARIADB
                ? `ALTER TABLE ${q(resolved.startTableName)} DROP FOREIGN KEY ${q(change.to.name)};`
                : `ALTER TABLE ${q(resolved.startTableName)} DROP CONSTRAINT ${q(change.to.name)};`;
            down.push(dropFk);
          }
          if (change.from && !change.to) {
            const resolved = resolveRel(diagrams.from, change.from);
            if (!resolved) break;

            const dropFk =
              database === DB.MYSQL || database === DB.MARIADB
                ? `ALTER TABLE ${q(resolved.startTableName)} DROP FOREIGN KEY ${q(change.from.name)};`
                : `ALTER TABLE ${q(resolved.startTableName)} DROP CONSTRAINT ${q(change.from.name)};`;
            up.push(dropFk);
            const addFk = `ALTER TABLE ${q(resolved.startTableName)} ADD CONSTRAINT ${q(change.from.name)} FOREIGN KEY (${q(resolved.startFieldName)}) REFERENCES ${q(resolved.endTableName)} (${q(resolved.endFieldName)}) ON UPDATE ${normalizeFkAction(change.from.updateConstraint)} ON DELETE ${normalizeFkAction(change.from.deleteConstraint)};`;
            down.push(addFk);
          }
          break;
        }

        const prop = keys[1];
        if (
          keys.length === 2 &&
          (prop === "updateConstraint" || prop === "deleteConstraint")
        ) {
          const idMatch = keys[0].match(/id=([^,\]]+)/);
          const relId = idMatch ? idMatch[1] : null;
          if (!relId) break;
          const relTo = getRels(diagrams.to).find(
            (r) => String(r.id) === String(relId),
          );
          const relFrom = getRels(diagrams.from).find(
            (r) => String(r.id) === String(relId),
          );
          if (!relTo || !relFrom) {
            break;
          }
          const resolvedTo = resolveRel(diagrams.to, relTo);
          const resolvedFrom = resolveRel(diagrams.from, relFrom);
          if (!resolvedTo || !resolvedFrom) break;

          const dropFk = (resolved, rel) =>
            database === DB.MYSQL || database === DB.MARIADB
              ? `ALTER TABLE ${q(resolved.startTableName)} DROP FOREIGN KEY ${q(rel.name)};`
              : `ALTER TABLE ${q(resolved.startTableName)} DROP CONSTRAINT ${q(rel.name)};`;
          const addFk = (rel, resolved) =>
            `ALTER TABLE ${q(resolved.startTableName)} ADD CONSTRAINT ${q(rel.name)} FOREIGN KEY (${q(resolved.startFieldName)}) REFERENCES ${q(resolved.endTableName)} (${q(resolved.endFieldName)}) ON UPDATE ${normalizeFkAction(rel.updateConstraint)} ON DELETE ${normalizeFkAction(rel.deleteConstraint)};`;
          up.push(dropFk(resolvedTo, relTo));
          up.push(addFk(relTo, resolvedTo));
          down.push(dropFk(resolvedFrom, relFrom));
          down.push(addFk(relFrom, resolvedFrom));
        }
        break;
      }
      case "types": {
        if (!databases[database]?.hasTypes) break;

        if (keys.length === 1) {
          if (change.to && !change.from) {
            up.push(toTypeDefinition(change.to, database, q));
            down.push(`DROP TYPE ${q(change.to.name)};`);
          }
          if (change.from && !change.to) {
            up.push(`DROP TYPE ${q(change.from.name)};`);
            down.push(toTypeDefinition(change.from, database, q));
          }
          break;
        }

        const prop = keys[1];
        if (prop === "name") {
          up.push(`ALTER TYPE ${q(change.from)} RENAME TO ${q(change.to)};`);
          down.push(`ALTER TYPE ${q(change.to)} RENAME TO ${q(change.from)};`);
        }
        if (prop === "comment") {
          up.push(
            `COMMENT ON TYPE ${q(name)} IS '${escapeQuotes(String(change.to ?? ""))}';`,
          );
          down.push(
            `COMMENT ON TYPE ${q(name)} IS '${escapeQuotes(String(change.from ?? ""))}';`,
          );
        }
        if (prop.startsWith("fields")) {
          const nameMatch = prop.match(/name=([^,\]]+)/);
          const typeMatch = prop.match(/type=([^,\]]+)/);

          const fieldName = nameMatch?.[1];
          const fieldType = typeMatch?.[1];

          if (!fieldName) break;

          if (change.to && !change.from) {
            up.push(
              `ALTER TYPE ${q(name)} ADD ATTRIBUTE ${q(fieldName)} ${fieldType};`,
            );
            down.push(`ALTER TYPE ${q(name)} DROP ATTRIBUTE ${q(fieldName)};`);
          }

          if (change.from && !change.to) {
            up.push(`ALTER TYPE ${q(name)} DROP ATTRIBUTE ${q(fieldName)};`);
            down.push(
              `ALTER TYPE ${q(name)} ADD ATTRIBUTE ${q(fieldName)} ${fieldType};`,
            );
          }

          const fieldProp = keys[2];
          if (fieldProp === "name") {
            up.push(
              `ALTER TYPE ${q(name)} RENAME ATTRIBUTE ${q(change.from)} TO ${q(change.to)};`,
            );
            down.push(
              `ALTER TYPE ${q(name)} RENAME ATTRIBUTE ${q(change.to)} TO ${q(change.from)};`,
            );
          }

          if (fieldProp === "type") {
            up.push(
              `ALTER TYPE ${q(name)} ALTER ATTRIBUTE ${q(fieldName)} SET DATA TYPE ${change.to};`,
            );
            down.push(
              `ALTER TYPE ${q(name)} ALTER ATTRIBUTE ${q(fieldName)} SET DATA TYPE ${change.from};`,
            );
          }

          break;
        }

        break;
      }
      case "enums": {
        if (!databases[database]?.hasEnums) break;

        if (keys.length === 1) {
          if (change.to && !change.from) {
            up.push(toEnumDefinition(change.to, database, q));
            down.push(`DROP TYPE ${q(change.to.name)};`);
          }
          if (change.from && !change.to) {
            up.push(`DROP TYPE ${q(change.from.name)};`);
            down.push(toEnumDefinition(change.from, database, q));
          }
          break;
        }

        const prop = keys[1];
        if (prop === "name") {
          up.push(`ALTER TYPE ${q(change.from)} RENAME TO ${q(change.to)};`);
          down.push(`ALTER TYPE ${q(change.to)} RENAME TO ${q(change.from)};`);
        }
        if (prop === "values") {
          const vals = (arr) =>
            (arr || []).map((v) => `'${escapeQuotes(String(v))}'`).join(", ");
          up.push(`DROP TYPE ${q(name)};`);
          up.push(`CREATE TYPE ${q(name)} AS ENUM (${vals(change.to)});`);
          down.push(`DROP TYPE ${q(name)};`);
          down.push(`CREATE TYPE ${q(name)} AS ENUM (${vals(change.from)});`);
        }
        break;
      }
      default:
        break;
    }
  }

  return { up: up.join("\n"), down: down.join("\n") };
};
