-- Storage для данных API Объектива (api.objctv.ru).
--
-- Схема ВЫРОВНЕНА по реальной форме payload, наблюдаемой на пробных вызовах
-- (data/raw/objective_test/20260507_*/), а не по гипотезам.
--
-- Реальная форма данных:
--   1. /v2/Report/GetReport?ReportType=Сводные&ReportName=Корпуса
--      → {"result": [ {Месяц, Проект, Девелопер, Район, Класс, Корпус,
--                      "Количество комнат (Данные Объектива)",
--                      Лотов по ПД шт, Площадь по ПД м2,
--                      Сделки ДДУ/ДКП/всего × {count, count_priced, vol, vol_priced,
--                                              sum_priced_млн, avg_price_per_m2_тыс,
--                                              avg_area},
--                      Объем предложения шт/м2, Сумм. стоимость в продаже млн,
--                      Средняя цена м2 в продаже тыс
--                     }, ...] }
--      → long-формат: одна строка = (месяц × корпус × room_bucket) per-row.
--      → corp_sum.json для одного ЖК Парк Культуры за 7 дней = 8 строк (4 bucket × 2 мес).
--
--   2. /v2/Report/GetReport?ReportType=Поквартирные&ReportName=Лоты
--      → {"result": [ {Id, "Id проекта", Проект, Корпус, Адрес, Класс, Секция, Этаж,
--                      "Номер лота", "Вид помещения", Статус, Продано,
--                      "Количество комнат(Сайт девелопера/ПД/Данные объектива)",
--                      "Площадь, м2(Сайт девелопера/ПД)",
--                      "Расчетный бюджет лота, Р", "Цена за м2, Р",
--                      "Цена предложения, Р", "Дельта цена, Р/%",
--                      "Дата договора", "Дата регистрации", "Тип сделки",
--                      "Тип покупателя", "Номер регистрации",
--                      "Тип обременения", Банк, "Дата начала обременения",
--                      "Дата актуальности данных из ЕГРН"}, ...] }
--      → per-flat snapshot. `Id` (Objective lot_id) — stable, можно использовать как UPSERT-ключ.
--
--   3. /v2/Report/GetReport?ReportType=Сводные&ReportName=Лоты — HTTP 500 на нашем тарифе
--   4. /v2/Report/GetReport?ReportType=Поквартирные&ReportName=Корпуса — HTTP 500 на нашем тарифе
--      Эти 2 отчёта недоступны / не реализованы у нас. Если откроют — добавим.
--
-- Стратегия storage:
--   - RAW: objective_raw_reports (jsonb payload) — страховка от ломки схемы.
--   - NORMALIZED:
--       - objective_corpus_room_month (long-формат из corp_sum)
--       - objective_lots (текущая запись per-flat, UPSERT по Objective.Id)
--       - objective_lots_history (append-only снапшоты per-flat per snapshot_date)
--   - MAPPING: objective_complex_mapping связывает Objective.ComplexName с domrf_kn_objects.obj_id.
--
-- Идемпотентно через IF NOT EXISTS.

CREATE EXTENSION IF NOT EXISTS postgis;

-- ── 0. Runs (журнал sync) ────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS objective_scrape_runs (
    run_id           BIGSERIAL PRIMARY KEY,
    started_at       timestamptz NOT NULL DEFAULT NOW(),
    finished_at      timestamptz,
    heartbeat_at     timestamptz,
    group_name       TEXT NOT NULL,
    reports_ok       INT NOT NULL DEFAULT 0,
    reports_failed   INT NOT NULL DEFAULT 0,
    rows_corpus_room INT NOT NULL DEFAULT 0,
    rows_lots        INT NOT NULL DEFAULT 0,
    rows_history     INT NOT NULL DEFAULT 0,
    requests_count   INT NOT NULL DEFAULT 0,
    status           TEXT NOT NULL DEFAULT 'running',  -- running | done | failed | zombie
    error            TEXT,
    triggered_by     TEXT NOT NULL DEFAULT 'beat'      -- beat | manual | resume
);

