-- 067_v_street_sales_vs_listings.sql
-- Purpose: PR K (Foundation Phase 1 of issue #564) — pairs ДКП-сделок (deals,
--          source='rosreestr') с listings (avito/cian/yandex/n1/domklik) для
--          comparison sales-vs-asks per-street.
--
-- Open dataset Росреестра агрегирует адреса до уровня улицы (без номера дома).
-- Поэтому matching работает per-street + area/rooms — а не per-house.
--
-- JOIN logic (encapsulated в view):
--   1. address ILIKE '%street_pattern%' — фильтрация per-street происходит в
--      endpoint (extract_street_name → pattern → передаётся обоим JOIN-sides).
--   2. Area: deals.area_m2 BETWEEN listings.area_m2 * (1 - tolerance)
--                                AND listings.area_m2 * (1 + tolerance).
--   3. Listing date: listings.listing_date BETWEEN
--      (deals.deal_date - window_days) AND deals.deal_date.
--      Допускаем listing немного после сделки (отложенный закрепляющий парсинг).
--   4. Rooms: exact match.
--
-- Result row: (deal_id, deal_date, deal_price, deal_area_m2, listing_id,
--              listing_source, listing_url, listing_date, listing_price,
--              listing_price_per_m2, days_listing_to_deal, discount_pct).
--
-- discount_pct = (deal_price - listing_price) / listing_price * 100
--   Положительный = сделка дороже asking → 'переплата' (раритет, чаще ошибка).
--   Отрицательный = торг (продали дешевле выставленного).
--
-- Реализация — table-valued function street_sales_vs_listings(...). View поверх
-- всех data слишком тяжёлый: deals × listings cross-product взорвёт RAM.
-- Function позволяет filter early по улице, area, rooms, window_days и
-- использовать существующие индексы (deals_rooms_area_idx, listings_rooms_area_idx).
--
-- Dependencies:
--   - deals (002_core_tables.sql) — source='rosreestr' filtered to ДКП в PR #549.
--   - listings (002_core_tables.sql) — все 4 источника объявлений.
--
-- Deploy order: после 066. Idempotent (CREATE OR REPLACE FUNCTION).

BEGIN;

CREATE OR REPLACE FUNCTION street_sales_vs_listings(
    p_street_pattern  text,
    p_area_m2         numeric,
    p_rooms           integer,
    p_window_days     integer DEFAULT 180,
    p_area_tolerance  numeric DEFAULT 0.15,
    p_period_months   integer DEFAULT 24
)
RETURNS TABLE (
    deal_id                bigint,
    deal_date              date,
    deal_price_rub         bigint,
    deal_price_per_m2      integer,
    deal_area_m2           numeric,
    deal_rooms             integer,
    deal_floor             integer,
    deal_address           text,
    listing_id             bigint,
    listing_source         text,
    listing_source_url     text,
    listing_date           date,
    listing_price_rub      bigint,
    listing_price_per_m2   integer,
    listing_area_m2        numeric,
    days_listing_to_deal   integer,
    discount_pct           numeric
)
LANGUAGE sql
STABLE
AS $$
    WITH window_deals AS (
        -- Сделки в улице + период. Фильтр по rooms + area.
        SELECT
            d.id           AS deal_id,
            d.deal_date    AS deal_date,
            d.price_rub    AS deal_price_rub,
            d.price_per_m2 AS deal_price_per_m2,
            d.area_m2      AS deal_area_m2,
            d.rooms        AS deal_rooms,
            d.floor        AS deal_floor,
            d.address      AS deal_address
        FROM deals d
        WHERE d.source = 'rosreestr'
          AND d.address ILIKE p_street_pattern
          AND d.rooms = p_rooms
          AND d.area_m2 BETWEEN p_area_m2 * (1.0 - p_area_tolerance)
                            AND p_area_m2 * (1.0 + p_area_tolerance)
          AND d.deal_date > NOW() - (p_period_months || ' months')::interval
          AND d.price_rub > 0
    ),
    window_listings AS (
        -- Кандидаты-listings на той же улице, rooms exact, area ±tolerance.
        SELECT
            l.id                 AS listing_id,
            l.source             AS listing_source,
            l.source_url         AS listing_source_url,
            l.listing_date       AS listing_date,
            l.price_rub          AS listing_price_rub,
            l.price_per_m2       AS listing_price_per_m2,
            l.area_m2            AS listing_area_m2,
            l.rooms              AS listing_rooms,
            COALESCE(l.listing_date, l.scraped_at::date) AS listing_event_date
        FROM listings l
        WHERE l.address ILIKE p_street_pattern
          AND l.rooms = p_rooms
          AND l.area_m2 BETWEEN p_area_m2 * (1.0 - p_area_tolerance)
                            AND p_area_m2 * (1.0 + p_area_tolerance)
          AND l.price_rub > 0
          AND COALESCE(l.listing_date, l.scraped_at::date)
                > NOW() - ((p_period_months + 6) || ' months')::interval
    ),
    paired AS (
        -- LEFT JOIN: сохраняем все сделки даже если нет listing match.
        -- Для каждой сделки выбираем listing с listing_date ближайший
        -- к deal_date (предпочтительно перед сделкой).
        SELECT DISTINCT ON (wd.deal_id)
            wd.deal_id,
            wd.deal_date,
            wd.deal_price_rub,
            wd.deal_price_per_m2,
            wd.deal_area_m2,
            wd.deal_rooms,
            wd.deal_floor,
            wd.deal_address,
            wl.listing_id,
            wl.listing_source,
            wl.listing_source_url,
            wl.listing_date,
            wl.listing_price_rub,
            wl.listing_price_per_m2,
            wl.listing_area_m2,
            (wd.deal_date - wl.listing_event_date)::integer AS days_listing_to_deal,
            CASE
                WHEN wl.listing_price_rub IS NOT NULL AND wl.listing_price_rub > 0
                THEN ROUND(
                    (wd.deal_price_rub - wl.listing_price_rub)::numeric
                    / wl.listing_price_rub * 100,
                    2
                )
                ELSE NULL
            END AS discount_pct
        FROM window_deals wd
        LEFT JOIN window_listings wl
          ON wl.listing_event_date
                BETWEEN (wd.deal_date - (p_window_days || ' days')::interval)::date
                    AND (wd.deal_date + interval '30 days')::date
        ORDER BY
            wd.deal_id,
            -- prefer listing event дата перед сделкой и ближе к ней
            CASE WHEN wl.listing_event_date IS NULL THEN 1 ELSE 0 END,
            CASE WHEN wl.listing_event_date <= wd.deal_date THEN 0 ELSE 1 END,
            ABS((wd.deal_date - wl.listing_event_date))
    )
    SELECT
        deal_id,
        deal_date,
        deal_price_rub,
        deal_price_per_m2,
        deal_area_m2,
        deal_rooms,
        deal_floor,
        deal_address,
        listing_id,
        listing_source,
        listing_source_url,
        listing_date,
        listing_price_rub,
        listing_price_per_m2,
        listing_area_m2,
        days_listing_to_deal,
        discount_pct
    FROM paired
    ORDER BY deal_date DESC;
