-- V019__create_product_prices.sql
-- PRD-003: ProductPrices — historial de precios por Producto con vigencia civil (Cat2).
--
-- Cambios:
--   1. dbo.ProductPrices (FK Product, SYSTEM_VERSIONING ON, retention 10 años).
--   2. Índices: filtered UQ un único activo; cover compuesto para GetPriceAt.
--   3. SP dbo.usp_AddProductPrice (SERIALIZABLE + UPDLOCK, cierre atómico forward-only).
--
-- Patrón: V018 (SYSTEM_VERSIONING + PAGE compression).
-- Idempotente: seguro para re-ejecutar.
-- Reversa: V019_ROLLBACK.sql.
-- Run on: SIGCM2 (dev), SIGCM2_Test_App, SIGCM2_Test_Api.
--
-- Notas:
--   - SysStartTime/SysEndTime como nombres de cols HIDDEN (no ValidFrom/ValidTo):
--     evita colisión con las business cols PriceValidFrom/PriceValidTo (D1).
--   - DECIMAL(12,2) para Price (distinto de Product.BasePrice DECIMAL(18,4)) — precios retail
--     en pesos con 2 decimales; la diferencia es intencional (D6).
--   - Sin seed inicial — Product.BasePrice queda ortogonal como fallback (OQ-B, D8).
--   - Forward-only estricto en SP: THROW 50409 si new PVF <= active PVF (no solo <).
--
-- SDD Design: engram sdd/prd-003-product-prices-historicos/design

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

-- ═══════════════════════════════════════════════════════════════════════
-- 1. dbo.ProductPrices
-- ═══════════════════════════════════════════════════════════════════════

IF OBJECT_ID(N'dbo.ProductPrices', N'U') IS NULL
BEGIN
    CREATE TABLE dbo.ProductPrices (
        Id              BIGINT IDENTITY(1,1) NOT NULL
            CONSTRAINT PK_ProductPrices PRIMARY KEY,
        ProductId       INT           NOT NULL,
        Price           DECIMAL(12,2) NOT NULL,
        PriceValidFrom  DATE          NOT NULL,
        PriceValidTo    DATE          NULL,
        FechaCreacion   DATETIME2(3)  NOT NULL
            CONSTRAINT DF_ProductPrices_FechaCreacion DEFAULT(SYSUTCDATETIME()),
        CONSTRAINT FK_ProductPrices_Product
            FOREIGN KEY (ProductId) REFERENCES dbo.Product(Id) ON DELETE NO ACTION,
        CONSTRAINT CK_ProductPrices_Price_Positive
            CHECK (Price > 0),
        CONSTRAINT CK_ProductPrices_ValidRange
            CHECK (PriceValidTo IS NULL OR PriceValidTo >= PriceValidFrom)
    );
    PRINT 'Table dbo.ProductPrices created.';
END
ELSE
    PRINT 'Table dbo.ProductPrices already exists — skip.';
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 2. SYSTEM_VERSIONING — ProductPrices
--    Las hidden cols se llaman SysStartTime/SysEndTime para evitar
--    colisión con las business cols PriceValidFrom/PriceValidTo (D1).
-- ═══════════════════════════════════════════════════════════════════════

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

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

IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'ProductPrices_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 = 'ProductPrices_History' AND p.data_compression = 2
   )
BEGIN
    ALTER TABLE dbo.ProductPrices_History REBUILD WITH (DATA_COMPRESSION = PAGE);
    PRINT 'ProductPrices_History: rebuilt with PAGE compression.';
END
GO

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

-- Un único activo por producto (imposibilita violar a nivel BD).
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UX_ProductPrices_Active' AND object_id = OBJECT_ID('dbo.ProductPrices'))
BEGIN
    CREATE UNIQUE INDEX UX_ProductPrices_Active
        ON dbo.ProductPrices (ProductId)
        WHERE PriceValidTo IS NULL;
    PRINT 'Index UX_ProductPrices_Active created.';
END
GO

-- Cover para GetPriceAt / GetByProductIdAsync (ProductId + PriceValidFrom con INCLUDEs).
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_ProductPrices_Lookup' AND object_id = OBJECT_ID('dbo.ProductPrices'))
BEGIN
    CREATE INDEX IX_ProductPrices_Lookup
        ON dbo.ProductPrices (ProductId, PriceValidFrom DESC)
        INCLUDE (Price, PriceValidTo);
    PRINT 'Index IX_ProductPrices_Lookup created.';
END
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 4. SP — dbo.usp_AddProductPrice
--    Cierre atómico forward-only: SERIALIZABLE + UPDLOCK + HOLDLOCK.
--    Params de salida: @NewId (BIGINT), @ClosedId (BIGINT — NULL si primer precio).
-- ═══════════════════════════════════════════════════════════════════════
GO
CREATE OR ALTER PROCEDURE dbo.usp_AddProductPrice
    @ProductId      INT,
    @Price          DECIMAL(12,2),
    @PriceValidFrom 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;

        -- Validación: producto debe existir y estar activo.
        IF NOT EXISTS (SELECT 1 FROM dbo.Product WITH (NOLOCK) WHERE Id = @ProductId AND IsActive = 1)
        BEGIN
            ROLLBACK;
            THROW 50404, 'Product not found or inactive', 1;
        END

        -- Lee activo con UPDLOCK + HOLDLOCK — bloquea el range key del filtered index.
        DECLARE @ActiveId BIGINT, @ActivePVF DATE;
        SELECT TOP 1
            @ActiveId  = Id,
            @ActivePVF = PriceValidFrom
        FROM dbo.ProductPrices WITH (UPDLOCK, HOLDLOCK, ROWLOCK)
        WHERE ProductId = @ProductId AND PriceValidTo IS NULL;

        -- Forward-only estricto: el nuevo PVF debe ser ESTRICTAMENTE mayor al activo.
        IF @ActiveId IS NOT NULL AND @PriceValidFrom <= @ActivePVF
        BEGIN
            ROLLBACK;
            THROW 50409, 'ProductPriceForwardOnly: new PriceValidFrom must be > active.PriceValidFrom', 1;
        END

        -- Cierra el activo previo: PVT = PVF(nuevo) - 1 día.
        IF @ActiveId IS NOT NULL
        BEGIN
            UPDATE dbo.ProductPrices
               SET PriceValidTo = DATEADD(DAY, -1, @PriceValidFrom)
             WHERE Id = @ActiveId;
            SET @ClosedId = @ActiveId;
        END
        ELSE
            SET @ClosedId = NULL;

        -- Inserta el nuevo activo.
        INSERT INTO dbo.ProductPrices (ProductId, Price, PriceValidFrom, PriceValidTo)
        VALUES (@ProductId, @Price, @PriceValidFrom, NULL);
        SET @NewId = SCOPE_IDENTITY();

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

PRINT '';
PRINT 'V019 applied — dbo.ProductPrices (temporal, retention 10y) + UX_ProductPrices_Active + IX_ProductPrices_Lookup + usp_AddProductPrice.';
PRINT 'Next migration: V020 (TBD).';
GO
