-- 079_listing_source_history.sql
-- #570 — per-source listing price history.
--
-- ПРОБЛЕМА: listing_sources хранит ТОЛЬКО последний снимок на (canonical listing × source)
--   (price_rub / last_seen_at / raw_payload перезаписываются при каждом скрейпе) — истории нет.
--   Скраперы сейчас DORMANT (avito/yandex sweep enabled=false), так что ждать накопления
--   истории «органически» нельзя. Решение: ежедневный internal snapshot существующих 18 355
--   строк listing_sources (4 источника) — история начинает копиться СРАЗУ, независимо от
--   скраперов. Снимок пишет snapshot_listing_sources() (app/tasks/listing_source_snapshot.py),
--   запускаемый in-app scheduler'ом (source='listing_source_snapshot', seed ниже, enabled=true).
--
-- СОЗДАЁТ:
--   listing_source_snapshots — daily snapshot на источник (PK (listing_source_id, snapshot_date)).
--   listing_source_events    — change-log (price_change/delisted/relisted/edited/first_seen).
--   offer_price_history.listing_source_id — backlink на конкретный источник (был только source text).
--   v_listing_source_price_on_date — view «цена источника на дату» (snapshots ⋈ listing_sources).
--
-- ПАРТИЦИОНИРОВАНИЕ — НАМЕРЕННО НЕТ (отложено).
--   ADR предлагал партиционировать listing_source_snapshots по snapshot_date (RANGE),
--   но на текущем объёме это преждевременно: ~18 355 строк/день × ~30 ≈ 540k строк/мес —
--   plain-таблица с индексом (snapshot_date DESC) обслуживает это без проблем. Партиционирование
--   вводим позже отдельной миграцией, когда retention/объём этого реально потребуют.
--
-- ЗАВИСИМОСТИ: listing_sources (existing), scrape_runs (015_scrape_runs.sql),
--   offer_price_history (existing), scrape_schedules (052_scrape_schedules.sql, UNIQUE(source)).
-- Idempotent: CREATE TABLE/INDEX IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, CREATE OR REPLACE VIEW,
--   ON CONFLICT (source) DO NOTHING — безопасно запускать повторно.
-- Apply after: 078_scrape_schedules_seed_yandex_sweep.sql

BEGIN;

-- ── listing_source_snapshots — ежедневный снимок на источник ──────────────────
-- is_active — derived: last_seen_at в пределах окна свежести (7 дней) на момент снимка.
-- payload_hash — md5(raw_payload::text), дешёвый детектор «изменился ли payload» между днями.
-- run_id — провенанс: какой scrape_runs создал снимок (ON DELETE SET NULL — run могут чистить).
CREATE TABLE IF NOT EXISTS listing_source_snapshots (
    listing_source_id bigint      NOT NULL REFERENCES listing_sources(id) ON DELETE CASCADE,
    snapshot_date     date        NOT NULL DEFAULT CURRENT_DATE,
    price_rub         bigint,
    is_active         boolean     NOT NULL,
    last_seen_at      timestamptz,
    payload_hash      text,
    observed_at       timestamptz NOT NULL DEFAULT now(),
    run_id            bigint      REFERENCES scrape_runs(id) ON DELETE SET NULL,
    PRIMARY KEY (listing_source_id, snapshot_date)
);

COMMENT ON TABLE listing_source_snapshots IS
    'Ежедневный снимок listing_sources на источник (#570). Plain-таблица (не партиционирована — '
    'см. шапку 079). PK (listing_source_id, snapshot_date), last-write-wins за день.';

CREATE INDEX IF NOT EXISTS idx_lss_snapshot_date
    ON listing_source_snapshots (snapshot_date DESC);
CREATE INDEX IF NOT EXISTS idx_lss_source_date
    ON listing_source_snapshots (listing_source_id, snapshot_date DESC);