$$;

COMMENT ON FUNCTION street_sales_vs_listings(text, numeric, integer, integer, numeric, integer) IS
  'Pairs (ДКП-сделка, listing) для улицы. PR K / issue #564 Foundation Phase 1. '
  'Per-street matching: address ILIKE, area ±tolerance, rooms exact, window_days '
  'до даты сделки (+30д grace). Возвращает LEFT JOIN — сделки без listing match '
  'имеют listing_* = NULL. discount_pct = (deal - listing) / listing * 100.';

-- ─────────────────────────────────────────────────────────────────────
-- v_street_sales_vs_listings — convenience view (без параметров)
-- Используется для prod-аудита linkage_rate. НЕ для hot API path —
-- view сканирует все deals × listings, поэтому только для analytics.
-- ─────────────────────────────────────────────────────────────────────
CREATE OR REPLACE VIEW v_street_sales_vs_listings AS
SELECT
    d.id          AS deal_id,
    d.deal_date,
    d.price_rub   AS deal_price_rub,
    d.price_per_m2 AS deal_price_per_m2,
    d.area_m2     AS deal_area_m2,
    d.rooms       AS deal_rooms,
    d.address     AS deal_address,
    l.id          AS listing_id,
    l.source      AS listing_source,
    l.source_url  AS listing_source_url,
    l.listing_date,
    l.price_rub   AS listing_price_rub,
    l.price_per_m2 AS listing_price_per_m2,
    l.area_m2     AS listing_area_m2,
    (d.deal_date - COALESCE(l.listing_date, l.scraped_at::date))::integer
        AS days_listing_to_deal,
    CASE
        WHEN l.price_rub IS NOT NULL AND l.price_rub > 0
        THEN ROUND((d.price_rub - l.price_rub)::numeric / l.price_rub * 100, 2)
        ELSE NULL
    END AS discount_pct
FROM deals d
LEFT JOIN listings l
  ON l.rooms = d.rooms
 AND l.area_m2 BETWEEN d.area_m2 * 0.85 AND d.area_m2 * 1.15
 AND COALESCE(l.listing_date, l.scraped_at::date)
        BETWEEN (d.deal_date - interval '180 days')::date
            AND (d.deal_date + interval '30 days')::date
 AND l.price_rub > 0
WHERE d.source = 'rosreestr'
  AND d.price_rub > 0;

COMMENT ON VIEW v_street_sales_vs_listings IS
  'Analytical view над deals × listings для prod linkage_rate audit. PR K / #564. '
  'НЕ для hot API path — слишком тяжёлый. Используется только для ad-hoc query.';

COMMIT;
