#!/usr/bin/env bash
#
# Повторяемая проверка, что DM-витрины и Superset metadata работают на данных
# стартовой истории генератора, а не на архивном сиде 2022 года.

set -euo pipefail

COMPOSE_BIN="${COMPOSE_BIN:-docker compose}"
CLICKHOUSE_SERVICE="${CLICKHOUSE_SERVICE:-clickhouse}"
CLICKHOUSE_USER="${CLICKHOUSE_USER:-default}"
CLICKHOUSE_PASSWORD="${CLICKHOUSE_PASSWORD:-123456}"
GEN_MODEL_T0="${GEN_MODEL_T0:-2026-01-01T00:00:00+00:00}"
GEN_MODEL_T_END="${GEN_MODEL_T_END:-2026-01-01T06:00:00+00:00}"
REQUIRE_SUPERSET="${REQUIRE_SUPERSET:-1}"
CHECK_LIVE_SEAM="${CHECK_LIVE_SEAM:-1}"
GEN_LIVE_CHECK_MINUTES="${GEN_LIVE_CHECK_MINUTES:-10}"
WAIT_LIVE_ROWS_SECONDS="${WAIT_LIVE_ROWS_SECONDS:-10}"

fail() {
  echo "Ошибка: $*" >&2
  exit 1
}

clickhouse_datetime_literal() {
  local value="$1"
  value="${value/T/ }"
  value="${value%Z}"
  if [[ "${value}" =~ ^(.*)[+-][0-9]{2}:[0-9]{2}$ ]]; then
    value="${BASH_REMATCH[1]}"
  fi
  echo "${value}"
}

ch_query() {
  ${COMPOSE_BIN} exec -T "${CLICKHOUSE_SERVICE}" clickhouse-client \
    --user="${CLICKHOUSE_USER}" \
    --password="${CLICKHOUSE_PASSWORD}" \
    --query "$1"
}

pg_query() {
  ${COMPOSE_BIN} exec -T postgres-metadata psql \
    -U airflow \
    -d superset \
    -At \
    -c "$1" | tr -d '\r'
}

CH_MODEL_T0="$(clickhouse_datetime_literal "${GEN_MODEL_T0}")"
CH_MODEL_T_END="$(clickhouse_datetime_literal "${GEN_MODEL_T_END}")"

[[ "${GEN_LIVE_CHECK_MINUTES}" =~ ^[0-9]+$ ]] \
  || fail "GEN_LIVE_CHECK_MINUTES должен быть целым числом минут"
[[ "${WAIT_LIVE_ROWS_SECONDS}" =~ ^[0-9]+$ ]] \
  || fail "WAIT_LIVE_ROWS_SECONDS должен быть целым числом секунд"
case "${CHECK_LIVE_SEAM}" in
  0|1|auto) ;;
  *) fail "CHECK_LIVE_SEAM должен быть 0, 1 или auto" ;;
esac

echo "=== Проверка startup-history manifest ==="

manifest_summary="$(${COMPOSE_BIN} run --rm --no-deps \
  generator python -m clickstream_generator.startup_history_artifact kafka-manifest-summary)"

IFS=$'\t' read -r manifest_events manifest_visits manifest_users manifest_min_ts manifest_max_ts manifest_model_t0 manifest_model_t_end manifest_launch_profile <<< "${manifest_summary}"

