-- ===============================================
-- DML-скрипт: загрузка и трансформация данных
-- Запускается ПОВТОРНО при каждой загрузке (идемпотентно!)
-- ===============================================
--
-- Карта загрузки в этом скрипте:
--   STG → ODS:  orders, order_items, products, customers (снимок: одна строка на BK)
--   STG → DDS:  dim_customer (SCD2, full backfill напрямую из STG — см. комментарий к п.5)
--   ODS → DDS:  dim_product, fact_sales
--   отдельно:   dim_date (генерация календаря)
--

-- 1. STG: имитация загрузки из источников (в реальности — COPY или INSERT из Kafka/NiFi)
-- ⚠️ В продакшене STG часто очищается перед загрузкой (TRUNCATE), либо используется партицирование по дате

DELETE FROM stg.customers_raw;
DELETE FROM stg.orders_raw;
DELETE FROM stg.order_items_raw;
DELETE FROM stg.products_raw;

-- STG (пример вставки с метками времени)
INSERT INTO stg.customers_raw (_load_id, _load_ts, event_ts, customer_id, email, phone, city) VALUES
('batch_20240101_0800', '2024-01-01 08:00', '2024-01-01',  '101','a@ex.com','700','Москва'),
('batch_20240101_0800', '2024-01-01 08:00', '2024-01-01',  '102','c@ex.com','701','СПб'),
('batch_20240516_0800', '2024-05-16 08:00', '2024-05-16',  '101','b@ex.com','700','Москва'),
('batch_20241001_0800', '2024-10-01 08:00', '2024-10-01',  '101','b@ex.com','700','Санкт-Петербург');


INSERT INTO stg.orders_raw (order_id, order_date, customer_id) VALUES
('5001', '2024-01-10', '101'),
('5002', '2024-02-05', '102');

INSERT INTO stg.order_items_raw (order_item_id, order_id, product_id, qty, price_at_sale) VALUES
('1', '5001', '9001', '2', '100.00'),
('2', '5001', '9002', '1', '50.00'),
('3', '5002', '9001', '1', '100.00');

INSERT INTO stg.products_raw (product_id, name) VALUES
('9001', 'Phone'),
('9002', 'Case');

-- 2. ODS: очистка и типизация
-- ⚠️ В продакшене используем UPSERT (INSERT ... ON CONFLICT DO UPDATE) или incremental load, не TRUNCATE+INSERT

TRUNCATE ods.customers, ods.orders, ods.order_items, ods.products;

INSERT INTO ods.orders (order_id, order_date, customer_id)
SELECT
    order_id::INT,
    TO_DATE(order_date, 'YYYY-MM-DD'),
    customer_id::INT
FROM stg.orders_raw
WHERE order_date IS NOT NULL AND customer_id ~ '^\d+$';

-- берём по BK самую позднюю запись (по дате события, иначе по дате загрузки)
WITH src AS (
  SELECT
      s.customer_id::INT                                     AS customer_id,
      NULLIF(trim(s.email), '')                              AS email,
      NULLIF(trim(s.phone), '')                              AS phone,
      NULLIF(trim(s.city),  '')                              AS city,
      NULLIF(s.event_ts, '')::timestamp                      AS event_ts,
      s._load_id,
      s._load_ts,
      COALESCE(NULLIF(s.event_ts, '')::date, s._load_ts::date) AS eff_date
  FROM stg.customers_raw s
  WHERE s.customer_id ~ '^\d+$'
),
ranked AS (
  SELECT
         customer_id, email, phone, city, event_ts, _load_id, _load_ts,
         row_number() OVER (PARTITION BY customer_id ORDER BY eff_date DESC, _load_ts DESC) AS rn
  FROM src
)
INSERT INTO ods.customers (customer_id, email, phone, city, event_ts, _load_id, _load_ts)
SELECT customer_id, email, phone, city, event_ts, _load_id, _load_ts
FROM ranked
WHERE rn = 1;

INSERT INTO ods.order_items (order_item_id, order_id, product_id, qty, price_at_sale)
SELECT
    order_item_id::INT,
    order_id::INT,
    product_id::INT,
    NULLIF(qty, '')::INT,
    NULLIF(price_at_sale, '')::NUMERIC(10,2)
FROM stg.order_items_raw
WHERE qty ~ '^\d+$' AND price_at_sale ~ '^\d+(\.\d+)?$';

INSERT INTO ods.products (product_id, name)
SELECT
    product_id::INT,
    TRIM(name)
FROM stg.products_raw
WHERE product_id ~ '^\d+$';

-- 3. DDS: dim_date — генерация календаря (идемпотентно: можно пересоздавать)
-- В реальности — делается ОДИН РАЗ, либо дополняется по мере необходимости

DELETE FROM dds.dim_date;

WITH RECURSIVE dates AS (
    SELECT DATE '2023-01-01' AS d
    UNION ALL
    SELECT (d + INTERVAL '1 day')::DATE  -- ← приведение к DATE
    FROM dates
    WHERE d + INTERVAL '1 day' <= DATE '2027-12-31'
)
INSERT INTO dds.dim_date (
    date_key, date_actual, year, quarter, month, day,
    weekday_name, weekday_num, is_first_week
)
SELECT
    CAST(TO_CHAR(d, 'YYYYMMDD') AS INT),
    d,
    EXTRACT(YEAR FROM d)::SMALLINT,
    EXTRACT(QUARTER FROM d)::SMALLINT,
    EXTRACT(MONTH FROM d)::SMALLINT,
    EXTRACT(DAY FROM d)::SMALLINT,
    TRIM(TO_CHAR(d, 'Day')),  -- ← TRIM — убрать trailing space
    EXTRACT(DOW FROM d)::SMALLINT,
    d BETWEEN DATE_TRUNC('month', d) 
          AND DATE_TRUNC('month', d) + INTERVAL '1 month' - INTERVAL '1 day'
      AND EXTRACT(DAY FROM d) <= 7
