namespace ZB.MOM.WW.Secrets.Replicator.SqlServer;

/// <summary>
/// Schema constants and DDL for the shared SQL-Server secret store. Mirrors
/// <c>ZB.MOM.WW.Secrets.Sqlite.SqliteSecretsSchema</c> column-for-column so a row means exactly the
/// same thing in either store.
/// </summary>
/// <remarks>
/// <para>
/// <b>Timestamps are ISO-8601 <c>nvarchar</c>, not <c>datetimeoffset</c>.</b> This looks wrong for a
/// SQL-Server schema and is deliberate: the same <see cref="ZB.MOM.WW.Secrets.Abstractions.StoredSecret"/>
/// rows replicate between this store and SQLite (which stores <c>"O"</c>-format text), and
/// last-writer-wins compares those timestamps for equality. <c>datetimeoffset</c> rounds to 100ns
/// while .NET <c>DateTimeOffset</c> ticks are also 100ns but the round-trip through the driver is
/// not guaranteed bit-identical across providers — a sub-tick difference would turn a tie into a
/// spurious "newer", and two nodes would then overwrite each other forever. Storing the exact
/// round-trippable text both providers already agree on removes the failure mode entirely.
/// </para>
/// <para>
/// The cost is that <c>ORDER BY updated_utc</c> is a lexicographic sort. That is still chronological
/// for <c>"O"</c>-format UTC strings (fixed-width, zero-padded, always <c>+00:00</c>), which is what
/// the store writes.
/// </para>
/// </remarks>
public static class SqlServerSecretsSchema
{
    /// <summary>
    /// The schema version this build creates and supports. The migrator refuses a database whose
    /// recorded version is <em>newer</em> than this.
    /// </summary>
    public const int CurrentVersion = 1;

    /// <summary>Default schema the secret tables live in.</summary>
    public const string DefaultSchemaName = "zbsecrets";

    /// <summary>Name of the single-row table tracking the applied schema version.</summary>
    public const string SchemaVersionTable = "schema_version";

    /// <summary>Name of the table storing envelope-encrypted secret records.</summary>
    public const string SecretTable = "secret";

    /// <summary>
    /// Returns the idempotent DDL that provisions the schema, both tables, and the supporting index
    /// in <paramref name="schemaName"/>.
    /// </summary>
    /// <param name="schemaName">The already-validated SQL schema name to create the objects in.</param>
    /// <returns>The DDL batch.</returns>
    /// <remarks>
    /// <paramref name="schemaName"/> is interpolated, not parameterized — T-SQL has no parameter form
    /// for an object name. It is re-validated here against the same strict identifier allow-list
    /// <see cref="SqlServerSecretsOptions.SchemaName"/> enforces, and bracket-quoted below. The
    /// duplicate check is deliberate: this method is <c>public</c>, so it must defend itself rather
    /// than trust that every caller routed through the options type.
    /// </remarks>
    /// <exception cref="ArgumentException"><paramref name="schemaName"/> is not a plain identifier.</exception>
    public static string CreateSchemaDdl(string schemaName)
    {
        SqlServerSecretsOptions.ValidateIdentifier(schemaName);

        return $"""
            IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = N'{schemaName}')
                EXEC(N'CREATE SCHEMA [{schemaName}]');

            IF OBJECT_ID(N'[{schemaName}].[{SchemaVersionTable}]', N'U') IS NULL
                CREATE TABLE [{schemaName}].[{SchemaVersionTable}] (
                    id          INT NOT NULL PRIMARY KEY CHECK (id = 1),
                    version     INT NOT NULL,
                    applied_utc NVARCHAR(33) NOT NULL
                );

            IF OBJECT_ID(N'[{schemaName}].[{SecretTable}]', N'U') IS NULL
                CREATE TABLE [{schemaName}].[{SecretTable}] (
                    name          NVARCHAR(450)   NOT NULL PRIMARY KEY,
                    description   NVARCHAR(MAX)   NULL,
                    content_type  NVARCHAR(64)    NOT NULL,
                    ciphertext    VARBINARY(MAX)  NOT NULL,
                    nonce         VARBINARY(MAX)  NOT NULL,
                    tag           VARBINARY(MAX)  NOT NULL,
                    wrapped_dek   VARBINARY(MAX)  NOT NULL,
                    wrap_nonce    VARBINARY(MAX)  NOT NULL,
                    wrap_tag      VARBINARY(MAX)  NOT NULL,
                    kek_id        NVARCHAR(256)   NOT NULL,
                    revision      BIGINT          NOT NULL CONSTRAINT DF_secret_revision DEFAULT 0,
                    is_deleted    BIT             NOT NULL CONSTRAINT DF_secret_is_deleted DEFAULT 0,
                    deleted_utc   NVARCHAR(33)    NULL,
                    created_utc   NVARCHAR(33)    NOT NULL,
                    updated_utc   NVARCHAR(33)    NOT NULL,
                    created_by    NVARCHAR(256)   NULL,
                    updated_by    NVARCHAR(256)   NULL
                );

            IF NOT EXISTS (
                SELECT 1 FROM sys.indexes
                WHERE name = N'ix_secret_updated_utc'
                  AND object_id = OBJECT_ID(N'[{schemaName}].[{SecretTable}]'))
                CREATE INDEX ix_secret_updated_utc
                    ON [{schemaName}].[{SecretTable}] (updated_utc);
            """;
    }
}
