#!/usr/bin/env bash
#
# Проверяет завершённые стыки между порциями модельной истории.

set -euo pipefail

COMPOSE_BIN="${COMPOSE_BIN:-docker compose}"
CLICKHOUSE_SERVICE="${CLICKHOUSE_SERVICE:-clickhouse}"
CLICKHOUSE_USER="${CLICKHOUSE_USER:-default}"
CLICKHOUSE_PASSWORD="${CLICKHOUSE_PASSWORD:-123456}"

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

clickhouse_datetime_literal() {
  local value="$1"
  local normalized
  if ! normalized="$(date --utc --date="${value}" '+%Y-%m-%d %H:%M:%S.%6N')"; then
    fail "не удалось привести границу к UTC: ${value}"
  fi
  echo "${normalized}"
}

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

mapfile -t boundaries < <(
  ${COMPOSE_BIN} run --rm --no-deps generator \
    python -m clickstream_generator.startup_history_artifact kafka-boundaries
)

boundary_count="${#boundaries[@]}"
(( boundary_count >= 2 )) || fail "manifest boundaries должен содержать T0 и T_end"

manifest_topic_rows="$(${COMPOSE_BIN} run --rm --no-deps generator \
  python -m clickstream_generator.startup_history_artifact kafka-topic-rows)"
IFS=$'\t' read -r expected_browser_rows expected_location_rows \
  expected_device_rows expected_geo_rows <<< "${manifest_topic_rows}"
for rows in \
  "${expected_browser_rows}" \
  "${expected_location_rows}" \
  "${expected_device_rows}" \
  "${expected_geo_rows}"; do
  [[ "${rows}" =~ ^[0-9]+$ ]] || fail "manifest содержит неверные счётчики топиков"
done

echo "=== Проверка порций истории ==="
for ((index = 0; index < boundary_count - 1; index++)); do
  left="$(clickhouse_datetime_literal "${boundaries[index]}")"
  right="$(clickhouse_datetime_literal "${boundaries[index + 1]}")"
  rows="$(ch_query "
WITH
    toDateTime64('${left}', 6, 'UTC') AS segment_start,
    toDateTime64('${right}', 6, 'UTC') AS segment_end
SELECT count()
FROM dm.v_events_enriched
WHERE parseDateTime64BestEffortOrNull(toString(event_ts), 6, 'UTC') >= segment_start
  AND parseDateTime64BestEffortOrNull(toString(event_ts), 6, 'UTC') < segment_end