-- ── listing_source_events — change-log по источнику ───────────────────────────
-- diff_percent — для price_change: (new-old)/old*100. payload_diff — опциональный JSON-дифф.
-- UNIQUE (listing_source_id, change_time, event_type) — идемпотентность писателя (ON CONFLICT).
CREATE TABLE IF NOT EXISTS listing_source_events (
    id                bigserial   PRIMARY KEY,
    listing_source_id bigint      NOT NULL REFERENCES listing_sources(id) ON DELETE CASCADE,
    change_time       timestamptz NOT NULL,
    event_type        text        NOT NULL CHECK (
        event_type IN ('price_change', 'delisted', 'relisted', 'edited', 'first_seen')
    ),
    price_rub         bigint,
    diff_percent      numeric,
    payload_diff      jsonb,
    recorded_at       timestamptz NOT NULL DEFAULT now(),
    UNIQUE (listing_source_id, change_time, event_type)
);

COMMENT ON TABLE listing_source_events IS
    'Change-log на источник (#570): price_change/delisted/relisted/edited/first_seen. '
    'Писатель — snapshot_listing_sources() (set-based, ON CONFLICT DO NOTHING).';

CREATE INDEX IF NOT EXISTS idx_lse_source_change_time
    ON listing_source_events (listing_source_id, change_time DESC);
CREATE INDEX IF NOT EXISTS idx_lse_type_change_time
    ON listing_source_events (event_type, change_time DESC);

-- ── offer_price_history — backlink на конкретный источник ─────────────────────
-- Раньше была только колонка source (text) — без ссылки на listing_sources.id.
-- ON DELETE SET NULL: история переживает удаление источника (canonical price-history сохраняется).
ALTER TABLE offer_price_history
    ADD COLUMN IF NOT EXISTS listing_source_id bigint
        REFERENCES listing_sources(id) ON DELETE SET NULL;

CREATE INDEX IF NOT EXISTS idx_oph_source_change_time
    ON offer_price_history (listing_source_id, change_time DESC);

-- ── v_listing_source_price_on_date — «цена источника на дату» ─────────────────
-- Удобный join snapshots ⋈ listing_sources: (listing_id, ext_source, дата, цена, активность).
CREATE OR REPLACE VIEW v_listing_source_price_on_date AS
SELECT
    ls.listing_id,
    ls.ext_source,
    lss.snapshot_date,
    lss.price_rub,
    lss.is_active
FROM listing_source_snapshots lss
JOIN listing_sources ls ON ls.id = lss.listing_source_id;

COMMENT ON VIEW v_listing_source_price_on_date IS
    'Цена/активность источника на дату снимка (#570): listing_source_snapshots ⋈ listing_sources.';

-- ── Seed scrape_schedules row для daily snapshot ──────────────────────────────
-- enabled = true — БЕЗОПАСНО включать сразу: pure-internal DB snapshot, никаких внешних
--   HTTP-вызовов / анти-бота (в отличие от avito/yandex sweep, которые seed'ятся DORMANT).
-- Окно 01:00-02:00 UTC — РАНЬШЕ остальных job'ов (cian/rosreestr 02:00+), чтобы снять
--   снимок до того, как backfill-задачи начнут менять listing_sources в тот же день.
-- next_run_at = завтрашнее наступление окна (tomorrow + 01:00 UTC), иначе при NULL
--   get_due_schedules() выстрелит сразу после деплоя. AT TIME ZONE 'UTC' фиксирует
--   wall-clock час в UTC независимо от session TimeZone GUC.
INSERT INTO scrape_schedules (
    source,
    enabled,
    window_start_hour,
    window_end_hour,
    next_run_at,
    default_params
)
VALUES
(
    'listing_source_snapshot',
    true,                                        -- SAFE: pure internal DB, no external calls
    1,
    2,
    ((CURRENT_DATE + INTERVAL '1 day') + make_interval(hours => 1)) AT TIME ZONE 'UTC',
    '{}'::jsonb
)
ON CONFLICT (source) DO NOTHING;

COMMENT ON TABLE scrape_schedules IS
    'In-app scheduler config (заменяет cron-script setup). '
    'Sources: avito_city_sweep, yandex_city_sweep (dormant, #561), '
    'cian_history_backfill, rosreestr_dkp_import, listing_source_snapshot (#570).';

COMMIT;
