-- 108_merge_duplicate_houses.sql
-- Цель: объединить дублирующиеся строки в таблице houses, возникшие из-за того, что
--   avito_houses.py до fix #108 использовал прямой INSERT ON CONFLICT(source, ext_house_id)
--   вместо canonического match_or_create_house. В результате один физический дом
--   мог иметь несколько строк — по одной на каждый source (avito-каталог vs listing-scraper).
--
-- Алгоритм:
--   1. Построить cluster_key по (cadastral_number / address / geo) — консервативная кластеризация.
--   2. Выбрать canonical дом в кластере: максимум не-NULL колонок, geom присутствует, min(id).
--   3. Перепривязать все зависимые таблицы с dup → canonical (обработка UNIQUE-коллизий).
--   4. Удалить дубли.
--   5. Вставить house_sources + address_aliases для backfill Tier1/2 на следующем скрейпе.
--   6. RAISE NOTICE с итоговыми счётчиками.
--
-- Зависимые таблицы (FK на houses.id):
--   listings               (house_id_fk)  — NO ACTION  — обязателен репойнт
--   house_placement_history (house_id)    — NO ACTION  — обязателен репойнт
--   house_reviews          (house_id)     — CASCADE    — репойнт перед DELETE чтобы обновить house_id
--   houses_price_dynamics  (house_id)     — CASCADE    — UNIQUE(house_id,month_date,source) → dedup
--   house_reliability_checks(house_id)    — CASCADE    — нет ограничивающего UNIQUE
--   house_sources          (house_id)     — CASCADE    — UNIQUE(ext_source,ext_id) → dedup
--   house_address_aliases  (house_id)     — CASCADE    — UNIQUE(normalized_address) → dedup
--   house_imv_evaluations  (house_id)     — CASCADE    — UNIQUE(house_id) → dedup
--   house_suggestions      (house_id)     — CASCADE    — UNIQUE(house_id, ext_item_id) → dedup
--   external_valuations    (house_id)     — CASCADE    — нет ограничивающего UNIQUE
--   address_mismatch_audit (house_id)     — CASCADE    — UNIQUE(house_id,audit_batch) → dedup
--
-- Идемпотентность: все UPDATE/DELETE ищут по mapping временной таблицы. При повторном
--   запуске mapping будет пустым (дублей нет) → все UPDATE/DELETE затронут 0 строк → no-op.
--
-- Стиль: следует 063_backfill_houses_and_link_listings.sql.
--
-- Деплой: после 107_scrape_schedules_seed_cian_city_sweep.sql

BEGIN;

-- -------------------------------------------------------------------------
-- Step 0: Счётчик до
-- -------------------------------------------------------------------------
DO $$
DECLARE
    v_before bigint;
BEGIN
    SELECT count(*) INTO v_before FROM houses;
    RAISE NOTICE 'merge 108 start: houses_before=%', v_before;
END $$;