FORMAT TabSeparated")"
  [[ "${rows}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать порцию ${boundaries[index]}"
  (( rows > 0 )) || fail "порция [${boundaries[index]}, ${boundaries[index + 1]}) пуста"
  echo "segment_${index}_rows=${rows}"
done

actual_topic_rows="$(ch_query "
SELECT
    (SELECT count() FROM stg.browser_raw) AS actual_topic_rows,
    (SELECT count() FROM stg.location_raw) AS actual_location_rows,
    (SELECT count() FROM stg.device_raw) AS actual_device_rows,
    (SELECT count() FROM stg.geo_raw) AS actual_geo_rows
FORMAT TabSeparated")"
IFS=$'\t' read -r actual_browser_rows actual_location_rows \
  actual_device_rows actual_geo_rows <<< "${actual_topic_rows}"
topic_names=(browser_events location_events device_events geo_events)
expected_rows=(
  "${expected_browser_rows}"
  "${expected_location_rows}"
  "${expected_device_rows}"
  "${expected_geo_rows}"
)
actual_rows=(
  "${actual_browser_rows}"
  "${actual_location_rows}"
  "${actual_device_rows}"
  "${actual_geo_rows}"
)
for ((index = 0; index < ${#topic_names[@]}; index++)); do
  [[ "${actual_rows[index]}" =~ ^[0-9]+$ ]] \
    || fail "не удалось прочитать число строк ${topic_names[index]} в STG"
  if (( actual_rows[index] > expected_rows[index] )); then
    fail "найден хвост ${topic_names[index]} после manifest: STG=${actual_rows[index]}, manifest=${expected_rows[index]}"
  fi
  if (( actual_rows[index] < expected_rows[index] )); then
    fail "в STG не хватает строк ${topic_names[index]}: STG=${actual_rows[index]}, manifest=${expected_rows[index]}"
  fi
done

last_boundary="$(clickhouse_datetime_literal "${boundaries[boundary_count - 1]}")"
data_tail_rows="$(ch_query "
SELECT count() AS data_tail_rows
FROM stg.browser_raw
WHERE parseDateTime64BestEffortOrNull(
          JSONExtractString(raw, 'event_timestamp'), 6, 'UTC'
      ) >= toDateTime64('${last_boundary}', 6, 'UTC')
FORMAT TabSeparated")"
[[ "${data_tail_rows}" =~ ^[0-9]+$ ]] \
  || fail "не удалось проверить хвост после ${boundaries[boundary_count - 1]}"
(( data_tail_rows == 0 )) \
  || fail "найден хвост данных после последней границы manifest: ${data_tail_rows}"

if (( boundary_count == 2 )); then
  echo "Завершённых внутренних стыков пока нет."
  exit 0
fi

echo "=== Проверка внутренних стыков boundaries ==="
for ((index = 1; index < boundary_count - 1; index++)); do
  boundary="$(clickhouse_datetime_literal "${boundaries[index]}")"

  pairing="$(ch_query "
WITH
    toDateTime64('${boundary}', 6, 'UTC') AS boundary,
    browser_rows AS (
        SELECT
            toUUIDOrNull(JSONExtractString(raw, 'event_id')) AS event_id,
            toUUIDOrNull(JSONExtractString(raw, 'click_id')) AS click_id,
            parseDateTime64BestEffortOrNull(
                JSONExtractString(raw, 'event_timestamp'), 6, 'UTC'
            ) AS event_ts
        FROM stg.browser_raw
        WHERE event_id IS NOT NULL AND click_id IS NOT NULL AND event_ts IS NOT NULL
        GROUP BY event_id, click_id, event_ts
    ),
    crossing AS (
        SELECT click_id
        FROM browser_rows
        GROUP BY click_id
        HAVING min(event_ts) < boundary AND max(event_ts) >= boundary
    ),
    browser_counts AS (
        SELECT click_id, count() AS browser_rows
        FROM browser_rows
        WHERE click_id IN (SELECT click_id FROM crossing)
        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 IN (SELECT click_id FROM crossing)
        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 IN (SELECT click_id FROM crossing)
        GROUP BY click_id
    )
SELECT
    (SELECT count() FROM crossing) AS crossing_visits,
    (
        SELECT count()
        FROM browser_rows AS b
        LEFT JOIN location_ids AS l ON l.event_id = b.event_id
        WHERE b.click_id IN (SELECT click_id FROM crossing)
          AND l.event_id IS NULL
    ) AS unpaired_location_rows,
    (
        SELECT ifNull(sum(greatest(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(greatest(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")"

  IFS=$'\t' read -r crossing_visits unpaired_location_rows unpaired_device_rows unpaired_geo_rows <<< "${pairing}"
  [[ "${crossing_visits}" =~ ^[0-9]+$ ]] \
    && [[ "${unpaired_location_rows}" =~ ^[0-9]+$ ]] \
    && [[ "${unpaired_device_rows}" =~ ^[0-9]+$ ]] \
    && [[ "${unpaired_geo_rows}" =~ ^[0-9]+$ ]] \
    || fail "не удалось прочитать пары на границе ${boundaries[index]}"
  if (( unpaired_location_rows > 0 || unpaired_device_rows > 0 || unpaired_geo_rows > 0 )); then
    fail "непарные строки на границе ${boundaries[index]}: location=${unpaired_location_rows}, device=${unpaired_device_rows}, geo=${unpaired_geo_rows}"
  fi

  context="$(ch_query "
WITH
    toDateTime64('${boundary}', 6, 'UTC') AS boundary,
    crossing AS (
        SELECT click_id
        FROM
        (
            SELECT
                click_id,
                parseDateTime64BestEffortOrNull(
                    toString(event_ts), 6, 'UTC'
                ) AS model_event_ts
            FROM dds.event
            WHERE event_ts IS NOT NULL
        )
        GROUP BY click_id
        HAVING min(model_event_ts) < boundary AND max(model_event_ts) >= boundary
    ),
    facts AS (
        SELECT
            e.click_id,
            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
        FROM dds.event AS e
        INNER JOIN crossing AS c ON c.click_id = e.click_id
        GROUP BY e.click_id
    )
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
FROM facts
FORMAT TabSeparated")"

  IFS=$'\t' read -r context_crossing_visits per_event_homogeneous_visits <<< "${context}"
  [[ "${context_crossing_visits}" =~ ^[0-9]+$ ]] \
    && [[ "${per_event_homogeneous_visits}" =~ ^[0-9]+$ ]] \
    || fail "не удалось прочитать фактуру на границе ${boundaries[index]}"
  (( context_crossing_visits == crossing_visits )) \
    || fail "STG и DDS нашли разное число переходящих визитов на ${boundaries[index]}"
  if (( crossing_visits == 0 )); then
    echo "boundary ${index}/${boundary_count}: crossing_visits=0 — однородность неприменима"
  else
    (( per_event_homogeneous_visits == crossing_visits )) \
      || fail "browser/referer/utm меняются на границе ${boundaries[index]}: ${per_event_homogeneous_visits}/${crossing_visits}"
    echo "boundary ${index}/${boundary_count}: crossing_visits=${crossing_visits} — однородность подтверждена"
  fi

  echo "boundary_${index}=${boundaries[index]}"
  echo "boundary_${index}_crossing_visits=${crossing_visits}"
  echo "boundary_${index}_unpaired_location_rows=${unpaired_location_rows}"
  echo "boundary_${index}_unpaired_device_rows=${unpaired_device_rows}"
  echo "boundary_${index}_unpaired_geo_rows=${unpaired_geo_rows}"
done
