-- 153_sale_share_listings_floors_plausibility.sql
--
-- CONTEXT: плаузибилити-гейт знаменателя `denom >= GREATEST(total_floors, 8)`
--   (мигр. 145) пропускает дома, у которых ГАР дико недосчитал квартиры И нет
--   ЖКХ-счёта по guid, А У САМОГО дома total_floors NULL. Прод-кейс — Переходный
--   пер. 6: gar_flat_count=9, house.total_floors NULL → GREATEST(0,8)=8, гейт 9>=8
--   проходит → ложные 77.8% «доли в продаже». Но ВСЕ листинги дома заявляют
--   total_floors 19-26 (это 19-24-эт. башня, ЖКХ=165 квартир) — здание в N этажей
--   физически не может иметь ~9 квартир. Сигнал недосчёта знаменателя — этажность,
--   о которой говорят сами листинги дома, а не (отсутствующая) house.total_floors.
--
-- WHAT:
--   v_building_sale_share — усиливаем нижнюю границу плаузибилити-гейта знаменателя
--   медианной этажностью оставленных (KEPT) активных листингов дома:
--     1) В CTE listing_agg добавлен ОДИН внутренний агрегат listings_med_floors =
--        percentile_cont(0.5) WITHIN GROUP (ORDER BY l.total_floors) под ТЕМИ ЖЕ
--        гео(≤300м)+floors(±3)-фильтрами, что и active_secondary (мигр. 150/152) —
--        чтобы чужие/дальние/height-mismatched листинги не задирали медиану.
--        Медиана (а не max) устойчива к одному чужому высокому листингу.
--     2) Обе плаузибилити-CASE (sale_share_pct, sale_share_pct_45d) расширяют нижнюю
--        границу этажа с `GREATEST(COALESCE(h.total_floors,0), 8)` до
--        `GREATEST(COALESCE(h.total_floors,0), COALESCE(la.listings_med_floors,0)::int, 8)`:
--        знаменатель должен быть НЕ МЕНЬШЕ собственной этажности дома ЛИБО медианной
--        этажности его листингов ЛИБО 8. Часть `AND числитель <= denom` и само деление
--        НЕ изменены; COALESCE-цепочка знаменателя (ЖКХ-приоритет) НЕ изменена.
--   listings_med_floors — ВНУТРЕННИЙ агрегат CTE, в top-level SELECT НЕ выводится.
--   Остальные 4 FILTER-агрегата (active_secondary, listings_45d, median_price_rub,
--   median_price_per_m2, avg_days_on_market — 5 шт.) с их гео+floors-гардами, весь
--   top-level SELECT / WHERE / appended-колонки (zhkh_flat_count, flat_count_source)
--   — БАЙТ-В-БАЙТ как в мигр. 152, дельта только: +1 агрегат в listing_agg и +1 терм
--   в двух GREATEST.
--
-- DEPENDENCIES: 143 (view + houses.gar_*), 144 (canon match → gar_flat_count),
--   145 (плаузибилити-гейт знаменателя GREATEST(total_floors,8)), 146 (listings_45d +
--   sale_share_pct_45d + zhkh в COALESCE + houses.zhkh_flat_count), 147 (canon strip
--   geo-prefixes), 148 (дедуп кросс-площадочных дублей), 149 (ЖКХ-приоритет знаменателя
--   + zhkh_flat_count/flat_count_source колонки + houses.zhkh_floors_raw text), 150
--   (гео-фильтр числителя ≤300м в CTE), 151 (clean bare-street aliases — view НЕ
--   трогала), 152 (houses.zhkh_floors smallint + floors-guard ±3 во всех 5 FILTER).
--   Базируется на текущем (152) определении view — меняем ТОЛЬКО CTE listing_agg
--   (+1 агрегат) + два GREATEST в CASE. Использует listings.total_floors (int),
--   houses.total_floors (int), houses.zhkh_floors (smallint, мигр. 152).
--
-- SAFETY / IDEMPOTENCY: CREATE OR REPLACE VIEW ONLY (структура top-level колонок не
--   меняется — те же позиции/типы/имена, что в 152; listings_med_floors внутренний) +
--   COMMENT. Никакого DDL над таблицами (zhkh_floors уже добавлен мигр. 152). Повторный
--   прогон — no-op (REPLACE на идентичное определение). Деплой-раннер гонит файл через
--   psql -v ON_ERROR_STOP=on БЕЗ --single-transaction → транзакцию открывает САМ файл
--   (BEGIN/COMMIT ниже), как 146/148/149/150/152.
--
-- NB по нумерации: последний занятый = 152; следующий свободный sequential = 153
--   (проверено `ls tradein-mvp/backend/data/sql | grep '^15'` → 150, 151, 152; коллизий нет).
--
-- Deploy order: после 152_sale_share_floors_guard.sql.

