-- V021__create_chargeable_char_config.sql
-- PRC-001: ChargeableCharConfig — configuración de caracteres especiales tasables con vigencia civil.
--
-- Cambios:
--   1. dbo.ChargeableCharConfig (FK Medios NULL=global, SYSTEM_VERSIONING ON, retention 10 años).
--   2. Índices: filtered UX vigente por (MedioId,Symbol); cover IX para GetActiveForMedio.
--   3. SP dbo.usp_ChargeableCharConfig_InsertWithClose (SERIALIZABLE + UPDLOCK, forward-only).
--   4. SP dbo.usp_ChargeableCharConfig_GetActiveForMedio (CTE + ROW_NUMBER per-medio/global).
--
-- Patrón: V019 (SYSTEM_VERSIONING + PAGE compression + SERIALIZABLE SP).
-- Idempotente: seguro para re-ejecutar.
-- Reversa: V021_ROLLBACK.sql.
-- Run on: SIGCM2 (dev), SIGCM2_Test_App, SIGCM2_Test_Api.
--
-- Notas:
--   - SysStartTime/SysEndTime como hidden cols: evita colisión con business cols ValidFrom/ValidTo (D4).
--   - DECIMAL(18,4) para PricePerUnit (mayor granularidad que ProductPrices) (D8).
--   - MedioId NULL = global fallback; per-medio overrides global in GetActiveForMedio (D2/D6).
--   - Forward-only estricto: THROW 50409 si new ValidFrom <= activo.ValidFrom (D9).
--   - UX filtered WHERE ValidTo IS NULL: SQL Server trata (NULL,'$') como valor igual → enforza 1 vigente global (D7).
--   - dbo.ChargeableCharConfig_History debe agregarse a TablesToIgnore en SqlTestFixture.cs (Respawn).
--
-- SDD Design: engram sdd/prc-001-word-counter-spike/design

SET QUOTED_IDENTIFIER ON;
SET ANSI_NULLS ON;
SET NOCOUNT ON;
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 1. dbo.ChargeableCharConfig
-- ═══════════════════════════════════════════════════════════════════════

IF OBJECT_ID(N'dbo.ChargeableCharConfig', N'U') IS NULL
BEGIN
    CREATE TABLE dbo.ChargeableCharConfig (
        Id            BIGINT IDENTITY(1,1) NOT NULL
            CONSTRAINT PK_ChargeableCharConfig PRIMARY KEY,
        MedioId       INT           NULL,    -- NULL = global fallback
        Symbol        NVARCHAR(4)   NOT NULL,
        Category      NVARCHAR(32)  NOT NULL, -- enum-as-string: Currency/Percentage/Exclamation/Question/Other
        PricePerUnit  DECIMAL(18,4) NOT NULL,
        ValidFrom     DATE          NOT NULL,
        ValidTo       DATE          NULL,
        IsActive      BIT           NOT NULL
            CONSTRAINT DF_ChargeableCharConfig_IsActive DEFAULT(1),
        FechaCreacion DATETIME2(3)  NOT NULL
            CONSTRAINT DF_ChargeableCharConfig_FechaCreacion DEFAULT(SYSUTCDATETIME()),
        CONSTRAINT FK_ChargeableCharConfig_Medio
            FOREIGN KEY (MedioId) REFERENCES dbo.Medio(Id) ON DELETE NO ACTION,
        CONSTRAINT CK_ChargeableCharConfig_Price_Positive
            CHECK (PricePerUnit > 0),
        CONSTRAINT CK_ChargeableCharConfig_Symbol_NotEmpty
            CHECK (LEN(Symbol) > 0),
        CONSTRAINT CK_ChargeableCharConfig_ValidRange
            CHECK (ValidTo IS NULL OR ValidTo >= ValidFrom)
    );
    PRINT 'Table dbo.ChargeableCharConfig created.';
END
ELSE
    PRINT 'Table dbo.ChargeableCharConfig already exists — skip.';
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 2. SYSTEM_VERSIONING — ChargeableCharConfig
--    SysStartTime/SysEndTime para no colisionar con business cols ValidFrom/ValidTo (D4).
-- ═══════════════════════════════════════════════════════════════════════