-- -------------------------------------------------------------------------
-- Step 1: Построить mapping дублей → canonical
--
-- cluster_key (приоритет):
--   1. cadastral_number (наиболее точный идентификатор)
--   2. 'addr:' || lower(trim(address))
--   3. 'geo:'  || round(lat,4) || ',' || round(lon,4)
--   строки без всех трёх полей — cluster_key IS NULL → пропускаем (не трогаем)
--
-- Канонический дом: первый по (geom NOT NULL DESC, count NOT NULL fields DESC, id ASC).
-- Считаем «значимые» NOT NULL поля чтобы выбрать самый полный ряд.
-- -------------------------------------------------------------------------
CREATE TEMP TABLE IF NOT EXISTS _108_dup_mapping AS
WITH clustered AS (
    SELECT
        id,
        COALESCE(
            NULLIF(trim(cadastral_number), ''),
            CASE WHEN NULLIF(lower(trim(address)), '') IS NOT NULL
                 THEN 'addr:' || lower(trim(address)) END,
            CASE WHEN lat IS NOT NULL AND lon IS NOT NULL
                 THEN 'geo:' || round(lat::numeric, 4)::text || ',' || round(lon::numeric, 4)::text
            END
        ) AS cluster_key
    FROM houses
),
clusters_with_count AS (
    SELECT cluster_key, count(*) AS n
    FROM clustered
    WHERE cluster_key IS NOT NULL
    GROUP BY cluster_key
    HAVING count(*) > 1
),
-- Только дома, входящие в кластер-дубль
dup_houses AS (
    SELECT h.id, cl.cluster_key
    FROM houses h
    JOIN clustered cl ON cl.id = h.id
    JOIN clusters_with_count cw ON cw.cluster_key = cl.cluster_key
),
-- Выбрать canonical (rn=1) и дубли (rn>1) внутри каждого кластера
ranked AS (
    SELECT
        dh.id,
        dh.cluster_key,
        ROW_NUMBER() OVER (
            PARTITION BY dh.cluster_key
            ORDER BY
                (h.geom IS NOT NULL) DESC,
                -- Считаем не-NULL значимые колонки для оценки полноты
                (
                    (h.address IS NOT NULL)::int +
                    (h.lat IS NOT NULL)::int +
                    (h.lon IS NOT NULL)::int +
                    (h.year_built IS NOT NULL)::int +
                    (h.house_type IS NOT NULL)::int +
                    (h.total_floors IS NOT NULL)::int +
                    (h.cadastral_number IS NOT NULL)::int +
                    (h.developer_name IS NOT NULL)::int +
                    (h.rating_score IS NOT NULL)::int +
                    (h.avito_id_hash IS NOT NULL)::int
                ) DESC,
                dh.id ASC
        ) AS rn,
        first_value(dh.id) OVER (
            PARTITION BY dh.cluster_key
            ORDER BY
                (h.geom IS NOT NULL) DESC,
                (
                    (h.address IS NOT NULL)::int +
                    (h.lat IS NOT NULL)::int +
                    (h.lon IS NOT NULL)::int +
                    (h.year_built IS NOT NULL)::int +
                    (h.house_type IS NOT NULL)::int +
                    (h.total_floors IS NOT NULL)::int +
                    (h.cadastral_number IS NOT NULL)::int +
                    (h.developer_name IS NOT NULL)::int +
                    (h.rating_score IS NOT NULL)::int +
                    (h.avito_id_hash IS NOT NULL)::int
                ) DESC,
                dh.id ASC
        ) AS canonical_id
    FROM dup_houses dh
    JOIN houses h ON h.id = dh.id
)
SELECT id AS dup_id, canonical_id
FROM ranked
WHERE rn > 1
  AND id <> canonical_id;

-- Лог размера mapping
DO $$
DECLARE
    v_dups bigint;
    v_clusters bigint;
BEGIN
    SELECT count(*), count(DISTINCT canonical_id) INTO v_dups, v_clusters
    FROM _108_dup_mapping;
    RAISE NOTICE 'merge 108: mapping built — dup_rows=%, clusters_to_merge=%', v_dups, v_clusters;
END $$;

-- -------------------------------------------------------------------------
-- Step 2: Репойнт зависимых таблиц (NO ACTION FK — обязательно до DELETE)
-- -------------------------------------------------------------------------

-- 2a. listings.house_id_fk  (NO ACTION — ключевой)
UPDATE listings l
SET house_id_fk = m.canonical_id
FROM _108_dup_mapping m
WHERE l.house_id_fk = m.dup_id;

-- 2b. house_placement_history.house_id  (NO ACTION)
UPDATE house_placement_history hph
SET house_id = m.canonical_id
FROM _108_dup_mapping m
WHERE hph.house_id = m.dup_id;

-- -------------------------------------------------------------------------
-- Step 3: Репойнт таблиц с CASCADE FK + UNIQUE-коллизиями
--
-- Паттерн: сначала удалить дублирующую строку дубля если canonical уже имеет
-- такой же ключ, затем репойнтить оставшееся.
-- -------------------------------------------------------------------------

-- 3a. house_sources  UNIQUE(ext_source, ext_id)
--   Удалить строки дубля, у которых (ext_source,ext_id) уже есть у canonical.
DELETE FROM house_sources hs
USING _108_dup_mapping m
WHERE hs.house_id = m.dup_id
  AND EXISTS (
      SELECT 1 FROM house_sources hs2
      WHERE hs2.house_id = m.canonical_id
        AND hs2.ext_source = hs.ext_source
        AND hs2.ext_id = hs.ext_id
  );
