using Microsoft.Data.Sqlite;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tracking;

/// <summary>
/// DDL for the <c>OperationTracking</c> table, extracted from
/// <see cref="OperationTrackingStore"/> so it can be applied by whoever owns the
/// database file.
/// <para>
/// Under LocalDb Phase 1 that owner is the Host's <c>AddZbLocalDb</c> onReady callback:
/// the table must exist before <c>ILocalDb.RegisterReplicated</c> installs its capture
/// triggers, and rows written before registration are never captured or snapshotted.
/// The store still calls this during its own initialization, so a store constructed
/// directly (tests, tooling) remains self-sufficient.
/// </para>
/// <para>
/// Deliberately depends only on <c>Microsoft.Data.Sqlite</c>, not on the LocalDb
/// library — the Host needs to apply this DDL to a LocalDb-managed connection, but
/// nothing about the schema itself is LocalDb-specific.
/// </para>
/// </summary>
public static class OperationTrackingSchema
{
    /// <summary>
    /// Creates the <c>OperationTracking</c> table and its indexes when absent, and
    /// additively upgrades a table created by an older build. Idempotent — safe to run
    /// on every startup.
    /// </summary>
    /// <param name="connection">An open connection to the database that should hold the table.</param>
    public static void Apply(SqliteConnection connection)
    {
        ArgumentNullException.ThrowIfNull(connection);

        using (var cmd = connection.CreateCommand())
        {
            // TrackedOperationId is a TEXT GUID, which is also what lets this table
            // replicate as-is: RegisterReplicated requires an explicit primary key and
            // rejects BLOB columns, and there are none here.
            cmd.CommandText = """
                CREATE TABLE IF NOT EXISTS OperationTracking (
                    TrackedOperationId TEXT    NOT NULL PRIMARY KEY,
                    Kind               TEXT    NOT NULL,
                    TargetSummary      TEXT    NULL,
                    Status             TEXT    NOT NULL,
                    RetryCount         INTEGER NOT NULL DEFAULT 0,
                    LastError          TEXT    NULL,
                    HttpStatus         INTEGER NULL,
                    CreatedAtUtc       TEXT    NOT NULL,
                    UpdatedAtUtc       TEXT    NOT NULL,
                    TerminalAtUtc      TEXT    NULL,
                    SourceInstanceId   TEXT    NULL,
                    SourceScript       TEXT    NULL,
                    SourceNode         TEXT    NULL
                );
                CREATE INDEX IF NOT EXISTS IX_OperationTracking_Status_Updated
                    ON OperationTracking (Status, UpdatedAtUtc);
                CREATE INDEX IF NOT EXISTS IX_OperationTracking_UpdatedAt
                    ON OperationTracking (UpdatedAtUtc);
                """;
            cmd.ExecuteNonQuery();
        }

        // SourceNode stamping: additively add the SourceNode column.
        // CREATE TABLE IF NOT EXISTS above does NOT add columns to an
        // OperationTracking table that already exists from a pre-SourceNode
        // build, so a tracking.db created by an older build needs the column
        // ALTER-ed in. The file is durable across restart/failover by design
        // (retention window default 7 days), so without this step every
        // RecordEnqueueAsync on an upgraded deployment would bind $sourceNode
        // against a missing column and the write would fail.
        // SQLite has no "ADD COLUMN IF NOT EXISTS"; the column presence is
        // probed first and the ALTER skipped when already there. The column is
        // nullable with no default, so any row written before this migration
        // reads back SourceNode = null (back-compat).
        AddColumnIfMissing(connection, "SourceNode", "TEXT NULL");
    }

    /// <summary>
    /// Additively adds a column to <c>OperationTracking</c> only when it is not
    /// already present. SQLite lacks <c>ADD COLUMN IF NOT EXISTS</c>, so the
    /// schema is probed via <c>PRAGMA table_info</c> first. Mirrors the
    /// <c>SqliteAuditWriter.AddColumnIfMissing</c> precedent.
    /// </summary>
    private static void AddColumnIfMissing(
        SqliteConnection connection, string columnName, string columnDefinition)
    {
        using (var probe = connection.CreateCommand())
        {
            probe.CommandText =
                "SELECT COUNT(*) FROM pragma_table_info('OperationTracking') WHERE name = $name";
            probe.Parameters.AddWithValue("$name", columnName);
            if (Convert.ToInt32(probe.ExecuteScalar()) > 0)
            {
                return;
            }
        }

        using var alter = connection.CreateCommand();
        // Column name + definition are caller-controlled constants, never user
        // input — safe to interpolate (parameters are not permitted in DDL).
        alter.CommandText =
            $"ALTER TABLE OperationTracking ADD COLUMN {columnName} {columnDefinition}";
        alter.ExecuteNonQuery();
    }
}