CREATE INDEX IF NOT EXISTS objective_runs_status_idx
    ON objective_scrape_runs(status, started_at DESC);

COMMENT ON TABLE objective_scrape_runs IS
    'Журнал запусков sync с api.objctv.ru. Один прогон = один (group_name) пакет отчётов.';

-- ── 1. RAW payloads ──────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS objective_raw_reports (
    raw_id           BIGSERIAL PRIMARY KEY,
    run_id           BIGINT REFERENCES objective_scrape_runs(run_id) ON DELETE SET NULL,
    fetched_at       timestamptz NOT NULL DEFAULT NOW(),
    page             TEXT NOT NULL,
    report_section   TEXT NOT NULL,
    report_type      TEXT NOT NULL,
    report_name      TEXT NOT NULL,
    group_name       TEXT NOT NULL,
    complex_name     TEXT,
    start_date       DATE,
    end_date         DATE,
    use_ddu          BOOLEAN,
    use_dkp          BOOLEAN,
    api_version      TEXT NOT NULL DEFAULT 'v2',
    payload          jsonb NOT NULL,
    payload_size     INT NOT NULL,
    rows_extracted   INT  -- сколько rows распарсено в normalized слой
);

CREATE INDEX IF NOT EXISTS objective_raw_combo_idx
    ON objective_raw_reports(report_section, report_type, report_name,
                             group_name, fetched_at DESC);
CREATE INDEX IF NOT EXISTS objective_raw_dates_idx
    ON objective_raw_reports(start_date, end_date) WHERE start_date IS NOT NULL;
CREATE INDEX IF NOT EXISTS objective_raw_run_idx
    ON objective_raw_reports(run_id);

COMMENT ON TABLE objective_raw_reports IS
    'Сырой JSON-payload каждого GetReport-вызова. Источник для re-parse при '
    'изменениях схемы Objective или переноса normalized-слоя на новую версию.';

-- ── 2. NORMALIZED — Сводные/Корпуса (long: month × corpus × room_bucket) ────

