using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
{
    /// <summary>
    ///     Hand-authored SQL squashed into <c>V3Initial</c> from the retired per-migration proc files
    ///     (<c>*_StoredProcedures.cs</c>, <c>*_ExtendComputeGenerationDiffWith*.cs</c>) and
    ///     <c>AuthorizationGrants.cs</c>.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         <b>Port audit (v3 WP1).</b> The v1 stored procs were built entirely on the
    ///         <c>ConfigGeneration</c> / <c>GenerationId</c> / <c>ClusterNodeGenerationState</c>
    ///         generation model, which <c>V2HostingAlignment</c> (2026-05-26) dropped outright — so
    ///         those procs have been vestigial since v2 (SQL Server's deferred name resolution let them
    ///         keep existing while referencing phantom tables). No production code calls them; the v2/v3
    ///         deploy + diff path is entirely C# (ControlPlane <c>ConfigComposer</c>,
    ///         <c>AddressSpaceChangeClassifier</c>, <c>DraftValidator</c>). Their only consumer is
    ///         <c>AuthorizationTests</c>, which exercises the DB-role EXECUTE grants and expects
    ///         RAISERROR / permission failures — never a successful data operation.
    ///     </para>
    ///     <para>
    ///         Under greenfield v3 the two-role authorization model (<c>OtOpcUaNode</c> /
    ///         <c>OtOpcUaAdmin</c> + EXECUTE grants) is <b>retained</b> as a real security feature, so the
    ///         eight proc <b>names</b> must exist for the grants + tests to bind. Each proc is rewritten
    ///         here to be schema-valid against the v3 schema (references NO dropped table/column):
    ///     </para>
    ///     <list type="bullet">
    ///         <item><c>sp_GetCurrentGenerationForCluster</c> keeps its <c>ClusterNodeCredential</c> +
    ///         <c>ClusterNode</c> "Unauthorized" auth check (both tables retained), then RAISERRORs a
    ///         v3-retirement message — preserving the exact behavior AuthorizationTests asserts.</item>
    ///         <item><c>sp_PublishGeneration</c> is granted to <c>OtOpcUaAdmin</c> only and RAISERRORs a
    ///         retirement message (never the word "permission"), so the Node-denied vs Admin-allowed
    ///         grant assertions still hold.</item>
    ///         <item><c>sp_ComputeGenerationDiff</c> is rewritten to reference the v3 tables
    ///         (RawFolder / DriverInstance / Device / TagGroup / Tag / Equipment / UnsTagReference /
    ///         NodeAcl) as a schema-valid current-state signature inventory. A genuine
    ///         generation-to-generation diff is architecturally impossible now (no GenerationId column);
    ///         the real diff lives in the C# ControlPlane.</item>
    ///         <item><c>sp_ReleaseExternalIdReservation</c> is ported verbatim — it only touches retained
    ///         tables (<c>ExternalIdReservation</c>, <c>ConfigAuditLog</c>).</item>
    ///     </list>
    /// </remarks>
    public partial class V3Initial
    {
        private static void CreateStoredProcedures(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql(GetCurrentGenerationForCluster);
            migrationBuilder.Sql(GetGenerationContent);
            migrationBuilder.Sql(RegisterNodeGenerationApplied);
            migrationBuilder.Sql(ValidateDraft);
            migrationBuilder.Sql(PublishGeneration);
            migrationBuilder.Sql(RollbackToGeneration);
            migrationBuilder.Sql(ComputeGenerationDiff);
            migrationBuilder.Sql(ReleaseExternalIdReservation);
        }

        private static void DropStoredProcedures(MigrationBuilder migrationBuilder)
        {
            foreach (var name in new[]
                     {
                         "sp_ReleaseExternalIdReservation", "sp_ComputeGenerationDiff", "sp_RollbackToGeneration",
                         "sp_PublishGeneration", "sp_ValidateDraft", "sp_RegisterNodeGenerationApplied",
                         "sp_GetGenerationContent", "sp_GetCurrentGenerationForCluster",
                     })
            {
                migrationBuilder.Sql($"IF OBJECT_ID(N'dbo.{name}', N'P') IS NOT NULL DROP PROCEDURE dbo.{name};");
            }
        }

        private static void CreateAuthorizationGrants(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql(@"
IF DATABASE_PRINCIPAL_ID('OtOpcUaNode') IS NULL
    CREATE ROLE OtOpcUaNode;
IF DATABASE_PRINCIPAL_ID('OtOpcUaAdmin') IS NULL
    CREATE ROLE OtOpcUaAdmin;
");

            migrationBuilder.Sql(@"
GRANT EXECUTE ON OBJECT::dbo.sp_GetCurrentGenerationForCluster TO OtOpcUaNode;
GRANT EXECUTE ON OBJECT::dbo.sp_GetGenerationContent            TO OtOpcUaNode;
GRANT EXECUTE ON OBJECT::dbo.sp_RegisterNodeGenerationApplied   TO OtOpcUaNode;

GRANT EXECUTE ON OBJECT::dbo.sp_GetCurrentGenerationForCluster TO OtOpcUaAdmin;
GRANT EXECUTE ON OBJECT::dbo.sp_GetGenerationContent            TO OtOpcUaAdmin;
GRANT EXECUTE ON OBJECT::dbo.sp_ValidateDraft                   TO OtOpcUaAdmin;
GRANT EXECUTE ON OBJECT::dbo.sp_PublishGeneration               TO OtOpcUaAdmin;
GRANT EXECUTE ON OBJECT::dbo.sp_RollbackToGeneration            TO OtOpcUaAdmin;
GRANT EXECUTE ON OBJECT::dbo.sp_ComputeGenerationDiff           TO OtOpcUaAdmin;
GRANT EXECUTE ON OBJECT::dbo.sp_ReleaseExternalIdReservation    TO OtOpcUaAdmin;

DENY UPDATE, DELETE, INSERT ON SCHEMA::dbo TO OtOpcUaNode;
DENY UPDATE, DELETE, INSERT ON SCHEMA::dbo TO OtOpcUaAdmin;
DENY SELECT ON SCHEMA::dbo TO OtOpcUaNode;
-- Admins may SELECT for reporting views in the future — grant views explicitly, not the schema.
DENY SELECT ON SCHEMA::dbo TO OtOpcUaAdmin;
");
        }

        private static void DropAuthorizationGrants(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql(@"
IF DATABASE_PRINCIPAL_ID('OtOpcUaNode') IS NOT NULL
    DROP ROLE OtOpcUaNode;
IF DATABASE_PRINCIPAL_ID('OtOpcUaAdmin') IS NOT NULL
    DROP ROLE OtOpcUaAdmin;
");
        }

        // ------------------------------------------------------------------------------------------
        // v3 retirement stubs. The generation model is gone (see class remarks); these preserve the
        // authorization surface + the AuthorizationTests behaviors while referencing only v3 tables.
        // ------------------------------------------------------------------------------------------

        private const string GetCurrentGenerationForCluster = @"
CREATE OR ALTER PROCEDURE dbo.sp_GetCurrentGenerationForCluster
    @NodeId    nvarchar(64),
    @ClusterId nvarchar(64)
AS
BEGIN
    SET NOCOUNT ON;

    -- Auth surface retained verbatim (ClusterNodeCredential + ClusterNode are v3 tables). An
    -- unbound caller still gets 'Unauthorized', which AuthorizationTests asserts.
    DECLARE @Caller nvarchar(128) = SUSER_SNAME();

    IF NOT EXISTS (
        SELECT 1 FROM dbo.ClusterNodeCredential
        WHERE NodeId = @NodeId AND Value = @Caller AND Enabled = 1)
    BEGIN
        RAISERROR('Unauthorized: caller %s is not bound to NodeId %s', 16, 1, @Caller, @NodeId);
        RETURN;
    END

    IF NOT EXISTS (
        SELECT 1 FROM dbo.ClusterNode
        WHERE NodeId = @NodeId AND ClusterId = @ClusterId AND Enabled = 1)
    BEGIN
        RAISERROR('Forbidden: NodeId %s does not belong to ClusterId %s', 16, 1, @NodeId, @ClusterId);
        RETURN;
    END

    RAISERROR('sp_GetCurrentGenerationForCluster is retired under v3: config/deploy state moved to the C# ControlPlane (Deployment table).', 16, 1);
END
";

        private const string GetGenerationContent = @"
CREATE OR ALTER PROCEDURE dbo.sp_GetGenerationContent
    @NodeId       nvarchar(64),
    @GenerationId bigint
AS
BEGIN
    SET NOCOUNT ON;
    RAISERROR('sp_GetGenerationContent is retired under v3: generation content is sealed into Deployment.ArtifactBlob by the C# ControlPlane.', 16, 1);
END
";

        private const string RegisterNodeGenerationApplied = @"
CREATE OR ALTER PROCEDURE dbo.sp_RegisterNodeGenerationApplied
    @NodeId       nvarchar(64),
    @GenerationId bigint,
    @Status       nvarchar(16),
    @Error        nvarchar(max) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    RAISERROR('sp_RegisterNodeGenerationApplied is retired under v3: node-apply state is tracked in NodeDeploymentState by the C# ControlPlane.', 16, 1);
END
";

        private const string ValidateDraft = @"
CREATE OR ALTER PROCEDURE dbo.sp_ValidateDraft
    @DraftGenerationId bigint
AS
BEGIN
    SET NOCOUNT ON;
    RAISERROR('sp_ValidateDraft is retired under v3: draft validation runs in the managed DraftValidator (Configuration.Validation).', 16, 1);
END
";

        private const string PublishGeneration = @"
CREATE OR ALTER PROCEDURE dbo.sp_PublishGeneration
    @ClusterId         nvarchar(64),
    @DraftGenerationId bigint,
    @Notes             nvarchar(1024) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    -- Message intentionally omits the word 'permission' so AuthorizationTests can distinguish a
    -- grant failure (Node role) from a body-level error (Admin role reaching the proc body).
    RAISERROR('sp_PublishGeneration is retired under v3: publishing is done by the C# ControlPlane (ConfigComposer + Deployment).', 16, 1);
END
";

        private const string RollbackToGeneration = @"
CREATE OR ALTER PROCEDURE dbo.sp_RollbackToGeneration
    @ClusterId          nvarchar(64),
    @TargetGenerationId bigint,
    @Notes              nvarchar(1024) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    RAISERROR('sp_RollbackToGeneration is retired under v3: rollback is a C# ControlPlane operation over the Deployment history.', 16, 1);
END
";

        // Rewritten to the v3 schema. There is no GenerationId to diff (the per-generation table model
        // was dropped in v2), so this emits a current-state signature inventory across the v3 tables —
        // referencing RawFolder, DriverInstance, Device, TagGroup, Tag, Equipment, UnsTagReference, and
        // NodeAcl. The genuine generation-to-generation diff now lives in the C# ControlPlane
        // (AddressSpaceChangeClassifier); this proc is retained only to carry the OtOpcUaAdmin EXECUTE
        // grant and to keep the v3 table surface referenced from SQL.
        private const string ComputeGenerationDiff = @"
CREATE OR ALTER PROCEDURE dbo.sp_ComputeGenerationDiff
    @FromGenerationId bigint,
    @ToGenerationId   bigint
AS
BEGIN
    SET NOCOUNT ON;

    -- @FromGenerationId / @ToGenerationId are ignored: the v3 config tables are single live-state
    -- (no GenerationId column). Emit a current-state signature per row so the shape (TableName,
    -- LogicalId, Sig) still round-trips through any legacy caller.
    SELECT TableName, LogicalId, Sig FROM (
        SELECT 'RawFolder'      AS TableName, RawFolderId       AS LogicalId, CHECKSUM(ClusterId, ParentRawFolderId, Name, SortOrder) AS Sig FROM dbo.RawFolder
        UNION ALL
        SELECT 'DriverInstance', DriverInstanceId, CHECKSUM(ClusterId, RawFolderId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) FROM dbo.DriverInstance
        UNION ALL
        SELECT 'Device',         DeviceId,         CHECKSUM(DriverInstanceId, Name, Enabled, CONVERT(varchar(max), DeviceConfig)) FROM dbo.Device
        UNION ALL
        SELECT 'TagGroup',       TagGroupId,       CHECKSUM(DeviceId, ParentTagGroupId, Name, SortOrder) FROM dbo.TagGroup
        UNION ALL
        SELECT 'Tag',            TagId,            CHECKSUM(DeviceId, TagGroupId, PollGroupId, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) FROM dbo.Tag
        UNION ALL
        SELECT 'Equipment',      EquipmentId,      CHECKSUM(EquipmentUuid, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) FROM dbo.Equipment
        UNION ALL
        SELECT 'UnsTagReference', UnsTagReferenceId, CHECKSUM(EquipmentId, TagId, DisplayNameOverride, SortOrder) FROM dbo.UnsTagReference
        UNION ALL
        SELECT 'NodeAcl',        NodeAclId,        CHECKSUM(ClusterId, LdapGroup, ScopeKind, ScopeId, PermissionFlags, Notes) FROM dbo.NodeAcl
    ) AS inventory;
END
";

        // Ported verbatim — touches only retained tables (ExternalIdReservation, ConfigAuditLog).
        private const string ReleaseExternalIdReservation = @"
CREATE OR ALTER PROCEDURE dbo.sp_ReleaseExternalIdReservation
    @Kind           nvarchar(16),
    @Value          nvarchar(64),
    @ReleaseReason  nvarchar(512)
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    IF @ReleaseReason IS NULL OR LEN(@ReleaseReason) = 0
    BEGIN
        RAISERROR('ReleaseReason is required', 16, 1);
        RETURN;
    END

    UPDATE dbo.ExternalIdReservation
    SET    ReleasedAt     = SYSUTCDATETIME(),
           ReleasedBy     = SUSER_SNAME(),
           ReleaseReason  = @ReleaseReason
    WHERE  Kind = @Kind AND Value = @Value AND ReleasedAt IS NULL;

    IF @@ROWCOUNT = 0
    BEGIN
        RAISERROR('No active reservation found for (%s, %s)', 16, 1, @Kind, @Value);
        RETURN;
    END

    -- Escape both caller-supplied values via STRING_ESCAPE so quotes/backslashes cannot break the
    -- JSON document or inject additional structure into the audit record.
    INSERT dbo.ConfigAuditLog (Principal, EventType, DetailsJson)
    VALUES (SUSER_SNAME(), 'ExternalIdReleased',
            CONCAT('{""kind"":""', STRING_ESCAPE(@Kind, 'json'),
                   '"",""value"":""', STRING_ESCAPE(@Value, 'json'), '""}'));
END
";
    }
}
