using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
{
    /// <summary>
    /// Makes <c>dbo.AuditLog</c>'s EventId uniqueness <b>partition-aligned</b> by
    /// dropping the non-aligned <c>UX_AuditLog_EventId</c> and leaving the clustered
    /// <c>PK_AuditLog (EventId, OccurredAtUtc)</c> — already aligned on
    /// <c>ps_AuditLog_Month(OccurredAtUtc)</c> — as the sole enforcement.
    /// </summary>
    /// <remarks>
    /// <para>
    /// <b>Why.</b> <c>ALTER TABLE … SWITCH PARTITION</c> refuses to run while a
    /// non-aligned index exists on the table, so the monthly retention purge
    /// (<c>AuditLogRepository.SwitchOutPartitionAsync</c>) had to DROP
    /// <c>UX_AuditLog_EventId</c>, switch, and then CREATE it again — an OFFLINE
    /// whole-table unique-index build, inside the switch transaction, blocking every
    /// audit writer for its duration. It also opened a window in which the index that
    /// backs ingest idempotency did not exist at all, and a mid-dance failure could
    /// leave the live table without it until a later tick's CATCH branch repaired it.
    /// With alignment there is nothing to drop, so the switch is metadata-only and the
    /// purge stops competing with ingest.
    /// </para>
    /// <para>
    /// <b>Why dropping it is safe — EventId is globally unique by construction.</b>
    /// The composite key enforces uniqueness of the PAIR, not of EventId alone, so in
    /// principle the same EventId could now be stored twice under two different
    /// <c>OccurredAtUtc</c> values (in two different partitions). That cannot happen
    /// here: <c>EventId</c> is a GUID minted ONCE at the emitting site, in the same
    /// operation that stamps <c>OccurredAtUtc</c>, and both travel together verbatim
    /// through telemetry and reconciliation — nothing downstream re-stamps either
    /// field. A given EventId therefore always arrives with the same OccurredAtUtc and
    /// can only ever map to one partition, which makes pair-uniqueness equivalent to
    /// EventId-uniqueness for every row this system produces. GUID collision across
    /// partitions is not a real risk.
    /// </para>
    /// <para>
    /// <b>The idempotency probe still seeks.</b> Both ingest forms test
    /// <c>WHERE EventId = @id</c>, which is the LEADING column of the clustered PK, so
    /// the probe remains an index seek. The cost changes shape rather than order: it
    /// becomes one seek per partition (the partition column is not in the predicate, so
    /// SQL Server cannot eliminate partitions) instead of a single seek on a
    /// non-partitioned index. Against a monthly scheme that is a couple of dozen
    /// shallow B-tree seeks — cheap, and paid on a path that now issues one statement
    /// per telemetry packet rather than one per row.
    /// </para>
    /// <para>
    /// <b>Edition note.</b> The alternative remedy — keeping the non-aligned index and
    /// rebuilding it with <c>ONLINE = ON</c> outside the switch transaction — requires
    /// Enterprise (or Azure SQL / Developer) edition; online index rebuild is not
    /// available on Standard, which this deployment does not guarantee. Alignment
    /// needs no edition-specific feature and removes the rebuild entirely, so it is
    /// preferred regardless of edition.
    /// </para>
    /// <para>
    /// <b>Down is a faithful reverse</b> and recreates the index on <c>[PRIMARY]</c>
    /// exactly as <c>CollapseAuditLogToCanonical</c> created it. Reverting also
    /// reinstates the SWITCH incompatibility, so the purge's guarded defensive
    /// <c>DROP INDEX</c> (retained in <c>SwitchOutPartitionAsync</c> for databases
    /// restored from pre-alignment backups) would remove it again on the next purge.
    /// The partition function/scheme (<c>pf_AuditLog_Month</c> /
    /// <c>ps_AuditLog_Month</c>) and every aligned index are untouched by both
    /// directions.
    /// </para>
    /// </remarks>
    public partial class AlignAuditLogEventIdUniqueness : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            // Raw, existence-guarded SQL rather than the scaffolded DropIndex: the
            // AuditLog table is raw-SQL managed (partition scheme, persisted computed
            // columns, append-only role grants), so its migrations stay explicit and
            // re-runnable. The guard also lets this apply cleanly to a database whose
            // index was already removed by the purge path's defensive cleanup.
            migrationBuilder.Sql(@"
IF EXISTS (SELECT 1 FROM sys.indexes
           WHERE name = 'UX_AuditLog_EventId' AND object_id = OBJECT_ID('dbo.AuditLog'))
    DROP INDEX UX_AuditLog_EventId ON dbo.AuditLog;");
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql(@"
IF NOT EXISTS (SELECT 1 FROM sys.indexes
               WHERE name = 'UX_AuditLog_EventId' AND object_id = OBJECT_ID('dbo.AuditLog'))
    CREATE UNIQUE NONCLUSTERED INDEX UX_AuditLog_EventId ON dbo.AuditLog (EventId) ON [PRIMARY];");
        }
    }
}
