-- 133_zone_regulation_cache.sql
-- #1067 (Site Finder v2 / ИРД-content «GG-форсайт», C8b) — кэш ПЗЗ-регламента по индексу зоны.
--
-- Регламент ПЗЗ идентичен для ВСЕХ участков одной территориальной зоны (Ц-1, Ж-2, ...), а зон
-- на город ~100. Поэтому резолвить ВРИ+предельные параметры через ЕКБ-геопортал urbanCard на
-- каждый участок — расточительно (reverse-eng API без SLA, см. ekb_geoportal_client.py). Кэшируем
-- ОДИН раз на (city, zone_index): 3 списка ВРИ (540н) + сырой текст предельных параметров +
-- числовой экстракт (regex) ключевых параметров застройки для capacity-оценки.
--
-- Что отдаёт urbanCard (live-verified 2026-06-06, vault research/EKB_Geoportal_PZZ_Content_API_Jun06):
--   pzzList[0]: urbanTerZone (index+имя) · main/condition/addition TypesPermittedUse (ВРИ) ·
--   limitTypesPermittedUse[].typePermittedUsePars[].otherDescription — предельные параметры
--   СВОБОДНЫМ ТЕКСТОМ (min площадь ЗУ / max % застройки / КСИ / подземная застройка / этажность),
--   часто «не подлежат установлению». Парсятся сервисом zone_regulation.py (regex) → params_numeric.
--
-- Зерно (grain): одна строка = один индекс зоны в городе. Дедуп-ключ — (city, zone_index).
--   re-fetch UPSERT-ит (latest wins): регламент меняется только при правке ПЗЗ (редко) →
--   fetched_at = freshness, refresh-воркер дёргает по возрасту.
--
-- Связь с ird_overlays (м.132): ird_overlays.zone_index (когда резолвлен) JOIN-ится сюда за
--   полным регламентом зоны участка. content в ird_overlays НЕ дублируется — single source тут.
--
-- Deploy: auto-applied deploy.yml через _schema_migrations (ровно один раз по NN). Idempotent:
--   CREATE TABLE/INDEX IF NOT EXISTS + guarded DO для UNIQUE (constraint нельзя добавить дважды,
--   прецедент м.40/110/125/132). НИКАКИХ строк не вставляется (данные — из сервиса C8b).

BEGIN;

-- ── 1. Таблица zone_regulation_cache ───────────────────────────────────────────
CREATE TABLE IF NOT EXISTS zone_regulation_cache (
    id                 BIGSERIAL PRIMARY KEY,
    city               TEXT NOT NULL DEFAULT 'ekb',  -- мульти-город forward-compat; пока только ЕКБ
    zone_index         TEXT NOT NULL,                -- индекс ПЗЗ-зоны ('Ц-1', 'Ж-2', 'ЦС-1')
    zone_full_name     TEXT,                         -- 'Ц-1 Общественно-деловая зона городского центра'
    -- ── ВРИ (списки строк «код 540н + текст», JSONB-массивы) ──
    main_vri           JSONB NOT NULL DEFAULT '[]'::jsonb,  -- основные ВРИ
    conditional_vri    JSONB NOT NULL DEFAULT '[]'::jsonb,  -- условно-разрешённые ВРИ
    auxiliary_vri      JSONB NOT NULL DEFAULT '[]'::jsonb,  -- вспомогательные ВРИ
    -- ── предельные параметры ──
    limit_params_raw   JSONB NOT NULL DEFAULT '[]'::jsonb,  -- сырые otherDescription-строки (forward-compat/LLM)
    params_numeric     JSONB NOT NULL DEFAULT '{}'::jsonb,  -- структурный экстракт: {param: [values...]} (regex, C8b)
    -- ── headline-скаляры для capacity (most-permissive из найденных; NULL = «не подлежат установлению»/нет) ──
    max_far            NUMERIC,   -- коэф. строит. использования ЗУ (КСИ), max найденный
    max_building_pct   NUMERIC,   -- макс. процент застройки, max найденный (%)
    underground_pct    NUMERIC,   -- процент застройки подземного пространства, max (%)
    min_parcel_area_m2 NUMERIC,   -- мин. площадь ЗУ, наименьший из найденных минимумов (кв. м)
    max_floors         INTEGER,   -- предельная этажность, max найденный
    max_height_m       NUMERIC,   -- предельная высота, max найденный (м)
    source             TEXT NOT NULL DEFAULT 'ekb_geoportal',  -- источник регламента
    fetched_at         timestamptz NOT NULL DEFAULT now(),     -- когда резолвили (freshness для refresh)
    -- city/zone_index — осмысленные только непустыми; ловим мусор громко на write.
    CONSTRAINT zone_regulation_cache_zone_index_chk CHECK (length(zone_index) > 0),
    CONSTRAINT zone_regulation_cache_city_chk CHECK (length(city) > 0)
);

COMMENT ON TABLE zone_regulation_cache IS
    'Кэш ПЗЗ-градрегламента по индексу зоны (#1067 C8b, GG-форсайт). 1 строка = 1 (city, '
    'zone_index). 3 списка ВРИ (540н) + сырой текст предельных параметров + числовой экстракт '
    '(params_numeric/headline-скаляры, regex). Резолв из ЕКБ-геопортала urbanCard (zone_regulation.py). '
    'UPSERT по uq_zone_regulation_cache (city, zone_index); latest wins. ird_overlays.zone_index JOIN-ится сюда.';
COMMENT ON COLUMN zone_regulation_cache.params_numeric IS
    'Числовой экстракт предельных параметров (regex из limit_params_raw): {max_far:[..], '
    'max_building_pct:[..], underground_pct:[..], min_parcel_area_m2:[..], max_floors:[..], '
    'max_height_m:[..]}. Списки т.к. параметр повторяется per-ВРИ. headline-скаляры — агрегат.';
COMMENT ON COLUMN zone_regulation_cache.max_far IS
    'Коэф. строит. использования ЗУ (КСИ) — max найденный (capacity best-case). NULL = не установлен';
COMMENT ON COLUMN zone_regulation_cache.min_parcel_area_m2 IS
    'Мин. площадь ЗУ — НАИМЕНЬШИЙ из найденных минимумов (самый разрешительный). NULL = не установлен';

-- ── 2. Дедуп/UPSERT-ключ (city, zone_index) ────────────────────────────────────
-- Цель ON CONFLICT для сервиса C8b: re-fetch обновляет регламент той же зоны, не плодя дубли.
-- Guard по pg_constraint → idempotent re-apply (прецедент м.125/132).
DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM pg_constraint WHERE conname = 'uq_zone_regulation_cache'
    ) THEN
        ALTER TABLE zone_regulation_cache
            ADD CONSTRAINT uq_zone_regulation_cache
            UNIQUE (city, zone_index);
    END IF;
END
$$;

-- ── 3. Индексы ─────────────────────────────────────────────────────────────────
-- Главный read-path: lookup регламента по индексу зоны (UNIQUE уже покрывает (city, zone_index),
-- но частый запрос — по zone_index без city). Очередь refresh — по возрасту (fetched_at).
CREATE INDEX IF NOT EXISTS idx_zone_regulation_cache_zone_index
    ON zone_regulation_cache (zone_index);
CREATE INDEX IF NOT EXISTS idx_zone_regulation_cache_fetched_at
    ON zone_regulation_cache (fetched_at);

COMMIT;