[[ "${manifest_events}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать startup-history manifest из Kafka"
[[ -n "${manifest_model_t0}" ]] || fail "startup-history manifest не содержит model_t0"
[[ -n "${manifest_model_t_end}" ]] || fail "startup-history manifest не содержит model_t_end"

CH_MODEL_T0="$(clickhouse_datetime_literal "${manifest_model_t0}")"
CH_MODEL_T_END="$(clickhouse_datetime_literal "${manifest_model_t_end}")"

expected_profile="${PROFILE:-${GEN_LAUNCH_PROFILE:-}}"
if [[ -n "${expected_profile}" && -n "${manifest_launch_profile}" && "${expected_profile}" != "${manifest_launch_profile}" ]]; then
  fail "проверка запущена для профиля ${expected_profile}, но manifest от ${manifest_launch_profile}"
fi

echo "manifest_events=${manifest_events}"
echo "manifest_visits=${manifest_visits}"
echo "manifest_users=${manifest_users}"
echo "manifest_model_t0=${manifest_model_t0}"
echo "manifest_model_t_end=${manifest_model_t_end}"
echo "manifest_launch_profile=${manifest_launch_profile}"

echo "=== Проверка ClickHouse: данные генерации в DM ==="

stats_query="
WITH
    toDateTime64('${CH_MODEL_T0}', 6) AS t0,
    toDateTime64('${CH_MODEL_T_END}', 6) AS t_end
SELECT
    count() AS events,
    uniqExact(click_id) AS visits,
    uniqExact(user_domain_id) AS users,
    toString(min(event_ts)) AS min_event_ts,
    toString(max(event_ts)) AS max_event_ts,
    users < visits AND visits < events AS pyramid_ok,
    min(event_ts) >= t0 AND max(event_ts) < t_end AS half_open_ok,
    countIf(toYear(event_ts) = 2022) = 0 AS no_2022_rows,
    hex(sipHash128(groupArray(tuple(event_id, click_id, user_domain_id, event_ts, page_url_path)))) AS digest
FROM
(
    SELECT
        event_id,
        click_id,
        user_domain_id,
        event_ts,
        page_url_path
    FROM dm.v_events_enriched
    WHERE event_ts >= t0 AND event_ts < t_end
    ORDER BY event_id
)
FORMAT TabSeparated"

stats="$(ch_query "${stats_query}")"
IFS=$'\t' read -r events visits users min_event_ts max_event_ts pyramid_ok half_open_ok no_2022_rows digest <<< "${stats}"

[[ "${events}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать число событий из dm.v_events_enriched"
(( events > 0 )) || fail "dm.v_events_enriched пустая в модельном диапазоне"
[[ "${pyramid_ok}" == "1" ]] || fail "нарушена пирамида users < visits < events"
[[ "${half_open_ok}" == "1" ]] || fail "данные вышли за диапазон [GEN_MODEL_T0, GEN_MODEL_T_END)"
[[ "${no_2022_rows}" == "1" ]] || fail "найдены строки 2022 года, похожие на архивный сид"

echo "events=${events}"
echo "visits=${visits}"
echo "users=${users}"
echo "min_event_ts=${min_event_ts}"
echo "max_event_ts=${max_event_ts}"
echo "digest=${digest}"

echo ""
echo "=== Проверка ClickHouse: возвраты пользователей ==="

returns_query="
WITH
    toDateTime64('${CH_MODEL_T0}', 6) AS t0,
    toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
    users AS (
        SELECT user_domain_id, uniqExact(click_id) AS visits
        FROM dm.v_events_enriched
        WHERE event_ts >= t0 AND event_ts < t_end
          AND user_domain_id IS NOT NULL
        GROUP BY user_domain_id
    )
SELECT
    count() AS users,
    countIf(visits > 1) AS returning_users,
    returning_users / users AS returning_share,
    max(visits) AS max_visits_per_user
FROM users
FORMAT TabSeparated"

returns="$(ch_query "${returns_query}")"
IFS=$'\t' read -r total_users returning_users returning_share max_visits_per_user <<< "${returns}"

[[ "${total_users}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать число пользователей"
(( total_users > 0 )) || fail "нет пользователей для проверки возвратов"
(( returning_users > 0 )) || fail "нет пользователей с повторными визитами"

echo "users=${total_users}"
echo "returning_users=${returning_users}"
echo "returning_share=${returning_share}"
echo "max_visits_per_user=${max_visits_per_user}"

echo ""
echo "=== Проверка ClickHouse: форма длины визита ==="

visit_shape_query="
WITH
    toDateTime64('${CH_MODEL_T0}', 6) AS t0,
    toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
    30 AS max_session_events,
    sessions AS (
        SELECT
            click_id,
            count() AS events_count,
            dateDiff('second', min(event_ts), max(event_ts)) AS duration_sec
        FROM dm.v_events_enriched
        WHERE event_ts >= t0 AND event_ts < t_end
        GROUP BY click_id
    )
SELECT
    count() AS visits,
    countIf(events_count <= 2) / visits AS short_visit_share,
    quantileExact(0.5)(events_count) AS median_events_per_visit,
    avg(events_count) AS avg_events_per_visit,
    countIf(events_count = max_session_events) / visits AS capped_visit_share,
    quantileExact(0.5)(duration_sec) AS median_duration_sec,
    quantileExact(0.95)(duration_sec) AS p95_duration_sec,
    max(events_count) AS max_events_per_visit
FROM sessions
FORMAT TabSeparated"

visit_shape="$(ch_query "${visit_shape_query}")"
IFS=$'\t' read -r shape_visits short_visit_share median_events_per_visit avg_events_per_visit capped_visit_share median_duration_sec p95_duration_sec max_events_per_visit <<< "${visit_shape}"

[[ "${shape_visits}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать число визитов"
(( shape_visits > 0 )) || fail "нет визитов для проверки формы"
(( max_events_per_visit <= 30 )) || fail "длина визита превысила GEN_MAX_SESSION_EVENTS"

echo "visits=${shape_visits}"
echo "short_visit_share=${short_visit_share}"
echo "median_events_per_visit=${median_events_per_visit}"
echo "avg_events_per_visit=${avg_events_per_visit}"
echo "capped_visit_share=${capped_visit_share}"
echo "median_duration_sec=${median_duration_sec}"
echo "p95_duration_sec=${p95_duration_sec}"
echo "max_events_per_visit=${max_events_per_visit}"

echo ""
echo "=== Проверка ClickHouse: ordered funnel ==="

ordered_funnel_query="
WITH
    toDateTime64('${CH_MODEL_T0}', 6) AS t0,
    toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
    sessions AS (
        SELECT
            click_id,
            minIf(event_ts, page_url_path = '/home') AS home_ts,
            minIf(event_ts, page_url_path IN ('/product_a', '/product_b')) AS product_ts,
            minIf(event_ts, page_url_path = '/cart') AS cart_ts,
            minIf(event_ts, page_url_path = '/payment') AS payment_ts,
            minIf(event_ts, page_url_path = '/confirmation') AS confirmation_ts
        FROM dm.v_events_enriched
        WHERE event_ts >= t0 AND event_ts < t_end
        GROUP BY click_id
    )
SELECT
    countIf(home_ts IS NOT NULL) AS home,
    countIf(home_ts IS NOT NULL AND product_ts > home_ts) AS products,
    countIf(home_ts IS NOT NULL AND product_ts > home_ts AND cart_ts > product_ts) AS cart,
    countIf(home_ts IS NOT NULL AND product_ts > home_ts AND cart_ts > product_ts AND payment_ts > cart_ts) AS payment,
    countIf(home_ts IS NOT NULL AND product_ts > home_ts AND cart_ts > product_ts AND payment_ts > cart_ts AND confirmation_ts > payment_ts) AS confirmation,
    products <= home AND cart <= products AND payment <= cart AND confirmation <= payment AS monotonic_ok,
    confirmation / home AS confirmation_share
FROM sessions
FORMAT TabSeparated"

ordered_funnel="$(ch_query "${ordered_funnel_query}")"
IFS=$'\t' read -r ordered_home ordered_products ordered_cart ordered_payment ordered_confirmation ordered_monotonic_ok ordered_confirmation_share <<< "${ordered_funnel}"

[[ "${ordered_home}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать ordered funnel"
(( ordered_home > 0 )) || fail "ordered funnel: нет /home"
[[ "${ordered_monotonic_ok}" == "1" ]] || fail "ordered funnel не монотонен"

echo "home=${ordered_home}"
echo "products=${ordered_products}"
echo "cart=${ordered_cart}"
echo "payment=${ordered_payment}"
echo "confirmation=${ordered_confirmation}"
echo "monotonic_ok=${ordered_monotonic_ok}"
echo "confirmation_share=${ordered_confirmation_share}"

echo ""
echo "=== Проверка ClickHouse: contains funnel ==="

contains_funnel_query="
WITH
    toDateTime64('${CH_MODEL_T0}', 6) AS t0,
    toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
    sessions AS (
        SELECT
            click_id,
            countIf(page_url_path = '/home') > 0 AS has_home,
            countIf(page_url_path IN ('/product_a', '/product_b')) > 0 AS has_product,
            countIf(page_url_path = '/cart') > 0 AS has_cart,
            countIf(page_url_path = '/payment') > 0 AS has_payment,
            countIf(page_url_path = '/confirmation') > 0 AS has_confirmation
        FROM dm.v_events_enriched
        WHERE event_ts >= t0 AND event_ts < t_end
        GROUP BY click_id
    )
SELECT
    countIf(has_home) AS home,
    countIf(has_home AND has_product) AS products,
    countIf(has_home AND has_product AND has_cart) AS cart,
    countIf(has_home AND has_product AND has_cart AND has_payment) AS payment,
    countIf(has_home AND has_product AND has_cart AND has_payment AND has_confirmation) AS confirmation,
    products <= home AND cart <= products AND payment <= cart AND confirmation <= payment AS monotonic_ok,
    confirmation / home AS confirmation_share
FROM sessions
FORMAT TabSeparated"

contains_funnel="$(ch_query "${contains_funnel_query}")"
IFS=$'\t' read -r contains_home contains_products contains_cart contains_payment contains_confirmation contains_monotonic_ok contains_confirmation_share <<< "${contains_funnel}"

[[ "${contains_home}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать contains funnel"
(( contains_home > 0 )) || fail "contains funnel: нет /home"
[[ "${contains_monotonic_ok}" == "1" ]] || fail "contains funnel не монотонен"

echo "home=${contains_home}"
echo "products=${contains_products}"
echo "cart=${contains_cart}"
echo "payment=${contains_payment}"
echo "confirmation=${contains_confirmation}"
echo "monotonic_ok=${contains_monotonic_ok}"
echo "confirmation_share=${contains_confirmation_share}"

should_check_live_seam="${CHECK_LIVE_SEAM}"
if [[ "${should_check_live_seam}" != "0" ]]; then
  live_rows_query="
WITH
    toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
    t_end + INTERVAL ${GEN_LIVE_CHECK_MINUTES} MINUTE AS t_live_end
SELECT count()
FROM dds.event
WHERE event_ts >= t_end AND event_ts < t_live_end
FORMAT TabSeparated"
  live_rows=0
  deadline=$((SECONDS + WAIT_LIVE_ROWS_SECONDS))
  while true; do
    live_rows="$(ch_query "${live_rows_query}")"
    [[ "${live_rows}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать live-строки DDS после GEN_MODEL_T_END"
    if (( live_rows > 0 || SECONDS >= deadline )); then
      break
    fi
    sleep 1
  done
  if [[ "${should_check_live_seam}" == "1" ]] && (( live_rows == 0 )); then
    fail "live-продолжение не записало строки в первые ${GEN_LIVE_CHECK_MINUTES} минут после manifest model_t_end"
  fi
  if [[ "${CHECK_LIVE_SEAM}" == "auto" ]]; then
    if (( live_rows > 0 )); then
      should_check_live_seam="1"
    else
      should_check_live_seam="0"
    fi
  fi
fi

if [[ "${should_check_live_seam}" == "1" ]]; then
  echo ""
  echo "=== Проверка ClickHouse: стык backfill/live в DDS ==="

  live_pairing_query="
WITH
    parseDateTime64BestEffort('${manifest_model_t_end}', 6, 'UTC') AS t_end,
    t_end + INTERVAL ${GEN_LIVE_CHECK_MINUTES} MINUTE AS t_live_end,
    live_browser AS (
        SELECT
            toUUIDOrNull(JSONExtractString(raw, 'event_id')) AS event_id,
            toUUIDOrNull(JSONExtractString(raw, 'click_id')) AS click_id
        FROM stg.browser_raw
        WHERE parseDateTime64BestEffortOrNull(
                  JSONExtractString(raw, 'event_timestamp'), 6, 'UTC'
              ) >= t_end
          AND parseDateTime64BestEffortOrNull(
                  JSONExtractString(raw, 'event_timestamp'), 6, 'UTC'
              ) < t_live_end
          AND event_id IS NOT NULL
          AND click_id IS NOT NULL
        GROUP BY event_id, click_id
    ),
    live_click_ids AS (
        SELECT click_id
        FROM live_browser
        GROUP BY click_id
    ),
    browser_counts AS (
        SELECT click_id, count() AS browser_rows
        FROM
        (
            SELECT
                toUUIDOrNull(JSONExtractString(raw, 'event_id')) AS event_id,
                toUUIDOrNull(JSONExtractString(raw, 'click_id')) AS click_id
            FROM stg.browser_raw
            WHERE event_id IS NOT NULL AND click_id IS NOT NULL
            GROUP BY event_id, click_id
        )
        WHERE click_id IN (SELECT click_id FROM live_click_ids)
        GROUP BY click_id
    ),
    location_ids AS (
        SELECT toUUIDOrNull(JSONExtractString(raw, 'event_id')) AS event_id
        FROM stg.location_raw
        WHERE event_id IS NOT NULL
        GROUP BY event_id
    ),
    device_counts AS (
        SELECT
            toUUIDOrNull(JSONExtractString(raw, 'click_id')) AS click_id,
            count() AS device_rows
        FROM stg.device_raw
        WHERE click_id IS NOT NULL
          AND click_id IN (SELECT click_id FROM live_click_ids)
        GROUP BY click_id
    ),
    geo_counts AS (
        SELECT
            toUUIDOrNull(JSONExtractString(raw, 'click_id')) AS click_id,
            count() AS geo_rows
        FROM stg.geo_raw
        WHERE click_id IS NOT NULL
          AND click_id IN (SELECT click_id FROM live_click_ids)
        GROUP BY click_id
    )
SELECT
    (SELECT count() FROM live_browser) AS live_browser_rows,
    (
        SELECT count()
        FROM live_browser AS b
        LEFT JOIN location_ids AS l ON l.event_id = b.event_id
        WHERE l.event_id IS NULL
    ) AS unpaired_location_rows,
    (
        SELECT ifNull(sum(if(
            b.browser_rows > ifNull(d.device_rows, 0),
            b.browser_rows - ifNull(d.device_rows, 0),
            0
        )), 0)
        FROM browser_counts AS b
        LEFT JOIN device_counts AS d ON d.click_id = b.click_id
    ) AS unpaired_device_rows,
    (
        SELECT ifNull(sum(if(
            b.browser_rows > ifNull(g.geo_rows, 0),
            b.browser_rows - ifNull(g.geo_rows, 0),
            0
        )), 0)
        FROM browser_counts AS b
        LEFT JOIN geo_counts AS g ON g.click_id = b.click_id
    ) AS unpaired_geo_rows
SETTINGS join_use_nulls = 1
FORMAT TabSeparated"

  live_pairing="$(ch_query "${live_pairing_query}")"
  IFS=$'\t' read -r live_browser_rows unpaired_location_rows unpaired_device_rows unpaired_geo_rows <<< "${live_pairing}"

  [[ "${live_browser_rows}" =~ ^[0-9]+$ ]] \
    && [[ "${unpaired_location_rows}" =~ ^[0-9]+$ ]] \
    && [[ "${unpaired_device_rows}" =~ ^[0-9]+$ ]] \
    && [[ "${unpaired_geo_rows}" =~ ^[0-9]+$ ]] \
    || fail "не удалось прочитать пары live-строк"
  if (( unpaired_location_rows > 0 || unpaired_device_rows > 0 || unpaired_geo_rows > 0 )); then
    fail "непарные live-строки: location=${unpaired_location_rows}, device=${unpaired_device_rows}, geo=${unpaired_geo_rows} из browser=${live_browser_rows}"
  fi

  echo "live_browser_rows=${live_browser_rows}"
  echo "unpaired_location_rows=${unpaired_location_rows}"
  echo "unpaired_device_rows=${unpaired_device_rows}"
  echo "unpaired_geo_rows=${unpaired_geo_rows}"

  seam_duplicates_query="
WITH
    toDateTime64('${CH_MODEL_T0}', 6) AS t0,
    toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
    t_end + INTERVAL ${GEN_LIVE_CHECK_MINUTES} MINUTE AS t_live_end
SELECT
    count() AS events,
    uniqExact(event_id) AS unique_events,
    events - unique_events AS duplicate_events
FROM dds.event
WHERE event_ts >= t0 AND event_ts < t_live_end
FORMAT TabSeparated"

  seam_duplicates="$(ch_query "${seam_duplicates_query}")"
  IFS=$'\t' read -r seam_events seam_unique_events seam_duplicate_events <<< "${seam_duplicates}"

  [[ "${seam_events}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать события DDS на стыке"
  (( seam_events > 0 )) || fail "нет событий DDS в диапазоне проверки стыка"
  (( seam_duplicate_events == 0 )) || fail "на стыке найдены дубли event_id: ${seam_duplicate_events}"

  echo "events=${seam_events}"
  echo "unique_events=${seam_unique_events}"
  echo "duplicate_events=${seam_duplicate_events}"

  seam_context_query="
WITH
    toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
    t_end + INTERVAL ${GEN_LIVE_CHECK_MINUTES} MINUTE AS t_live_end,
    crossing AS (
        SELECT
            e.click_id,
            min(e.event_ts) AS first_ts,
            max(e.event_ts) AS last_ts,
            groupUniqArray(coalesce(toString(e.browser_name), '__NULL__')) AS browser_names,
            groupUniqArray(coalesce(toString(e.browser_language), '__NULL__')) AS browser_languages,
            groupUniqArray(coalesce(toString(e.browser_user_agent), '__NULL__')) AS browser_user_agents,
            groupUniqArray(coalesce(toString(e.referer_url), '__NULL__')) AS referer_urls,
            groupUniqArray(coalesce(toString(e.referer_medium), '__NULL__')) AS referer_mediums,
            groupUniqArray(coalesce(toString(e.utm_medium), '__NULL__')) AS utm_mediums,
            groupUniqArray(coalesce(toString(e.utm_source), '__NULL__')) AS utm_sources,
            groupUniqArray(coalesce(toString(e.utm_content), '__NULL__')) AS utm_contents,
            groupUniqArray(coalesce(toString(e.utm_campaign), '__NULL__')) AS utm_campaigns,
            groupUniqArray(coalesce(toString(c.device_type), '__NULL__')) AS device_types,
            groupUniqArray(coalesce(toString(c.os_name), '__NULL__')) AS os_names,
            groupUniqArray(coalesce(toString(c.geo_country), '__NULL__')) AS geo_countries
        FROM dds.event AS e
        LEFT JOIN dds.click AS c ON c.click_id = e.click_id
        WHERE e.event_ts >= t_end - INTERVAL 30 MINUTE
          AND e.event_ts < t_live_end
        GROUP BY e.click_id
        HAVING first_ts < t_end AND last_ts >= t_end
    )
SELECT
    count() AS crossing_visits,
    countIf(
        length(browser_names) = 1
        AND length(browser_languages) = 1
        AND length(browser_user_agents) = 1
        AND length(referer_urls) = 1
        AND length(referer_mediums) = 1
        AND length(utm_mediums) = 1
        AND length(utm_sources) = 1
        AND length(utm_contents) = 1
        AND length(utm_campaigns) = 1
    ) AS per_event_homogeneous_visits,
    countIf(
        length(device_types) = 1
        AND length(os_names) = 1
        AND length(geo_countries) = 1
    ) AS click_context_homogeneous_visits
FROM crossing
FORMAT TabSeparated"

  seam_context="$(ch_query "${seam_context_query}")"
  IFS=$'\t' read -r crossing_visits per_event_homogeneous_visits click_context_homogeneous_visits <<< "${seam_context}"

  [[ "${crossing_visits}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать переходящие визиты"
  (( crossing_visits > 0 )) || fail "нет визитов, переходящих через GEN_MODEL_T_END"
  (( per_event_homogeneous_visits == crossing_visits )) \
    || fail "per-event фактура меняется на стыке: ${per_event_homogeneous_visits}/${crossing_visits}"
  (( click_context_homogeneous_visits == crossing_visits )) \
    || fail "device/os/geo меняются на стыке: ${click_context_homogeneous_visits}/${crossing_visits}"

  echo "crossing_visits=${crossing_visits}"
  echo "per_event_homogeneous_visits=${per_event_homogeneous_visits}"
  echo "click_context_homogeneous_visits=${click_context_homogeneous_visits}"

  ods_context_query="
WITH
    toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
    t_end + INTERVAL ${GEN_LIVE_CHECK_MINUTES} MINUTE AS t_live_end,
    crossing AS (
        SELECT
            click_id
        FROM dds.event
        WHERE event_ts >= t_end - INTERVAL 30 MINUTE
          AND event_ts < t_live_end
        GROUP BY click_id
        HAVING min(event_ts) < t_end AND max(event_ts) >= t_end
    ),
    device_conflicts AS (
        SELECT
            d.click_id
        FROM ods.device_by_click AS d
        INNER JOIN crossing AS c ON c.click_id = d.click_id
        WHERE d.click_id IS NOT NULL
        GROUP BY d.click_id
        HAVING
            uniqExact(coalesce(toString(device_type), '__NULL__')) > 1
            OR uniqExact(coalesce(toString(device_is_mobile), '__NULL__')) > 1
            OR uniqExact(coalesce(toString(os_name), '__NULL__')) > 1
            OR uniqExact(coalesce(toString(os), '__NULL__')) > 1
            OR uniqExact(coalesce(toString(os_timezone), '__NULL__')) > 1
            OR uniqExact(coalesce(toString(user_domain_id), '__NULL__')) > 1
    ),
    geo_conflicts AS (
        SELECT
            g.click_id
        FROM ods.geo_by_click AS g
        INNER JOIN crossing AS c ON c.click_id = g.click_id
        WHERE g.click_id IS NOT NULL
        GROUP BY g.click_id
        HAVING
            uniqExact(coalesce(toString(geo_country), '__NULL__')) > 1
            OR uniqExact(coalesce(toString(geo_timezone), '__NULL__')) > 1
            OR uniqExact(coalesce(toString(geo_region_name), '__NULL__')) > 1
            OR uniqExact(coalesce(toString(geo_latitude), '__NULL__')) > 1
            OR uniqExact(coalesce(toString(geo_longitude), '__NULL__')) > 1
            OR uniqExact(coalesce(toString(ip_address), '__NULL__')) > 1
    )
SELECT
    (SELECT count() FROM crossing) AS crossing_visits,
    (
      SELECT count()
      FROM ods.device_by_click AS d
      INNER JOIN crossing AS c ON c.click_id = d.click_id
      WHERE d.click_id IS NOT NULL
    ) AS ods_device_rows,
    (
      SELECT count()
      FROM ods.geo_by_click AS g
      INNER JOIN crossing AS c ON c.click_id = g.click_id
      WHERE g.click_id IS NOT NULL
    ) AS ods_geo_rows,
    (SELECT count() FROM device_conflicts) AS ods_device_conflicts,
    (SELECT count() FROM geo_conflicts) AS ods_geo_conflicts
FORMAT TabSeparated"

  ods_context="$(ch_query "${ods_context_query}")"
  IFS=$'\t' read -r ods_crossing_visits ods_device_rows ods_geo_rows ods_device_conflicts ods_geo_conflicts <<< "${ods_context}"

  [[ "${ods_crossing_visits}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать ODS-проверку стыка"
  (( ods_crossing_visits == crossing_visits )) \
    || fail "ODS и DDS нашли разное число переходящих визитов: ${ods_crossing_visits}/${crossing_visits}"
  (( ods_device_rows > 0 )) || fail "ODS device пустой на стыке"
  (( ods_geo_rows > 0 )) || fail "ODS geo пустой на стыке"
  (( ods_device_conflicts == 0 )) || fail "ODS device конфликтует на стыке: ${ods_device_conflicts}"
  (( ods_geo_conflicts == 0 )) || fail "ODS geo конфликтует на стыке: ${ods_geo_conflicts}"

  echo "ods_device_rows=${ods_device_rows}"
  echo "ods_geo_rows=${ods_geo_rows}"
  echo "ods_device_conflicts=${ods_device_conflicts}"
  echo "ods_geo_conflicts=${ods_geo_conflicts}"
else
  echo ""
  echo "Проверка стыка backfill/live пропущена: CHECK_LIVE_SEAM=${CHECK_LIVE_SEAM}, live_rows=${live_rows:-0}"
fi

echo ""
echo "=== Проверка ClickHouse: основные DM-витрины не пустые ==="

views_query="
SELECT source, rows
FROM
(
    SELECT 'dm.v_events_enriched' AS source, count() AS rows FROM dm.v_events_enriched
    UNION ALL SELECT 'dm.v_daily_traffic', count() FROM dm.v_daily_traffic
    UNION ALL SELECT 'dm.v_top_pages_daily', count() FROM dm.v_top_pages_daily
    UNION ALL SELECT 'dm.v_utm_effectiveness', count() FROM dm.v_utm_effectiveness
    UNION ALL SELECT 'dm.v_session_overview', count() FROM dm.v_session_overview
    UNION ALL SELECT 'dm.dq_summary', count() FROM dm.dq_summary
)
ORDER BY source
FORMAT TabSeparated"

if ! views_rows="$(ch_query "${views_query}")"; then
  fail "не удалось прочитать основные DM-витрины"
fi
[[ -n "${views_rows}" ]] || fail "ClickHouse не вернул строки основных DM-витрин"

while IFS=$'\t' read -r source rows; do
  [[ -n "${source}" ]] || continue
  [[ "${rows}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать число строк для ${source}"
  (( rows > 0 )) || fail "${source} пустая"
  echo "${source}=${rows}"
done <<< "${views_rows}"

if [[ "${REQUIRE_SUPERSET}" != "1" ]]; then
  echo ""
  echo "Проверка Superset пропущена: REQUIRE_SUPERSET=${REQUIRE_SUPERSET}"
  exit 0
fi

echo ""
echo "=== Проверка Superset: datasets, charts и dashboard созданы ==="

if ! ${COMPOSE_BIN} ps --services --filter "status=running" | grep -qx "superset"; then
  fail "сервис superset не запущен"
fi

${COMPOSE_BIN} exec -T superset curl -fsS http://localhost:8088/health >/dev/null \
  || fail "Superset health endpoint не отвечает"

dataset_count="$(pg_query "
SELECT count(*)
FROM tables
WHERE schema = 'dm'
  AND table_name IN (
    'v_events_enriched',
    'v_daily_traffic',
    'v_utm_effectiveness',
    'v_top_pages_daily',
    'v_session_overview',
    'dq_summary'
  );")"

dashboard_count="$(pg_query "
SELECT count(*)
FROM dashboards
WHERE slug = 'ecommerce-analytics';")"

chart_count="$(pg_query "
SELECT count(*)
FROM dashboard_slices ds
JOIN dashboards d ON d.id = ds.dashboard_id
WHERE d.slug = 'ecommerce-analytics';")"

[[ "${dataset_count}" == "6" ]] || fail "ожидалось 6 Superset datasets, найдено ${dataset_count}"
[[ "${dashboard_count}" == "1" ]] || fail "dashboard ecommerce-analytics не найден"
(( chart_count > 0 )) || fail "dashboard ecommerce-analytics не связан с chart"

echo "superset_datasets=${dataset_count}"
echo "superset_dashboards=${dashboard_count}"
echo "superset_dashboard_charts=${chart_count}"
echo "dashboard_url=http://localhost:8088/superset/dashboard/ecommerce-analytics/"

echo ""
echo "=== Проверка Superset: dashboard открывается и читает ClickHouse ==="

superset_probe="$(
  ${COMPOSE_BIN} exec -T superset bash -s <<'PY'
set -euo pipefail
python - <<'PYTHON'
import json
import urllib.request

from sqlalchemy import text

from superset.app import create_app

app = create_app()
with app.app_context():
    from superset.extensions import db
    from superset.models.core import Database
    from superset.models.dashboard import Dashboard

    dashboard = (
        db.session.query(Dashboard)
        .filter_by(slug="ecommerce-analytics")
        .one()
    )

    database = (
        db.session.query(Database)
        .filter_by(database_name="clickhouse_dwh")
        .one()
    )
    with database.get_sqla_engine() as engine:
      with engine.connect() as connection:
        events = connection.execute(
            text("SELECT count() FROM dm.v_events_enriched")
        ).scalar()

login_payload = json.dumps(
    {
        "username": "admin",
        "password": "admin",
        "provider": "db",
        "refresh": True,
    }
).encode("utf-8")

login_request = urllib.request.Request(
    "http://localhost:8088/api/v1/security/login",
    data=login_payload,
    headers={"Content-Type": "application/json"},
    method="POST",
)
with urllib.request.urlopen(login_request, timeout=10) as response:
    login_status = response.status
    token = json.loads(response.read().decode("utf-8"))["access_token"]

dashboard_request = urllib.request.Request(
    f"http://localhost:8088/api/v1/dashboard/{dashboard.id}",
    headers={"Authorization": f"Bearer {token}"},
    method="GET",
)
with urllib.request.urlopen(dashboard_request, timeout=10) as response:
    dashboard_status = response.status
    dashboard_payload = json.loads(response.read().decode("utf-8"))

dashboard_title = dashboard_payload["result"]["dashboard_title"]

print(f"login_api_status={login_status}")
print(f"dashboard_api_status={dashboard_status}")
print(f"dashboard_title={dashboard_title}")
print(f"superset_clickhouse_events={events}")

if login_status >= 400:
    raise SystemExit("Superset login API failed")
if dashboard_status >= 400:
    raise SystemExit("Superset dashboard API did not open")
if "E-commerce Analytics" not in dashboard_title:
    raise SystemExit("Unexpected dashboard title")
if not isinstance(events, int) or events <= 0:
    raise SystemExit("Superset ClickHouse query returned no data")
PYTHON
PY
)"

echo "${superset_probe}"