-- Заменяет старую objective_oks (была спроектирована под per-corpus с
-- rooms[N]_count колонками, не подходит под реальный long-формат).
CREATE TABLE IF NOT EXISTS objective_corpus_room_month (
    id                  BIGSERIAL PRIMARY KEY,
    -- Ключ записи (month × corpus × room_bucket)
    report_month        DATE NOT NULL,           -- 1-е число месяца из "Месяц": "апрель-2026"
    group_name          TEXT NOT NULL,
    project_name        TEXT NOT NULL,           -- "Парк Культуры"
    developer           TEXT,                    -- "PRINZIP"
    district            TEXT,                    -- "Автовокзал"
    class               TEXT,                    -- "Комфорт" / "Бизнес" / "Стандарт" / ...
    corpus_name         TEXT NOT NULL,           -- "1.1"
    room_bucket         TEXT NOT NULL,           -- "студия" | "1" | "2" | "3" | "4" | "5+"
    rooms_int           SMALLINT,                -- нормализованный 0=студия, 1, 2, 3, 4, 5
    -- Динамика стройки (повторяется в каждой строке, but stable per-corpus per snapshot)
    sales_start_date    DATE,
    plan_completion_date DATE,                   -- "Планируемая дата ввода"
    actual_completion_date DATE,                 -- "Фактическая дата ввода"
    months_in_realization INT,
    -- Capacity по ПД
    lots_pd_count       INT,                     -- "Лотов по ПД, шт."
    lots_pd_area        NUMERIC,                 -- "Площадь по ПД, м2."
    -- Сделки ДДУ
    deals_ddu_count        INT,
    deals_ddu_count_priced INT,                  -- из них с известной ценой
    deals_ddu_vol_m2       NUMERIC,
    deals_ddu_vol_m2_priced NUMERIC,
    deals_ddu_sum_mln_rub  NUMERIC,              -- "млн.Р"
    deals_ddu_avg_price_thousand_rub_per_m2 NUMERIC,  -- "тыс.Р/м2"
    deals_ddu_avg_area_m2  NUMERIC,
    -- Сделки ДКП
    deals_dkp_count        INT,
    deals_dkp_count_priced INT,
    deals_dkp_vol_m2       NUMERIC,
    deals_dkp_vol_m2_priced NUMERIC,
    deals_dkp_sum_mln_rub  NUMERIC,
    deals_dkp_avg_price_thousand_rub_per_m2 NUMERIC,
    deals_dkp_avg_area_m2  NUMERIC,
    -- Сделки всего (= ДДУ + ДКП)
    deals_total_count        INT,
    deals_total_count_priced INT,
    deals_total_vol_m2       NUMERIC,
    deals_total_vol_m2_priced NUMERIC,
    deals_total_sum_mln_rub  NUMERIC,
    deals_total_avg_price_thousand_rub_per_m2 NUMERIC,
    deals_total_avg_area_m2  NUMERIC,
    -- Текущее предложение (stock на конец месяца)
    offer_count             INT,                 -- "Объем предложения, шт."
    offer_area_m2           NUMERIC,             -- "Объем предложения, м2."
    offer_sum_mln_rub       NUMERIC,             -- "Суммарная стоимость лотов в продаже, млн.Р"
    offer_avg_price_thousand_rub_per_m2 NUMERIC, -- "Средняя цена м2 лота в продаже, тыс.Р/м2"
    -- Audit
    raw_id              BIGINT REFERENCES objective_raw_reports(raw_id) ON DELETE SET NULL,
    raw_props           jsonb,
    fetched_at          timestamptz NOT NULL DEFAULT NOW(),

    -- Один (месяц, корпус, room_bucket, group) — одна запись.
    UNIQUE (report_month, group_name, project_name, corpus_name, room_bucket)
);

CREATE INDEX IF NOT EXISTS objective_crm_project_month_idx
    ON objective_corpus_room_month(project_name, report_month DESC);
CREATE INDEX IF NOT EXISTS objective_crm_district_class_idx
    ON objective_corpus_room_month(district, class, report_month DESC);
CREATE INDEX IF NOT EXISTS objective_crm_developer_idx
    ON objective_corpus_room_month(developer, report_month DESC);
CREATE INDEX IF NOT EXISTS objective_crm_rooms_idx
    ON objective_corpus_room_month(rooms_int, report_month DESC);

COMMENT ON TABLE objective_corpus_room_month IS
    'Сводные/Корпуса в реальном long-формате: (месяц × корпус × количество комнат). '
    'Главное преимущество над domrf_kn — готовая monthly history с разбивкой по mix '
    '(studio/1/2/3/4) с явными метриками: остатки, средняя цена, средняя площадь.';

-- ── 3. NORMALIZED — Поквартирные/Лоты ────────────────────────────────────────

