-- 178_deal_city_price_bands.sql
-- Per-city ppm² plausible-deal guard-bands — снятие Екатеринбург-центричного
-- глобального DEAL_MIN_PPM2=50_000 (estimator.py) для остальной Свердловской обл.
--
-- WHY:
--   Миграция 177 открыла +47183 не-ЕКБ сделок (deals.city заполнен для всей
--   области). Но estimator.py всё ещё режет ДКП-сделки абсолютным guard-band
--   [DEAL_MIN_PPM2=50_000, DEAL_MAX_PPM2=800_000] ₽/м² — калиброванным
--   ИСКЛЮЧИТЕЛЬНО под ЕКБ-вторичку (см. комментарий у DEAL_MIN_PPM2 в
--   estimator.py). Для дешёвых городов области этот пол — не anti-outlier
--   guard, а cut-off легитимного рынка:
--     Североуральск median ≈ 21.7k ₽/м², Новоуральск ≈ 36k, Асбест ≈ 41k —
--     ВСЕ ниже 50k → 46.6% не-ЕКБ сделок молча дропаются как «доля/ошибка»,
--     хотя это нормальная цена для города.
--
-- WHAT:
--   Таблица deal_city_price_bands — per-city [ppm2_min, ppm2_max] band,
--   вычисленный из РЕАЛЬНЫХ rosreestr-сделок города (p1/p99 персентиль,
--   зажатый абсолютными hard-floor/ceiling 8000/800000 — доли и опечатки
--   режутся даже в самых дешёвых городах). Екатеринбург НАМЕРЕННО исключён
--   из популяции — estimator.py при отсутствии строки в этой таблице
--   fallback'ит на глобальные DEAL_MIN_PPM2/DEAL_MAX_PPM2 константы, т.е.
--   ЕКБ-поведение byte-identical сегодняшнему (см. invariant в PR).
--
--   Города с < 30 сделок исключены (HAVING count(*) >= 30) — слишком тонкая
--   выборка для устойчивого перцентиля, estimator fallback'ит на global band.
--
-- USAGE (estimator.py, тот же PR):
--   - SQL enforcement (_fetch_dkp_corridor, ~line 1263): LEFT JOIN на
--     deals.city = deal_city_price_bands.city, COALESCE(b.ppm2_min/max, global).
--   - Python enforcement (_is_plausible_deal via _fetch_deals): bands dict,
--     загружен один раз через _load_city_price_bands(db), .get(city, global).
--
-- IDEMPOTENCY / SAFETY:
--   CREATE TABLE IF NOT EXISTS — безопасный re-run. Populate — INSERT ...
--   ON CONFLICT (city) DO UPDATE, т.е. повторный прогон РЕФРЕШИТ бэнды под
--   свежие сделки (не no-op по дизайну — таблица предполагает периодический
--   refresh по мере роста deals; scheduler для авто-рефреша НЕ входит в этот
--   PR, см. follow-up в PR description).
--
-- Dependencies: 177_deals_city_region.sql (deals.city, backfill).

BEGIN;

CREATE TABLE IF NOT EXISTS deal_city_price_bands (
    city         text PRIMARY KEY,
    ppm2_min     int NOT NULL,
    ppm2_max     int NOT NULL,
    n_deals      int NOT NULL,
    refreshed_at timestamptz NOT NULL DEFAULT now()
);

COMMENT ON TABLE deal_city_price_bands IS
  'Per-city ppm² plausible-deal guard-bands (estimator.py _is_plausible_deal / '
  '_fetch_dkp_corridor). Екатеринбург НЕ включён — estimator fallback на глобальные '
  'DEAL_MIN_PPM2=50000/DEAL_MAX_PPM2=800000 сохраняет ЕКБ-поведение byte-identical. '
  'Refresh: повторный прогон этого файла (ON CONFLICT DO UPDATE); авто-scheduler '
  'пока не реализован (см. issue follow-up).';

INSERT INTO deal_city_price_bands (city, ppm2_min, ppm2_max, n_deals, refreshed_at)
SELECT
    city,
    GREATEST(round(percentile_cont(0.01) WITHIN GROUP (ORDER BY price_per_m2))::int, 8000)
        AS ppm2_min,
    LEAST(round(percentile_cont(0.99) WITHIN GROUP (ORDER BY price_per_m2))::int, 800000)
        AS ppm2_max,
    count(*) AS n_deals,
    now() AS refreshed_at
FROM deals
WHERE source = 'rosreestr'
  AND price_per_m2 IS NOT NULL
  AND city IS NOT NULL
  AND city <> 'Екатеринбург'
GROUP BY city
HAVING count(*) >= 30
   -- Инвариант ppm2_max >= ppm2_min: если весь распределение города < 8000 ₽/м²
   -- (p99 < 8000), LEAST(p99,800000) < GREATEST(p1,8000) → band [8000, <8000] не
   -- матчит ничего → все сделки города молча дропались бы. Гейтим такие города
   -- (для жилой ДКП недостижимо, но airtight против мусорного города).
   AND round(percentile_cont(0.99) WITHIN GROUP (ORDER BY price_per_m2))::int >= 8000
ON CONFLICT (city) DO UPDATE
   SET ppm2_min = EXCLUDED.ppm2_min,
       ppm2_max = EXCLUDED.ppm2_max,
       n_deals = EXCLUDED.n_deals,
       refreshed_at = EXCLUDED.refreshed_at;

COMMIT;