BEGIN;

CREATE OR REPLACE VIEW v_building_sale_share AS
 WITH listing_agg AS (
         SELECT l.house_id_fk AS house_id,
            count(DISTINCT (l.rooms, round(l.area_m2), l.floor)) FILTER (WHERE l.is_active AND l.listing_segment = 'vtorichka'::text AND (l.geom IS NULL OR hg.geom IS NULL OR ST_DistanceSphere(l.geom, hg.geom) <= 300) AND (l.total_floors IS NULL OR COALESCE(hg.zhkh_floors, hg.total_floors) IS NULL OR abs(l.total_floors - COALESCE(hg.zhkh_floors, hg.total_floors)) <= 3)) AS active_secondary,
            count(DISTINCT (l.rooms, round(l.area_m2), l.floor)) FILTER (
                WHERE l.listing_segment = 'vtorichka'::text
                  AND l.last_seen_at >= (now() - interval '45 days')
                  AND (l.geom IS NULL OR hg.geom IS NULL OR ST_DistanceSphere(l.geom, hg.geom) <= 300)
                  AND (l.total_floors IS NULL OR COALESCE(hg.zhkh_floors, hg.total_floors) IS NULL OR abs(l.total_floors - COALESCE(hg.zhkh_floors, hg.total_floors)) <= 3)
            ) AS listings_45d,
            percentile_cont(0.5::double precision) WITHIN GROUP (ORDER BY (l.price_rub::double precision))
                FILTER (WHERE l.is_active AND l.listing_segment = 'vtorichka'::text AND l.price_rub IS NOT NULL AND (l.geom IS NULL OR hg.geom IS NULL OR ST_DistanceSphere(l.geom, hg.geom) <= 300) AND (l.total_floors IS NULL OR COALESCE(hg.zhkh_floors, hg.total_floors) IS NULL OR abs(l.total_floors - COALESCE(hg.zhkh_floors, hg.total_floors)) <= 3)) AS median_price_rub,
            percentile_cont(0.5::double precision) WITHIN GROUP (ORDER BY (l.price_per_m2::double precision))
                FILTER (WHERE l.is_active AND l.listing_segment = 'vtorichka'::text AND l.price_per_m2 IS NOT NULL AND (l.geom IS NULL OR hg.geom IS NULL OR ST_DistanceSphere(l.geom, hg.geom) <= 300) AND (l.total_floors IS NULL OR COALESCE(hg.zhkh_floors, hg.total_floors) IS NULL OR abs(l.total_floors - COALESCE(hg.zhkh_floors, hg.total_floors)) <= 3)) AS median_price_per_m2,
            avg(l.days_on_market)
                FILTER (WHERE l.is_active AND l.listing_segment = 'vtorichka'::text AND l.days_on_market IS NOT NULL AND (l.geom IS NULL OR hg.geom IS NULL OR ST_DistanceSphere(l.geom, hg.geom) <= 300) AND (l.total_floors IS NULL OR COALESCE(hg.zhkh_floors, hg.total_floors) IS NULL OR abs(l.total_floors - COALESCE(hg.zhkh_floors, hg.total_floors)) <= 3)) AS avg_days_on_market,
            percentile_cont(0.5) WITHIN GROUP (ORDER BY l.total_floors)
                FILTER (WHERE l.is_active AND l.listing_segment = 'vtorichka'::text AND (l.geom IS NULL OR hg.geom IS NULL OR ST_DistanceSphere(l.geom, hg.geom) <= 300) AND (l.total_floors IS NULL OR COALESCE(hg.zhkh_floors, hg.total_floors) IS NULL OR abs(l.total_floors - COALESCE(hg.zhkh_floors, hg.total_floors)) <= 3)) AS listings_med_floors
           FROM listings l
             JOIN houses hg ON hg.id = l.house_id_fk
          WHERE l.house_id_fk IS NOT NULL
          GROUP BY l.house_id_fk
        )
 SELECT h.id AS house_id,
    h.short_address,
    h.full_address,
    h.address,
    h.lat,
    h.lon,
    h.year_built,
    h.house_type,
    h.total_floors,
    h.series_name,
    h.is_emergency,
    COALESCE(h.zhkh_flat_count, h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0)) AS flat_count_effective,
    h.gar_flat_count,
    h.gar_match_method,
    la.active_secondary,
    la.median_price_rub,
    la.median_price_per_m2,
    la.avg_days_on_market,
        CASE
            WHEN COALESCE(h.zhkh_flat_count, h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0))
                     >= GREATEST(COALESCE(h.total_floors, 0), COALESCE(la.listings_med_floors, 0)::int, 8)
             AND la.active_secondary <= COALESCE(h.zhkh_flat_count, h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0))
            THEN round(100.0 * la.active_secondary::numeric
                       / COALESCE(h.zhkh_flat_count, h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0))::numeric, 1)
            ELSE NULL::numeric
        END AS sale_share_pct,
    la.listings_45d,
        CASE
            WHEN COALESCE(h.zhkh_flat_count, h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0))
                     >= GREATEST(COALESCE(h.total_floors, 0), COALESCE(la.listings_med_floors, 0)::int, 8)
             AND la.listings_45d <= COALESCE(h.zhkh_flat_count, h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0))
            THEN round(100.0 * la.listings_45d::numeric
                       / COALESCE(h.zhkh_flat_count, h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0))::numeric, 1)
            ELSE NULL::numeric
        END AS sale_share_pct_45d,
    h.zhkh_flat_count,
    CASE
        WHEN h.zhkh_flat_count IS NOT NULL THEN 'zhkh'
        WHEN h.gar_flat_count IS NOT NULL THEN 'gar'
        WHEN NULLIF(h.total_units, 0) IS NOT NULL THEN 'total_units'
        WHEN NULLIF(h.flat_count, 0) IS NOT NULL THEN 'flat_count'
        ELSE NULL::text
    END AS flat_count_source
   FROM houses h
     JOIN listing_agg la ON la.house_id = h.id
  WHERE h.geom IS NOT NULL AND (la.active_secondary > 0 OR la.listings_45d > 0);