--   Репойнтить оставшиеся строки дубля
UPDATE house_sources hs
SET house_id = m.canonical_id
FROM _108_dup_mapping m
WHERE hs.house_id = m.dup_id;

-- 3b. house_address_aliases  UNIQUE(normalized_address)
--   Удалить строки дубля, у которых normalized_address уже есть у canonical.
DELETE FROM house_address_aliases haa
USING _108_dup_mapping m
WHERE haa.house_id = m.dup_id
  AND EXISTS (
      SELECT 1 FROM house_address_aliases haa2
      WHERE haa2.house_id = m.canonical_id
        AND haa2.normalized_address = haa.normalized_address
  );
--   Репойнтить оставшиеся
UPDATE house_address_aliases haa
SET house_id = m.canonical_id
FROM _108_dup_mapping m
WHERE haa.house_id = m.dup_id;

-- 3c. houses_price_dynamics  UNIQUE(house_id, month_date, source)
--   Collision-safe: дедуп по ЦЕЛЕВОМУ ключу (canonical, month_date, source) —
--   ловит и dup-vs-canonical, и dup-vs-dup в кластерах >2. Выживает строка
--   canonical'а (m.canonical_id IS NULL), иначе минимальный id.
DELETE FROM houses_price_dynamics t
USING (
    SELECT t2.id,
           ROW_NUMBER() OVER (
               PARTITION BY COALESCE(m.canonical_id, t2.house_id), t2.month_date, t2.source
               ORDER BY (m.canonical_id IS NULL) DESC, t2.id ASC
           ) AS rn
    FROM houses_price_dynamics t2
    LEFT JOIN _108_dup_mapping m ON m.dup_id = t2.house_id
) d
WHERE t.id = d.id AND d.rn > 1;
--   Репойнтить оставшиеся (целевой ключ теперь свободен)
UPDATE houses_price_dynamics hpd
SET house_id = m.canonical_id
FROM _108_dup_mapping m
WHERE hpd.house_id = m.dup_id;

-- 3d. house_imv_evaluations  UNIQUE(house_id)
--   Collision-safe: одна evaluation на canonical. Выживает canonical'а, иначе min id.
DELETE FROM house_imv_evaluations t
USING (
    SELECT t2.id,
           ROW_NUMBER() OVER (
               PARTITION BY COALESCE(m.canonical_id, t2.house_id)
               ORDER BY (m.canonical_id IS NULL) DESC, t2.id ASC
           ) AS rn
    FROM house_imv_evaluations t2
    LEFT JOIN _108_dup_mapping m ON m.dup_id = t2.house_id
) d
WHERE t.id = d.id AND d.rn > 1;
--   Репойнтить оставшиеся
UPDATE house_imv_evaluations hie
SET house_id = m.canonical_id
FROM _108_dup_mapping m
WHERE hie.house_id = m.dup_id;

-- 3e. house_suggestions  UNIQUE(house_id, ext_item_id)
--   Collision-safe: дедуп по (canonical, ext_item_id) — ловит dup-vs-dup.
DELETE FROM house_suggestions t
USING (
    SELECT t2.id,
           ROW_NUMBER() OVER (
               PARTITION BY COALESCE(m.canonical_id, t2.house_id), t2.ext_item_id
               ORDER BY (m.canonical_id IS NULL) DESC, t2.id ASC
           ) AS rn
    FROM house_suggestions t2
    LEFT JOIN _108_dup_mapping m ON m.dup_id = t2.house_id
) d
WHERE t.id = d.id AND d.rn > 1;
--   Репойнтить оставшиеся
UPDATE house_suggestions hs
SET house_id = m.canonical_id
FROM _108_dup_mapping m
WHERE hs.house_id = m.dup_id;

-- 3f. house_reviews (CASCADE, нет UNIQUE конфликта — просто репойнт)
UPDATE house_reviews hr
SET house_id = m.canonical_id
FROM _108_dup_mapping m
WHERE hr.house_id = m.dup_id;

-- 3g. house_reliability_checks (CASCADE, нет блокирующего UNIQUE)
UPDATE house_reliability_checks hrc
SET house_id = m.canonical_id
FROM _108_dup_mapping m
WHERE hrc.house_id = m.dup_id;