IF COL_LENGTH('dbo.ChargeableCharConfig', 'SysStartTime') IS NULL
BEGIN
    ALTER TABLE dbo.ChargeableCharConfig
    ADD
        SysStartTime DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
            CONSTRAINT DF_ChargeableCharConfig_SysStartTime DEFAULT(SYSUTCDATETIME()),
        SysEndTime   DATETIME2(3) GENERATED ALWAYS AS ROW END   HIDDEN NOT NULL
            CONSTRAINT DF_ChargeableCharConfig_SysEndTime   DEFAULT(CONVERT(DATETIME2(3),'9999-12-31 23:59:59.999')),
        PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime);
    PRINT 'ChargeableCharConfig: PERIOD FOR SYSTEM_TIME added (SysStartTime/SysEndTime).';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig') AND temporal_type = 2)
BEGIN
    ALTER TABLE dbo.ChargeableCharConfig
    SET (SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = dbo.ChargeableCharConfig_History,
        HISTORY_RETENTION_PERIOD = 10 YEARS
    ));
    PRINT 'ChargeableCharConfig: SYSTEM_VERSIONING = ON (history: dbo.ChargeableCharConfig_History, retention: 10 years).';
END
ELSE
    PRINT 'ChargeableCharConfig: SYSTEM_VERSIONING already ON — skip.';
GO

IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'ChargeableCharConfig_History' AND schema_id = SCHEMA_ID('dbo'))
   AND NOT EXISTS (
       SELECT 1 FROM sys.partitions p
       JOIN sys.tables t ON t.object_id = p.object_id
       WHERE t.name = 'ChargeableCharConfig_History' AND p.data_compression = 2
   )
BEGIN
    ALTER TABLE dbo.ChargeableCharConfig_History REBUILD WITH (DATA_COMPRESSION = PAGE);
    PRINT 'ChargeableCharConfig_History: rebuilt with PAGE compression.';
END
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 3. Índices
-- ═══════════════════════════════════════════════════════════════════════

-- Un único vigente por (MedioId, Symbol).
-- SQL Server trata NULL como "distinto" en índices únicos: (NULL,'$') colisiona consigo mismo
-- → enforza exactamente 1 vigente global por símbolo (D7).
IF NOT EXISTS (
    SELECT 1 FROM sys.indexes
    WHERE name = 'UX_ChargeableCharConfig_Vigente'
      AND object_id = OBJECT_ID('dbo.ChargeableCharConfig')
)
BEGIN
    CREATE UNIQUE INDEX UX_ChargeableCharConfig_Vigente
        ON dbo.ChargeableCharConfig (MedioId, Symbol)
        WHERE ValidTo IS NULL;
    PRINT 'Index UX_ChargeableCharConfig_Vigente created.';
END
GO

-- Cover para GetActiveForMedio y List.
IF NOT EXISTS (
    SELECT 1 FROM sys.indexes
    WHERE name = 'IX_ChargeableCharConfig_Query'
      AND object_id = OBJECT_ID('dbo.ChargeableCharConfig')
)
BEGIN
    CREATE INDEX IX_ChargeableCharConfig_Query
        ON dbo.ChargeableCharConfig (MedioId, Symbol, ValidFrom, ValidTo)
        INCLUDE (PricePerUnit, IsActive, Category);
    PRINT 'Index IX_ChargeableCharConfig_Query created.';