FROM dates;

-- 4. DDS: dim_product — полная перезагрузка (если товары редко меняются)
-- В реальности — инкрементальная загрузка по BK

DELETE FROM dds.dim_product;

INSERT INTO dds.dim_product (product_bk, product_name)
SELECT product_id, name
FROM ods.products;

-- 5. DDS: dim_customer — первичная загрузка SCD2 (full backfill из STG)
--    В ЭТОМ ДЕМО: dim_customer строится напрямую из stg.customers_raw, который играет роль
--    устойчивого event-лога (все события по клиенту в одном месте).
--    Это удобно для учебной первичной загрузки (full backfill), когда мы один раз
--    восстанавливаем всю историю клиента.
--    В РЕАЛЬНОМ DWH: так делают редко. Исторические измерения обычно строят
--    поверх очищенных и нормализованных слоёв (ODS / PSA / Data Vault).
--    Для примера инкрементальной заливки SCD2 по снимку из ODS см. 03_demo_increment.sql и SCD.md.
--
-- Идея SCD2 простыми словами:
--   - одна строка = один период, когда атрибуты клиента (email/phone/city) были одинаковыми;
--   - valid_from = дата, когда "стало так";
--   - valid_to = дата следующего изменения (NULL = текущая версия).
--
-- Откуда берём дату изменения:
--   - если в событии есть event_ts — считаем, что изменение произошло тогда;
--   - если event_ts пустой — берём дату загрузки (_load_ts), чтобы не терять историю.
--
-- Важно для демо: считаем, что у клиента не бывает двух разных изменений в один и тот же день.
TRUNCATE dds.dim_customer, dds.fact_sales;

WITH src AS (  -- 1) Приводим типы, готовим дату изменения (eff_date) и считаем hashdiff атрибутов
  SELECT
    s.customer_id::INT                                         AS customer_bk,
    NULLIF(trim(s.email), '')                                   AS email,
    NULLIF(trim(s.phone), '')                                   AS phone,
    NULLIF(trim(s.city),  '')                                   AS city,
    COALESCE(NULLIF(s.event_ts, '')::date, s._load_ts::date)     AS eff_date,
    dds.customer_hash(s.email, s.phone, s.city)                 AS hashdiff
  FROM stg.customers_raw s
  WHERE s.customer_id ~ '^\d+$'
),
ordered AS (  -- 2) Сортируем по датам и смотрим "какой hashdiff был до этого" (LAG)
  SELECT *,
          lag(hashdiff) OVER (PARTITION BY customer_bk ORDER BY eff_date) AS prev_hash
  FROM src
),
changes AS (  -- 3) Оставляем только первое состояние и реальные изменения (где hashdiff поменялся)
  SELECT *
  FROM ordered
  WHERE prev_hash IS DISTINCT FROM hashdiff OR prev_hash IS NULL
),
framed AS (  -- 4) Превращаем изменения в периоды: valid_to = дата следующего изменения (LEAD)
  SELECT
    customer_bk, email, phone, city, hashdiff,
    eff_date                                                         AS valid_from,
    lead(eff_date) OVER (PARTITION BY customer_bk ORDER BY eff_date)  AS valid_to
  FROM changes
)
INSERT INTO dds.dim_customer (
    customer_bk, email, phone, city, hashdiff,
    valid_from,                                           valid_to,
    created_at, updated_at
)
SELECT
    customer_bk, email, phone, city, hashdiff,
    valid_from,
    valid_to,
    now(), now()
FROM framed
ORDER BY customer_bk, valid_from;

-- 6. DDS: fact_sales — загрузка фактов с учётом SCD
-- В продакшене — фильтруем по диапазону дат (инкрементально)

--TRUNCATE dds.fact_sales;

INSERT INTO dds.fact_sales (customer_sk, product_sk, date_key, quantity, amount)
SELECT
    dc.customer_sk,
    dp.product_sk,
    CAST(TO_CHAR(o.order_date, 'YYYYMMDD') AS INT),
    oi.qty,
    oi.price_at_sale * oi.qty
FROM ods.orders o
JOIN ods.order_items oi ON o.order_id = oi.order_id
JOIN ods.products p     ON oi.product_id = p.product_id
JOIN dds.dim_product dp ON p.product_id = dp.product_bk
JOIN dds.dim_customer dc
  ON o.customer_id = dc.customer_bk
 AND o.order_date >= dc.valid_from
 AND (dc.valid_to IS NULL OR o.order_date < dc.valid_to);

-- 7. Проверка — вывод итогов (не часть ETL, но полезно для отладки)
-- В реальном пайплайне такие SELECT выносятся в отдельные скрипты или дашборды

 SELECT 'dim_customer current = ' || COUNT(*) FROM dds.dim_customer WHERE valid_to IS NULL;
 SELECT 'fact_sales count = ' || COUNT(*) FROM dds.fact_sales;
