-- 124_domrf_kn_free_flats.sql
-- #950 (Site Finder v2 / 3-layer склад, EPIC 6) — PR A: persist free_flats.
--
-- Добавляет domrf_kn_objects.free_flats = freeFlatsInfo.numberFlats из kn-API dom.рф
-- (кол-во квартир ЖК В ТЕКУЩЕЙ ПРОДАЖЕ). Это единственный детерминированный сигнал,
-- разблокирующий object-level Layer2: hidden = flat_count − free_flats
-- (flat_count = всего квартир по проектной декларации; free_flats = выставлено сейчас).
--
-- До #950 payload-блок freeFlatsInfo парсился ТОЛЬКО ради priceMin → price_min_rub
-- (миграция 113); numberFlats отбрасывался. Поле free_flats — sibling price_min_rub:
-- bigint→int, та же null-safety, тот же EXCLUDED-overwrite в UPSERT_OBJECT_SQL.
--
-- Backfill: сырой list объектов kn-API сохраняется как JSONB в domrf_raw_endpoints
-- (section='kn_api', endpoint='kn_object_place_<place>'; см. domrf_kn._insert_raw),
-- каждый элемент массива несёт objId + freeFlatsInfo.numberFlats. Поэтому historical
-- строки можно заполнить БЕЗ повторного скрейпа — join raw-элементов к
-- domrf_kn_objects по (obj_id, snapshot_date). Детерминированно, без LLM.
--
-- Deploy: auto-applied by deploy.yml через _schema_migrations tracking (ровно один раз).
--   Idempotent: ADD COLUMN IF NOT EXISTS + backfill только WHERE free_flats IS NULL.
-- Migration order: эта SQL-миграция применяется ПЕРВОЙ, backend domrf_kn.py — следом.
-- free_flats НЕnullable НЕ делаем: historical строки до бекфилла/без raw остаются NULL,
--   L2-сервис в этом случае падает на fallback (flat_count − sold).

BEGIN;

-- ── 1. Колонка ─────────────────────────────────────────────────────────────────
ALTER TABLE domrf_kn_objects
    ADD COLUMN IF NOT EXISTS free_flats integer;

COMMENT ON COLUMN domrf_kn_objects.free_flats IS
    'Кол-во квартир в текущей продаже (freeFlatsInfo.numberFlats из kn-API dom.рф). '
    'NULL = не заполнено (старый снапшот до #950). Для Layer2 hidden = flat_count − free_flats.';

-- ── 2. Backfill из сохранённого raw-payload ────────────────────────────────────
-- domrf_raw_endpoints.payload — JSONB-массив сырых object-строк kn-API. Разворачиваем
-- его, тянем objId + freeFlatsInfo.numberFlats и матчим к уже записанным строкам по
-- (obj_id, snapshot_date). Только WHERE free_flats IS NULL — повторный прогон no-op.
-- Guard: payload должен быть массивом; objId и numberFlats — числовые строки (регексп
-- отсекает null/мусор ДО CAST, иначе invalid input syntax for type integer). Digit-cap
-- {1,9}/{1,18} держит numberFlats в int4, objId в bigint — отсекает overflow ДО CAST
-- (числа-кандидаты семантически ограничены квартирами одного ЖК, но guard явный).
-- DISTINCT ON (obj_id, snapshot_date): один objId не дублируется в одном снапшоте, но
-- на случай нескольких place-endpoint'ов на дату берём первый детерминированно.
WITH raw_flats AS (
    SELECT DISTINCT ON (obj_id, r.snapshot_date)
        CAST(elem->>'objId' AS bigint)                            AS obj_id,
        r.snapshot_date                                           AS snapshot_date,
        CAST(elem->'freeFlatsInfo'->>'numberFlats' AS integer)    AS free_flats
    FROM domrf_raw_endpoints AS r
    CROSS JOIN LATERAL jsonb_array_elements(r.payload) AS elem
    WHERE r.section = 'kn_api'
      AND r.endpoint LIKE 'kn_object_place_%'
      AND jsonb_typeof(r.payload) = 'array'
      AND elem->>'objId' ~ '^[0-9]{1,18}$'
      AND elem->'freeFlatsInfo'->>'numberFlats' ~ '^[0-9]{1,9}$'
    ORDER BY obj_id, r.snapshot_date
)
UPDATE domrf_kn_objects AS o
   SET free_flats = rf.free_flats
  FROM raw_flats AS rf
 WHERE o.obj_id = rf.obj_id
   AND o.snapshot_date = rf.snapshot_date
   AND o.free_flats IS NULL;

COMMIT;