COMMENT ON VIEW v_building_sale_share IS
  'Per-building rollup вторички для «доли квартир дома в продаже» (мигр. 143; знаменатель — '
  'ГАР canon-match мигр. 144; 2-й источник ЖКХ + окно 45д мигр. 146; дедуп кросс-площадочных '
  'дублей мигр. 148; ЖКХ-приоритет знаменателя мигр. 149; гео-фильтр числителя ≤300м мигр. 150). '
  'flat_count_effective = '
  'COALESCE(zhkh_flat_count, gar_flat_count, NULLIF(total_units,0), NULLIF(flat_count,0)) — '
  'ЖКХ ПРИОРИТЕТ (ГИС ЖКХ точнее ГАР, который дико недосчитывает квартиры в МКД; мигр. 149). '
  'Колонки zhkh_flat_count (сырой ЖКХ-счёт) + flat_count_source (zhkh|gar|total_units|flat_count|'
  'NULL — какой источник реально дал знаменатель) добавлены для прозрачности. Оба числителя '
  'считают УНИКАЛЬНЫЕ КВАРТИРЫ по сигнатуре count(DISTINCT (rooms, round(area_m2), floor)), а не '
  'объявления: одна квартира на avito+cian+domclick = разные dedup_hash, но одна сигнатура. '
  'active_secondary = FILTER (is_active AND vtorichka); listings_45d = FILTER (vtorichka AND '
  'last_seen_at>=now()-45d). sale_share_pct = active_secondary/denom; sale_share_pct_45d = '
  'listings_45d/denom. Оба под плаузибилити-гейтом (denom>=GREATEST(total_floors, '
  'листинговая-медианная-этажность, 8) AND числитель<=denom; мигр. 145 + 153), иначе NULL. '
  'Нижняя граница знаменателя включает медианную этажность листингов дома '
  '(percentile_cont(0.5) total_floors под тем же гео+floors-фильтром; мигр. 153) — режет ложные '
  'высокие доли, когда ГАР дико недосчитал квартиры, ЖКХ-счёта нет, а house.total_floors NULL '
  '(прод-кейс Переходный пер. 6: gar=9 vs 19-24-эт. башня). Частично-NULL сигнатуры считаются '
  'раздельно (count(DISTINCT ROW) игнорит лишь полностью-NULL строку). Фильтр: geom NOT NULL AND '
  '(active_secondary>0 OR listings_45d>0) — churn-only дома (0 активных, но есть листинги за '
  '45д) тоже видны. active_secondary/listings_45d/медианы цены и срока теперь считают ТОЛЬКО '
  'листинги ≤300м от geom своего дома (JOIN houses hg + ST_DistanceSphere<=300 в каждой FILTER; '
  'анти-mis-bucketing раздутого числителя из-за бага house_id_fk; мигр. 150). Листинги/дома без '
  'geom — кепим. Знаменатель НЕ изменён. Поверх гео — floors-guard (мигр. 152): листинг входит в '
  'числитель/медианы, только если его total_floors неизвестен, либо этажность дома '
  'COALESCE(zhkh_floors, total_floors) неизвестна, либо |разница| <= 3 этажа — режет height-'
  'mismatched mis-bucketing (5-эт. дом vs 19-эт. башни <300м), который гео-фильтр пропускает.';

COMMIT;