END
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 4. SP — dbo.usp_ChargeableCharConfig_InsertWithClose
--    Cierre atómico forward-only: SERIALIZABLE + UPDLOCK + HOLDLOCK.
--    @MedioId NULL = global; existencia validada sólo cuando NOT NULL.
--    THROW 50404: Medio not found.
--    THROW 50409: ForwardOnly — new ValidFrom must be > active.ValidFrom.
--    Params de salida: @NewId (BIGINT), @ClosedId (BIGINT — NULL si primer precio).
-- ═══════════════════════════════════════════════════════════════════════
GO
CREATE OR ALTER PROCEDURE dbo.usp_ChargeableCharConfig_InsertWithClose
    @MedioId      INT           = NULL,
    @Symbol       NVARCHAR(4),
    @Category     NVARCHAR(32),
    @PricePerUnit DECIMAL(18,4),
    @ValidFrom    DATE,
    @NewId        BIGINT OUTPUT,
    @ClosedId     BIGINT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

    BEGIN TRY
        BEGIN TRANSACTION;

        -- Validar MedioId sólo cuando se proporciona (NULL = global fallback siempre válido).
        IF @MedioId IS NOT NULL
           AND NOT EXISTS (SELECT 1 FROM dbo.Medio WITH (NOLOCK) WHERE Id = @MedioId)
        BEGIN
            ROLLBACK;
            THROW 50404, 'Medio not found', 1;
        END

        -- Lee el vigente actual con bloqueo de rango para serialización.
        DECLARE @ActiveId BIGINT, @ActiveValidFrom DATE;
        SELECT TOP 1
            @ActiveId        = Id,
            @ActiveValidFrom = ValidFrom
        FROM dbo.ChargeableCharConfig WITH (UPDLOCK, HOLDLOCK, ROWLOCK)
        WHERE ((@MedioId IS NULL     AND MedioId IS NULL)
            OR (@MedioId IS NOT NULL AND MedioId = @MedioId))
          AND Symbol  = @Symbol
          AND ValidTo IS NULL;

        -- Forward-only estricto: new ValidFrom debe ser ESTRICTAMENTE mayor al activo.
        IF @ActiveId IS NOT NULL AND @ValidFrom <= @ActiveValidFrom
        BEGIN
            ROLLBACK;
            THROW 50409, 'ChargeableCharConfigForwardOnly: new ValidFrom must be > active.ValidFrom', 1;
        END

        -- Cierra el vigente previo: ValidTo = ValidFrom(nuevo) - 1 día.
        IF @ActiveId IS NOT NULL
        BEGIN
            UPDATE dbo.ChargeableCharConfig
               SET ValidTo = DATEADD(DAY, -1, @ValidFrom)
             WHERE Id = @ActiveId;
            SET @ClosedId = @ActiveId;
        END
        ELSE
            SET @ClosedId = NULL;

        -- Inserta el nuevo vigente.
        INSERT INTO dbo.ChargeableCharConfig
            (MedioId, Symbol, Category, PricePerUnit, ValidFrom, ValidTo, IsActive)
        VALUES
            (@MedioId, @Symbol, @Category, @PricePerUnit, @ValidFrom, NULL, 1);
        SET @NewId = SCOPE_IDENTITY();

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
        THROW;
    END CATCH
END
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 5. SP — dbo.usp_ChargeableCharConfig_GetActiveForMedio
--    Resolución per-medio + global fallback: 1 fila por Symbol.
--    CTE + ROW_NUMBER PARTITION BY Symbol ORDER BY per-medio(0) vs global(1).
-- ═══════════════════════════════════════════════════════════════════════
GO
CREATE OR ALTER PROCEDURE dbo.usp_ChargeableCharConfig_GetActiveForMedio
    @MedioId   INT,
    @AsOfDate  DATE
AS
BEGIN
    SET NOCOUNT ON;
    WITH Candidates AS (
        SELECT
            Id, MedioId, Symbol, Category, PricePerUnit, ValidFrom, ValidTo, IsActive,
            ROW_NUMBER() OVER (
                PARTITION BY Symbol
                ORDER BY
                    CASE WHEN MedioId = @MedioId THEN 0 ELSE 1 END, -- prefer specific over global
                    ValidFrom DESC
            ) AS rn
        FROM dbo.ChargeableCharConfig
        WHERE IsActive   = 1
          AND ValidFrom <= @AsOfDate
          AND (ValidTo IS NULL OR ValidTo >= @AsOfDate)
          AND (MedioId = @MedioId OR MedioId IS NULL)
    )
    SELECT Id, MedioId, Symbol, Category, PricePerUnit, ValidFrom, ValidTo, IsActive
    FROM   Candidates
    WHERE  rn = 1;
END
GO

PRINT '';
PRINT 'V021 applied — dbo.ChargeableCharConfig (temporal, retention 10y) + UX_ChargeableCharConfig_Vigente + IX_ChargeableCharConfig_Query + usp_ChargeableCharConfig_InsertWithClose + usp_ChargeableCharConfig_GetActiveForMedio.';
PRINT 'Next migration: V022 (seed ChargeableCharConfig).';
GO
