using System.Text;

namespace ZB.MOM.WW.LocalDb.Registration;

/// <summary>
/// Emits the three AFTER triggers (insert/update/delete) that capture every row change on a
/// registered table into <c>__localdb_oplog</c> + <c>__localdb_row_version</c> within the
/// consumer's own write transaction. The <c>WHEN COALESCE(applying, 0) = 0</c> guard skips
/// replicated applies (COALESCE so a missing guard row captures loudly rather than silently
/// disabling CDC); the <c>last_insert_rowid()</c> back-reference binds each row-version row to
/// the SAME <c>zb_hlc_next()</c> stamp as its oplog entry (one stamp per row change). A
/// PK-changing UPDATE additionally tombstones the OLD key first — delete+insert semantics.
/// </summary>
internal static class TriggerSqlGenerator
{
    private const string ApplyingGuard =
        "COALESCE((SELECT applying FROM __localdb_applying WHERE id = 1), 0) = 0";

    private const string TombstoneTail = "1, strftime('%Y-%m-%dT%H:%M:%fZ','now')";

    // Explicit upsert, NOT `INSERT OR REPLACE`: SQLite replaces a trigger-body statement's
    // OR-conflict algorithm with the OUTER statement's conflict handling, so under a consumer
    // `INSERT ... ON CONFLICT DO UPDATE` an OR REPLACE here degraded to a plain INSERT and hit
    // SQLITE_CONSTRAINT_PRIMARYKEY on the existing row_version row. An explicit ON CONFLICT
    // clause is not overridden. (The SELECT's WHERE clause also disambiguates the upsert parse.)
    private const string RowVersionConflictClause =
        "ON CONFLICT(table_name, pk_json) DO UPDATE SET hlc = excluded.hlc, node_id = excluded.node_id, " +
        "is_tombstone = excluded.is_tombstone, tombstone_utc = excluded.tombstone_utc";

    public static string TriggerName(string table, string suffix) => $"__localdb_{table}_{suffix}";

    /// <summary>DROP IF EXISTS + CREATE for all three triggers, ready to run in one transaction.</summary>
    public static string BuildInstallScript(ReplicatedTable table)
    {
        var sb = new StringBuilder();
        foreach (var suffix in new[] { "ai", "au", "ad" })
            sb.Append("DROP TRIGGER IF EXISTS ").Append(Ident(TriggerName(table.Name, suffix))).Append(";\n");

        sb.Append(BuildInsert(table)).Append('\n');
        sb.Append(BuildUpdate(table)).Append('\n');
        sb.Append(BuildDelete(table)).Append('\n');
        return sb.ToString();
    }

    private static string BuildInsert(ReplicatedTable t) =>
        Wrap(t, "ai", "INSERT", NewRowCapture(t));

    private static string BuildUpdate(ReplicatedTable t) =>
        Wrap(t, "au", "UPDATE", OldPkTombstoneIfChanged(t) + NewRowCapture(t));

    private static string BuildDelete(ReplicatedTable t) =>
        Wrap(t, "ad", "DELETE",
            $"""
              INSERT INTO __localdb_oplog (table_name, pk_json, row_json, hlc, node_id, is_tombstone)
              VALUES ({Literal(t.Name)},
                      {JsonObject("OLD", t.PkColumns)},
                      NULL,
                      zb_hlc_next(),
                      (SELECT node_id FROM __localdb_meta WHERE id = 1),
                      1);
              INSERT INTO __localdb_row_version (table_name, pk_json, hlc, node_id, is_tombstone, tombstone_utc)
              SELECT table_name, pk_json, hlc, node_id, {TombstoneTail} FROM __localdb_oplog WHERE seq = last_insert_rowid()
              {RowVersionConflictClause};

            """);

    private static string Wrap(ReplicatedTable t, string suffix, string verb, string body) =>
        $"""
        CREATE TRIGGER {Ident(TriggerName(t.Name, suffix))} AFTER {verb} ON {Ident(t.Name)}
        WHEN {ApplyingGuard}
        BEGIN
        {body.TrimEnd('\n')}
        END;
        """;

    private static string NewRowCapture(ReplicatedTable t) =>
        $"""
          INSERT INTO __localdb_oplog (table_name, pk_json, row_json, hlc, node_id, is_tombstone)
          VALUES ({Literal(t.Name)},
                  {JsonObject("NEW", t.PkColumns)},
                  {JsonObject("NEW", t.Columns.Select(c => c.Name))},
                  zb_hlc_next(),
                  (SELECT node_id FROM __localdb_meta WHERE id = 1),
                  0);
          INSERT INTO __localdb_row_version (table_name, pk_json, hlc, node_id, is_tombstone, tombstone_utc)
          SELECT table_name, pk_json, hlc, node_id, 0, NULL FROM __localdb_oplog WHERE seq = last_insert_rowid()
          {RowVersionConflictClause};

        """;

    // Both statements carry the SAME pk-changed condition: when the tombstone insert did not fire,
    // the row-version statement must also be a no-op, or a stale last_insert_rowid would mis-correlate.
    private static string OldPkTombstoneIfChanged(ReplicatedTable t)
    {
        var changed = string.Join(" OR ", t.PkColumns.Select(p => $"OLD.{Ident(p)} IS NOT NEW.{Ident(p)}"));
        return
            $"""
              INSERT INTO __localdb_oplog (table_name, pk_json, row_json, hlc, node_id, is_tombstone)
              SELECT {Literal(t.Name)},
                     {JsonObject("OLD", t.PkColumns)},
                     NULL,
                     zb_hlc_next(),
                     (SELECT node_id FROM __localdb_meta WHERE id = 1),
                     1
              WHERE {changed};
              INSERT INTO __localdb_row_version (table_name, pk_json, hlc, node_id, is_tombstone, tombstone_utc)
              SELECT table_name, pk_json, hlc, node_id, {TombstoneTail} FROM __localdb_oplog
              WHERE seq = last_insert_rowid() AND ({changed})
              {RowVersionConflictClause};

            """;
    }

    // json_object('col', ALIAS."col", ...) — natively handles NULLs and SQLite scalar types.
    private static string JsonObject(string alias, IEnumerable<string> columns)
    {
        var args = columns.Select(c => $"{Literal(c)}, {alias}.{Ident(c)}");
        return $"json_object({string.Join(", ", args)})";
    }

    private static string Ident(string name) => "\"" + name.Replace("\"", "\"\"") + "\"";

    private static string Literal(string value) => "'" + value.Replace("'", "''") + "'";
}
