-- 152_mv_ddu_price_indicator.sql
-- Issue #99 — «Индикатор цен ДДУ» (ARN-framework applied to primary market).
--
-- ARN (НСПД /api/market-analytics) считает индикатор цен ВТОРИЧНОГО рынка.
-- У нас в rosreestr_deals — ПЕРВИЧКА (ДДУ). Это дополнение их фреймворка:
-- тот же «индикатор = median(price/m²) тек.периода / median базисного периода»,
-- но по новостройкам.
--
-- ── MVP SCOPE / честные упрощения (см. issue #99 «Risks / open») ───────────
-- Полный ARN: 4 indicator-типа (M/Q/H/Y) × 6 area buckets × 3 calc methods ×
-- 4 уровня гранулярности (Округ→Субъект→Нас.пункт→Cad_quarter). Реализовано
-- НЕ всё — данных по первичке хватает только на часть матрицы:
--
--   1. ГРАНУЛЯРНОСТЬ = СУБЪЕКТ (region_code=66), НЕ cad_quarter.
--      Per-cad_quarter × area_bucket для ДДУ безнадёжно разрежено: после смены
--      агрегации rosreestr (2025Q1) на квартал приходится ~190-220 строк ИТОГО
--      по всей области; на отдельный cad_quarter — единицы. Субъект-уровень —
--      единственный статистически устойчивый уровень для первички.
--      (Per-quarter price ОТДЕЛЬНО уже есть в mv_quarter_price_index — там
--      кросс-секционный индекс «квартал vs город», другая метрика.)
--
--   2. PERIOD_TYPE = 'Q' (квартал). M/H/Y не делаем:
--      - M (месяц): rosreestr публикует данные поквартально (period_start_date —
--        начало квартала), месячной гранулярности в источнике НЕТ.
--      - H/Y: см. п.3 — слишком мало сопоставимых периодов.
--
--   3. ОКНО = 2025-Q2 .. now. ЖЁСТКИЙ обрыв на 2025-Q1:
--      rosreestr с 2025Q1 резко увеличил агрегацию (1 строка = пакет из N ДДУ,
--      area=SUM, deal_count=N). Сырых строк до 2025Q1: 700-2400/кв; с 2025Q2:
--      ~190-220/кв. Медиана price/m² скачет 66K→154K на границе 2024Q4→2025Q2
--      ЧИСТО из-за смены формата, не из-за инфляции. Поэтому периоды до 2025Q2
--      исключены — иначе basis/prev-индикатор даёт фейковые 1.4-1.6.
--      => calc_method 3 (год назад) НЕ реализуем: нет сопоставимого 2025Q1-.
--
--   4. CALC METHODS: 1 = basis (к первому периоду окна = 2025-Q2),
--                    2 = previous (к предыдущему ПРИСУТСТВУЮЩЕМУ периоду).
--      method 3 (год назад) — BLOCKED (нет данных, см. п.3).
--
--   5. AREA_BUCKET: 1..6 по ARN (0-25/25-40/40-60/60-80/80-100/100+ м²)
--      на ПОКВАРТИРНОЙ площади (area/deal_count — пакетные ДДУ!), плюс
--      bucket 0 = ALL (headline, все площади вместе).
--
--   6. MIN_DEALS = 10 на (bucket, quarter): ниже порога медиана шумная →
--      строка не создаётся (HAVING). prev/basis сравнивают только присутствующие
--      периоды → prev_period_value хранится явно (может быть не «−1 квартал»).
--
-- ── VALIDATION (prod, 2026-06, source rosreestr_deals) ─────────────────────
--   ALL-buckets (bucket=0), method=previous:
--     2025-Q2 (basis) → 2025-Q3 1.0026 → 2025-Q4 1.0501 → 2026-Q1 1.0185
--   ARN benchmark Екатеринбург Q1 2026 (вторичка) = 1.03.
--   Наш Q1 2026 (первичка) = 1.0185 → within ±5% (issue acceptance). ✓
--   Расхождение ожидаемо: primary vs secondary (объяснять в UI tooltip).
--
-- Источник: rosreestr_deals (партиционированная, фильтр realestate_type_code=
--   '002001003000' новостройки + doc_type='ДДУ'). Прямой запрос к таблице
--   (НЕ через mv_quarter_price_per_m2: тому MV нужна area_bucket-разбивка,
--   которой там нет — он агрегирует по cad_quarter без поквартирной площади).
--
-- REFRESH CONCURRENTLY: UNIQUE индекс создаётся сразу после CREATE WITH NO DATA.
-- Первый populate — non-concurrent (MV пуст). Re-apply idempotent: DROP ... CASCADE.
-- _schema_migrations (deploy.yml) предотвращает повторный apply на prod.
-- Refresh helper: backend/app/services/analytics/ddu_price_indicator.py (не в beat).

BEGIN;

DROP MATERIALIZED VIEW IF EXISTS mv_ddu_price_indicator CASCADE;

CREATE MATERIALIZED VIEW mv_ddu_price_indicator AS
WITH per_unit AS (
    -- Поквартирная площадь и цена/м². Пакетные ДДУ: area=SUM, deal_count=N,
    -- поэтому area_per_unit = area/deal_count (иначе пакет из 5 студий
    -- попадает в bucket «80+»). price_per_sqm pre-computed; fallback deal_price/area.
    SELECT
        date_trunc('quarter', period_start_date)::date AS quarter_start,
        (area / deal_count)                            AS area_per_unit,
        CASE
            WHEN price_per_sqm IS NOT NULL           THEN price_per_sqm
            WHEN deal_price IS NOT NULL AND area > 0 THEN deal_price / area
            ELSE NULL
        END                                            AS price_m2
    FROM rosreestr_deals
    WHERE region_code = 66
      AND realestate_type_code = '002001003000'  -- квартиры в новостройках
      AND doc_type = 'ДДУ'
      AND deal_count > 0
      AND area > 0
      -- Жёсткий обрыв формата (см. шапку п.3): окно строго с 2025-Q2.
      AND period_start_date >= DATE '2025-04-01'
),
filtered AS (
    SELECT quarter_start, area_per_unit, price_m2
    FROM per_unit
    WHERE area_per_unit BETWEEN 10 AND 300        -- реалистичная 1-квартира
      AND price_m2 BETWEEN 30000 AND 800000       -- срез выбросов (как в #33 MV)
),
bucketed AS (
    -- Каждую сделку дублируем в её bucket (1..6) И в bucket 0 (ALL/headline)
    -- через UNION ALL, чтобы один GROUP BY посчитал и разбивку, и агрегат.
    SELECT quarter_start, price_m2,
           CASE
               WHEN area_per_unit < 25  THEN 1
               WHEN area_per_unit < 40  THEN 2
               WHEN area_per_unit < 60  THEN 3
               WHEN area_per_unit < 80  THEN 4
               WHEN area_per_unit < 100 THEN 5
               ELSE 6
           END AS area_bucket
    FROM filtered
    UNION ALL
    SELECT quarter_start, price_m2, 0 AS area_bucket
    FROM filtered
),
agg AS (
    SELECT
        area_bucket,
        quarter_start,
        COUNT(*)                                                AS deals_count,
        PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price_m2)   AS median_price_per_m2
    FROM bucketed
    GROUP BY area_bucket, quarter_start
    HAVING COUNT(*) >= 10        -- min_deals: ниже порога медиана шумная
),
indexed AS (
    SELECT
        area_bucket,
        quarter_start,
        deals_count,
        median_price_per_m2,
        -- basis = первый присутствующий квартал окна для этого bucket
        FIRST_VALUE(median_price_per_m2) OVER w_asc  AS basis_median,
        -- previous = предыдущий ПРИСУТСТВУЮЩИЙ квартал (после HAVING-фильтра)
        LAG(median_price_per_m2)  OVER w_asc         AS prev_median,
        LAG(quarter_start)        OVER w_asc         AS prev_quarter_start
    FROM agg
    WINDOW w_asc AS (PARTITION BY area_bucket ORDER BY quarter_start)
)
SELECT
    area_bucket,
    -- period_value в ARN-стиле: '2026-Q1'
    (EXTRACT(YEAR FROM quarter_start)::int::text || '-Q'
        || EXTRACT(QUARTER FROM quarter_start)::int::text)      AS period_value,
    quarter_start,
    deals_count,
    median_price_per_m2::numeric(12, 2)                         AS median_price_per_m2,
    -- calc_method 1: basis (к первому периоду окна)
    (median_price_per_m2 / NULLIF(basis_median, 0))::numeric(8, 4) AS index_basis,
    -- calc_method 2: previous (к предыдущему присутствующему периоду)
    (median_price_per_m2 / NULLIF(prev_median, 0))::numeric(8, 4)  AS index_previous,
    -- какой период реально сравнивается в index_previous (может быть не −1 кв,
    -- если промежуточный quarter не прошёл HAVING) — честность для UI tooltip
    CASE WHEN prev_quarter_start IS NOT NULL THEN
        (EXTRACT(YEAR FROM prev_quarter_start)::int::text || '-Q'
            || EXTRACT(QUARTER FROM prev_quarter_start)::int::text)
    END                                                         AS prev_period_value,
    'Q'::text                                                   AS period_type,
    NOW()                                                       AS computed_at
FROM indexed
WITH NO DATA;

-- UNIQUE индекс для REFRESH CONCURRENTLY + O(1) lookup эндпойнтом.
CREATE UNIQUE INDEX IF NOT EXISTS mv_ddu_price_indicator_pk
    ON mv_ddu_price_indicator (area_bucket, period_value);

-- Первый populate (non-concurrent — MV пуст, CONCURRENTLY на пустом запрещён).
REFRESH MATERIALIZED VIEW mv_ddu_price_indicator;

COMMENT ON MATERIALIZED VIEW mv_ddu_price_indicator IS
    'ARN-style ценовой индикатор ПЕРВИЧНОГО рынка (ДДУ) по кварталам × area_bucket. '
    'Source: rosreestr_deals (region 66, realestate_type_code=002001003000, doc_type=ДДУ). '
    'MVP scope (issue #99): granularity=субъект (НЕ cad_quarter — первичка разрежена), '
    'period_type=Q, окно с 2025-Q2 (обрыв формата rosreestr на 2025-Q1), '
    'calc methods 1=basis / 2=previous (method 3 год-назад BLOCKED: нет данных). '
    'area_bucket 0=ALL(headline)/1..6=ARN-площади на поквартирной area/deal_count. '
    'min_deals=10/строка. Q1-2026 headline=1.0185 vs ARN-вторичка 1.03 (±5%, primary vs secondary). '
    'Refresh: ddu_price_indicator.refresh_ddu_price_indicator() (не в beat). Issue #99.';

COMMIT;