-- 3h. external_valuations (CASCADE, нет блокирующего UNIQUE)
UPDATE external_valuations ev
SET house_id = m.canonical_id
FROM _108_dup_mapping m
WHERE ev.house_id = m.dup_id;

-- 3i. address_mismatch_audit  UNIQUE(house_id, audit_batch) — collision-safe
--   (заголовок ранее ошибочно помечал «нет блокирующего UNIQUE»).
DELETE FROM address_mismatch_audit t
USING (
    SELECT t2.id,
           ROW_NUMBER() OVER (
               PARTITION BY COALESCE(m.canonical_id, t2.house_id), t2.audit_batch
               ORDER BY (m.canonical_id IS NULL) DESC, t2.id ASC
           ) AS rn
    FROM address_mismatch_audit t2
    LEFT JOIN _108_dup_mapping m ON m.dup_id = t2.house_id
) d
WHERE t.id = d.id AND d.rn > 1;
--   Репойнтить оставшиеся
UPDATE address_mismatch_audit ama
SET house_id = m.canonical_id
FROM _108_dup_mapping m
WHERE ama.house_id = m.dup_id;

-- -------------------------------------------------------------------------
-- Step 4: Удалить дубли (все FK уже перепривязаны или CASCADE)
-- -------------------------------------------------------------------------
DELETE FROM houses h
USING _108_dup_mapping m
WHERE h.id = m.dup_id;

-- -------------------------------------------------------------------------
-- Step 5: Backfill house_sources для canonical-домов
--   Гарантируем что у каждого canonical'а есть source-запись для avito,
--   чтобы Tier1 (source_exact) в match_or_create_house сработал на следующем скрейпе.
--
--   Используем ext_house_id из houses.ext_house_id — только для домов с source='avito'.
--   Для домов source='derived' ориентируемся на address — backfill address_aliases ниже.
-- -------------------------------------------------------------------------
INSERT INTO house_sources (house_id, ext_source, ext_id, confidence, matched_method, matched_at, last_seen_at)
SELECT
    h.id,
    h.source,
    h.ext_house_id,
    1.0,
    'backfill_merge',
    NOW(),
    NOW()
FROM houses h
WHERE h.source = 'avito'
  AND h.ext_house_id IS NOT NULL
  AND h.id IN (SELECT canonical_id FROM _108_dup_mapping)
ON CONFLICT (ext_source, ext_id) DO NOTHING;

-- Backfill address_aliases — нормализованный адрес как low(trim(address)).
-- Питоновский normalize_address() недоступен в SQL; используем lower(trim(address))
-- как приближение. Если это недостаточно точно — forward-fix само залечит через
-- geo-tier при следующем скрейпе (Tier3 строит alias автоматически).
INSERT INTO house_address_aliases (house_id, normalized_address, fingerprint, source)
SELECT
    h.id,
    lower(trim(h.address)),
    NULL,  -- fingerprint не вычисляем в SQL — обновится при следующем скрейпе
    'backfill_merge'
FROM houses h
WHERE h.address IS NOT NULL
  AND length(trim(h.address)) >= 5
  AND h.id IN (SELECT canonical_id FROM _108_dup_mapping)
ON CONFLICT (normalized_address) DO NOTHING;

-- -------------------------------------------------------------------------
-- Step 6: Итоговые счётчики
-- -------------------------------------------------------------------------
DO $$
DECLARE
    v_before          bigint;
    v_clusters_merged bigint;
    v_rows_deleted    bigint;
    v_after           bigint;
BEGIN
    -- before был зафиксирован в Step 0 RAISE NOTICE; здесь пересчитываем через mapping
    SELECT count(DISTINCT canonical_id) INTO v_clusters_merged FROM _108_dup_mapping;
    SELECT count(*)                     INTO v_rows_deleted    FROM _108_dup_mapping;
    SELECT count(*)                     INTO v_after           FROM houses;
    -- before = after + deleted
    v_before := v_after + v_rows_deleted;

    RAISE NOTICE
        'merge 108 done: houses before=%, clusters_merged=%, rows_deleted=%, after=%',
        v_before, v_clusters_merged, v_rows_deleted, v_after;
END $$;

DROP TABLE IF EXISTS _108_dup_mapping;

COMMIT;