CREATE TABLE IF NOT EXISTS objective_lots (
    id                  BIGSERIAL PRIMARY KEY,
    objective_lot_id    BIGINT NOT NULL UNIQUE,  -- "Id" в payload — stable Objective ID
    objective_project_id BIGINT,                 -- "Id проекта"
    -- Identification
    project_name        TEXT NOT NULL,
    developer           TEXT,
    city                TEXT,
    district            TEXT,
    corpus_name         TEXT,
    address             TEXT,
    class               TEXT,                    -- "комфорт" в lots_pf (lowercase!) vs "Комфорт" в corp_sum
    section             TEXT,
    floor               INT,
    lot_number          TEXT,                    -- "Номер лота" — может быть "1а" etc, поэтому TEXT
    premise_kind        TEXT,                    -- "квартира" | "нежилое" | "паркинг" | "кладовая"
    -- Stage
    sales_start_date    DATE,
    plan_completion_date DATE,
    actual_completion_date DATE,
    readiness_pct       SMALLINT,                -- "17%" → 17
    construction_stage  TEXT,                    -- "строящийся" | "сдан" | ...
    finishing           TEXT,                    -- "Отделка по корпусу" — "под чистовую" | "с отделкой" | ...
    -- Status
    status              TEXT,                    -- "резерв" | "свободна" | "продан" | "бронь"
    is_sold             BOOLEAN,                 -- "Продано": "да"/"нет" → true/false
    -- Mix (3 источника от Объектива)
    rooms_dev_site      TEXT,                    -- "Количество комнат(Сайт девелопера)"
    rooms_pd            TEXT,                    -- "Количество комнат(ПД)"
    rooms_objective     TEXT,                    -- "Количество комнат(Данные объектива)" — самое нормализованное
    rooms_int           SMALLINT,                -- нормализуем objective: студия→0, "1"→1, ...
    -- Areas
    area_dev_site       NUMERIC,
    area_pd             NUMERIC,
    -- Pricing
    price_calculated_total_rub NUMERIC,          -- "Расчетный бюджет лота, Р"
    price_per_m2_rub    NUMERIC,                 -- "Цена за м2, Р"
    price_offer_total_rub NUMERIC,               -- "Цена предложения, Р"
    price_delta_rub     NUMERIC,                 -- "Дельта цена, Р"
    price_delta_pct     NUMERIC,                 -- "Дельта цена, %"
    pricing_method      TEXT,                    -- "Способ определения цены"
    price_set_date      DATE,                    -- "Дата установки цены"
    price_actual_date   DATE,                    -- "Дата актуальности цены"
    -- Deal
    contract_date       DATE,                    -- "Дата договора"
    registration_date   DATE,                    -- "Дата регистрации"
    contract_type       TEXT,                    -- "Тип сделки": ДДУ | ДКП | ...
    buyer_type          TEXT,                    -- "Тип покупателя"
    registration_number TEXT,                    -- "Номер регистрации"
    -- Encumbrance / mortgage
    encumbrance_type    TEXT,                    -- "Тип обременения"
    bank_name           TEXT,                    -- "Банк"
    encumbrance_start_date DATE,                 -- "Дата начала обременения"
    egrn_actual_date    DATE,                    -- "Дата актуальности данных из ЕГРН"
    -- Audit
    raw_id              BIGINT REFERENCES objective_raw_reports(raw_id) ON DELETE SET NULL,
    raw_props           jsonb,
    snapshot_date       DATE NOT NULL,           -- дата снимка (= когда мы синкнули)
    fetched_at          timestamptz NOT NULL DEFAULT NOW(),
    updated_at          timestamptz NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS objective_lots_project_idx
    ON objective_lots(project_name, snapshot_date DESC);
CREATE INDEX IF NOT EXISTS objective_lots_status_idx
    ON objective_lots(status) WHERE status IS NOT NULL;
CREATE INDEX IF NOT EXISTS objective_lots_rooms_area_idx
    ON objective_lots(rooms_int, area_pd) WHERE area_pd IS NOT NULL;
CREATE INDEX IF NOT EXISTS objective_lots_district_class_idx
    ON objective_lots(district, class) WHERE district IS NOT NULL;
CREATE INDEX IF NOT EXISTS objective_lots_bank_idx
    ON objective_lots(bank_name) WHERE bank_name IS NOT NULL;
CREATE INDEX IF NOT EXISTS objective_lots_reg_date_idx
    ON objective_lots(registration_date) WHERE registration_date IS NOT NULL;

COMMENT ON TABLE objective_lots IS
    'Per-flat snapshot из Объектива (Поквартирные/Лоты). UPSERT по objective_lot_id. '
    'Текущее (самое последнее) состояние каждой квартиры. Полная history — '
    'в objective_lots_history.';

-- ── 4. NORMALIZED — История квартир (append-only snapshots) ─────────────────

-- Объектив НЕ отдаёт per-day price/status timeline в одном вызове.
-- Зато при еженедельном sync мы можем собирать сами: при каждом вызове
-- делаем INSERT в _history для каждого изменившегося лота. Это даст нам
-- weekly-resolution timeline price/status/area per flat — достаточно для
-- elasticity и velocity-anomaly детектора.

CREATE TABLE IF NOT EXISTS objective_lots_history (
    id                  BIGSERIAL PRIMARY KEY,
    objective_lot_id    BIGINT NOT NULL,         -- ссылка на objective_lots.objective_lot_id
    snapshot_date       DATE NOT NULL,           -- день sync
    status              TEXT,
    is_sold             BOOLEAN,
    price_calculated_total_rub NUMERIC,
    price_per_m2_rub    NUMERIC,
    price_offer_total_rub NUMERIC,
    price_delta_pct     NUMERIC,
    area_pd             NUMERIC,
    contract_date       DATE,
    registration_date   DATE,
    contract_type       TEXT,
    bank_name           TEXT,
    raw_id              BIGINT REFERENCES objective_raw_reports(raw_id) ON DELETE SET NULL,
    fetched_at          timestamptz NOT NULL DEFAULT NOW(),
    -- На каждый (lot, snapshot_date) — ровно одна запись.
    UNIQUE (objective_lot_id, snapshot_date)
);

CREATE INDEX IF NOT EXISTS objective_lots_history_lot_idx
    ON objective_lots_history(objective_lot_id, snapshot_date DESC);
CREATE INDEX IF NOT EXISTS objective_lots_history_date_idx
    ON objective_lots_history(snapshot_date);
CREATE INDEX IF NOT EXISTS objective_lots_history_reg_idx
    ON objective_lots_history(registration_date)
    WHERE registration_date IS NOT NULL;

COMMENT ON TABLE objective_lots_history IS
    'Append-only weekly snapshots каждого лота. Главное преимущество для '
    'velocity-anomaly detection и для elasticity (точность регрессии × лет).';

-- ── 5. MAPPING — Objective ↔ domrf_kn_objects ────────────────────────────────

CREATE TABLE IF NOT EXISTS objective_complex_mapping (
    id                  BIGSERIAL PRIMARY KEY,
    objective_complex_name TEXT NOT NULL,        -- "Парк Культуры"
    objective_project_id BIGINT,                 -- из payload "Id проекта"
    objective_group     TEXT NOT NULL,
    domrf_obj_id        BIGINT,                  -- domrf_kn_objects.obj_id; NULL = не сматчили
    is_reviewed         BOOLEAN NOT NULL DEFAULT FALSE,
    match_method        TEXT,                    -- 'exact_name' | 'fuzzy_name' | 'manual' | 'spatial'
    match_score         NUMERIC,                 -- 0..1 для fuzzy
    note                TEXT,
    created_at          timestamptz NOT NULL DEFAULT NOW(),
    updated_at          timestamptz NOT NULL DEFAULT NOW(),
    UNIQUE (objective_complex_name, objective_group)
);

CREATE INDEX IF NOT EXISTS objective_mapping_domrf_idx
    ON objective_complex_mapping(domrf_obj_id) WHERE domrf_obj_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS objective_mapping_unreviewed_idx
    ON objective_complex_mapping(is_reviewed) WHERE is_reviewed = FALSE;
CREATE INDEX IF NOT EXISTS objective_mapping_project_id_idx
    ON objective_complex_mapping(objective_project_id) WHERE objective_project_id IS NOT NULL;

COMMENT ON TABLE objective_complex_mapping IS
    'Маппинг Objective.ComplexName ↔ domrf_kn_objects.obj_id. is_reviewed=TRUE — '
    'человек подтвердил соответствие. Auto-matching пишет с is_reviewed=FALSE.';
