"""Чистая логика пульта Airflow для стартовой истории."""

from __future__ import annotations

import os
import urllib.error
import urllib.request
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterator

from clickstream_generator.config import Config
from clickstream_generator.kafka_io import (
    KafkaStateManager,
    KafkaStartupHistoryManifest,
    ensure_topics,
)
from clickstream_generator.launch import build_launch_env
from clickstream_generator.service import GeneratorService
from clickstream_generator.startup_history_artifact import (
    KafkaRawPublisher,
    KafkaTopicInspector,
    ManifestCounters,
    compare_clickhouse_stats_to_manifest,
    cumulative_counter_reference,
    import_startup_history_artifact,
    known_counter_ids_from_state,
    load_startup_history_artifact,
    manifest_boundaries,
    validate_startup_history_artifact,
    validate_manifest_state,
)
from clickstream_generator.stand_clean import (
    STG_EMPTY_SQL,
    assert_kafka_data_topics_empty,
    assert_stg_counts_empty,
)


AIRFLOW_DATA_DIR = "/opt/airflow/data"
KAFKA_BOOTSTRAP_SERVERS = "kafka:29092"
GENERATOR_METRICS_URL = "http://generator:9109/metrics"

WORLD_OVERRIDE_KEYS = {
    "GEN_SEED",
    "GEN_MODEL_T0",
    "GEN_MODEL_TIMEZONE",
    "GEN_MODEL_TIME_SPEED",
    "GEN_TICK_SECONDS",
    "GEN_LAMBDA_BASE_PER_MIN",
    "GEN_JITTER_PCT",
    "GEN_MIN_EVENTS_PER_TICK",
    "GEN_MAX_EVENTS_PER_TICK",
}

NEXT_DAY_SETTING_ENV_KEYS = {
    "tick_seconds": "GEN_TICK_SECONDS",
    "lambda_base_per_min": "GEN_LAMBDA_BASE_PER_MIN",
    "jitter_pct": "GEN_JITTER_PCT",
    "min_events_per_tick": "GEN_MIN_EVENTS_PER_TICK",
    "max_events_per_tick": "GEN_MAX_EVENTS_PER_TICK",
    "max_session_events": "GEN_MAX_SESSION_EVENTS",
    "max_active_sessions": "GEN_MAX_ACTIVE_SESSIONS",
    "population_max": "GEN_POPULATION_MAX",
    "p_new_user": "GEN_P_NEW_USER",
    "min_return_minutes": "GEN_MIN_RETURN_MINUTES",
    "model_time_speed": "GEN_MODEL_TIME_SPEED",
}

CLICKHOUSE_STATS_SQL = """
WITH
    toDateTime64('{model_t0}', 6) AS t0,
    toDateTime64('{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_timestamp,
    toString(max(event_ts)) AS max_event_timestamp
FROM dm.v_events_enriched
WHERE event_ts >= t0 AND event_ts < t_end
"""


def build_control_env(
    operation: str,
    *,
    profile_name: str,
    duration: str | None = None,
    artifact_path: str | None = None,
    overrides: dict[str, str] | None = None,
) -> dict[str, str]:
    """Готовит env для операции пульта без доступа к Docker."""
    if operation not in {"backfill", "import"}:
        raise ValueError("operation must be backfill or import")
    if operation == "import" and not artifact_path:
        raise ValueError("artifact_path is required for import")

    selected_overrides = {
        key: value
        for key, value in (overrides or {}).items()
        if key in WORLD_OVERRIDE_KEYS and value != ""
    }
    env = build_launch_env(
        "backfill",
        profile_name=profile_name,
        duration=duration or None,
        overrides=selected_overrides,
    )
    env["KAFKA_BOOTSTRAP_SERVERS"] = KAFKA_BOOTSTRAP_SERVERS
    env["GEN_DATA_DIR"] = AIRFLOW_DATA_DIR
    env["GEN_METRICS_ENABLED"] = "false"
    if artifact_path:
        env["GEN_STARTUP_HISTORY_ARTIFACT"] = artifact_path
    return env


def build_next_day_env(manifest: dict) -> dict[str, str]:
    """Восстанавливает окружение следующего дня из manifest мира."""
    manifest_boundaries(manifest)
    settings = manifest.get("generation_settings") or {}
    missing = [key for key in NEXT_DAY_SETTING_ENV_KEYS if key not in settings]
    if missing:
        raise ValueError(
            "в manifest не хватает generation_settings: "
            + ", ".join(sorted(missing))
        )
    overrides = {
        "GEN_SEED": str(manifest["gen_seed"]),
        "GEN_MODEL_T0": str(manifest["model_t0"]),
        "GEN_MODEL_T_END": str(manifest["model_t_end"]),
        "GEN_MODEL_TIMEZONE": str(manifest["model_timezone"]),
    }
    env = build_launch_env(
        "next-day",
        profile_name=str(manifest.get("launch_profile") or "ci"),
        overrides=overrides,
    )
    for setting, env_key in NEXT_DAY_SETTING_ENV_KEYS.items():
        env[env_key] = str(settings[setting])
    env["KAFKA_BOOTSTRAP_SERVERS"] = KAFKA_BOOTSTRAP_SERVERS
    env["GEN_DATA_DIR"] = AIRFLOW_DATA_DIR
    env["GEN_METRICS_ENABLED"] = "false"
    return env


def assert_expected_t_end(expected_t_end: str, actual_t_end: str) -> None:
    """Отвергает повторный запуск от уже сдвинутой границы."""
    if not expected_t_end:
        return
    try:
        expected = _parse_utc_timestamp(expected_t_end)
        actual = _parse_utc_timestamp(actual_t_end)
    except ValueError as exc:
        raise RuntimeError(f"Некорректная граница next-day: {exc}") from exc
    if expected != actual:
        raise RuntimeError(
            "Граница next-day изменилась: "
            f"ожидалась {expected_t_end}, фактическая {actual_t_end}."
        )


def assert_next_day_snapshot(manifest: dict | None, state) -> None:
    """Проверяет наличие и согласованность точки продолжения next-day."""
    if not manifest:
        raise RuntimeError("Manifest стартовой истории не найден в Kafka.")
    if state is None:
        raise RuntimeError("State генератора не найден в Kafka.")
    try:
        manifest_boundaries(manifest)
        validate_manifest_state(manifest, state)
    except (KeyError, TypeError, ValueError) as exc:
        raise RuntimeError(
            f"state не согласован с T_end manifest: {exc}"
        ) from exc
    counter_reference = state.cumulative_manifest_counters
    counter_state = manifest.get("cumulative_manifest_counters")
    if counter_reference is None or counter_state is None:
        raise RuntimeError(
            "В state нет накопительных счётчиков manifest; "
            "повторно запустите import эталонного мира."
        )
    if cumulative_counter_reference(counter_state) != counter_reference:
        raise RuntimeError(
            "Накопительные счётчики state и manifest не совпадают; "
            "повторно запустите import эталонного мира."
        )
    known_click_ids, known_user_ids = known_counter_ids_from_state(state)
    try:
        counters = ManifestCounters.from_state(
            counter_state,
            known_click_ids=known_click_ids,
            known_user_ids=known_user_ids,
        )
    except ValueError as exc:
        raise RuntimeError(
            "Накопительные счётчики manifest повреждены; "
            "повторно запустите import эталонного мира."
        ) from exc
    if (
        counters.to_manifest_topics() != manifest.get("topics")
        or counters.to_manifest_totals() != manifest.get("totals")
    ):
        raise RuntimeError(
            "Накопительные счётчики не совпадают с числами manifest; "
            "повторно запустите import эталонного мира."
        )


def _parse_utc_timestamp(value: str):
    timestamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
    if timestamp.tzinfo is None:
        raise ValueError("время должно содержать часовой пояс")
    return timestamp.astimezone(timezone.utc)


def assert_stand_clean(kafka_bootstrap_servers: str, clickhouse_hook) -> None:
    """Проверяет, что backfill/import не смешает миры."""
    assert_kafka_data_topics_empty(kafka_bootstrap_servers)
    assert_stg_tables_empty(clickhouse_hook)


def assert_stg_tables_empty(clickhouse_hook) -> None:
    """Падает, если в STG уже есть строки."""
    result = clickhouse_hook.execute(STG_EMPTY_SQL)
    counts = result[0] if result else ()
    assert_stg_counts_empty(counts)


def assert_live_generator_not_running(url: str = GENERATOR_METRICS_URL) -> None:
    """Мягко предупреждает о live-сервисе по HTTP-метрикам."""
    _assert_metrics_not_responding(
        url,
        "Live-генератор отвечает на metrics-порту. Остановите его с консоли "
        "перед backfill/import, чтобы не смешать миры.",
    )


def assert_live_generator_not_running_for_next_day(
    url: str = GENERATOR_METRICS_URL,
) -> None:
    """Падает, если next-day может смешаться с работающим live-потоком."""
    _assert_metrics_not_responding(
        url,
        "Live-генератор отвечает на metrics-порту. Остановите его с консоли "
        "перед next-day, чтобы не смешать дни.",
    )


def _assert_metrics_not_responding(url: str, error_message: str) -> None:
    """Проверяет, что HTTP-метрики live-генератора недоступны."""
    try:
        urllib.request.urlopen(url, timeout=2).close()
    except (urllib.error.URLError, TimeoutError, OSError):
        return
    raise RuntimeError(error_message)


def run_backfill(env: dict[str, str]) -> None:
    """Запускает backfill в процессе Airflow worker."""
    with _patched_environ(env):
        config = Config()
        GeneratorService(config).start()


def run_next_day(env: dict[str, str]) -> None:
    """Запускает ограниченную доливку следующего дня в Airflow worker."""
    with _patched_environ(env):
        config = Config()
        GeneratorService(config).start()


def run_import(env: dict[str, str], artifact_path: str) -> dict:
    """Импортирует портативный артефакт в Kafka."""
    with _patched_environ(env):
        config = Config()
        artifact = load_startup_history_artifact(artifact_path)
        ensure_topics(config.kafka_bootstrap_servers)
        publisher = KafkaRawPublisher(config.kafka_bootstrap_servers)
        state_manager = KafkaStateManager(config.kafka_bootstrap_servers)
        manifest_manager = KafkaStartupHistoryManifest(config.kafka_bootstrap_servers)
        topic_inspector = KafkaTopicInspector(config.kafka_bootstrap_servers)
        try:
            return import_startup_history_artifact(
                artifact,
                publisher=publisher,
                state_manager=state_manager,
                manifest_manager=manifest_manager,
                expected_config=config,
                topic_inspector=topic_inspector,
            )
        finally:
            publisher.close()
            state_manager.close()
            manifest_manager.close()


def validate_import_artifact(env: dict[str, str], artifact_path: str) -> None:
    """Проверяет артефакт и настройки import до записи в Kafka."""
    with _patched_environ(env):
        config = Config()
        artifact = load_startup_history_artifact(artifact_path)
        validate_startup_history_artifact(artifact, expected_config=config)


def load_manifest_from_kafka(
    kafka_bootstrap_servers: str = KAFKA_BOOTSTRAP_SERVERS,
) -> dict:
    """Читает manifest стартовой истории из compact-топика."""
    manager = KafkaStartupHistoryManifest(kafka_bootstrap_servers)
    try:
        manifest = manager.load()
    finally:
        manager.close()
    if not manifest:
        raise RuntimeError("Manifest стартовой истории не найден в Kafka.")
    return manifest


def load_state_from_kafka(
    kafka_bootstrap_servers: str = KAFKA_BOOTSTRAP_SERVERS,
):
    """Читает state генератора из compact-топика."""
    manager = KafkaStateManager(kafka_bootstrap_servers)
    try:
        return manager.load()
    finally:
        manager.close()


def assert_clickhouse_matches_manifest(manifest: dict, clickhouse_hook) -> None:
    """Сверяет контрольные числа ClickHouse с manifest."""
    model_t0 = _clickhouse_datetime_literal(str(manifest["model_t0"]))
    model_t_end = _clickhouse_datetime_literal(str(manifest["model_t_end"]))
    result = clickhouse_hook.execute(
        CLICKHOUSE_STATS_SQL.format(model_t0=model_t0, model_t_end=model_t_end)
    )
    if not result:
        raise RuntimeError("ClickHouse не вернул контрольные числа.")
    row = result[0]
    stats = {
        "events": str(row[0]),
        "visits": str(row[1]),
        "users": str(row[2]),
        "min_event_timestamp": str(row[3]),
        "max_event_timestamp": str(row[4]),
    }
    mismatches = compare_clickhouse_stats_to_manifest(manifest, stats)
    if mismatches:
        raise RuntimeError(
            "ClickHouse расходится с manifest: " + ", ".join(mismatches)
        )


def target_dag_trigger_error(dag_id: str, dag_model) -> str | None:
    """Возвращает причину, почему зависимый DAG нельзя запускать."""
    if dag_model is None:
        return (
            f"DAG {dag_id} ещё не найден Airflow. Подождите парсинга DAG-файлов "
            "или перезапустите Airflow через make up."
        )
    if dag_model.is_paused:
        return (
            f"DAG {dag_id} стоит на паузе: снимите паузу в Airflow UI, "
            "затем повторите запуск DAG."
        )
    return None


def default_artifact_path(operation: str) -> str:
    """Возвращает путь артефакта по умолчанию в общем томе data."""
    if operation == "import":
        return str(
            Path(AIRFLOW_DATA_DIR)
            / "startup_history"
            / "reference-world.json.xz"
        )
    return str(Path(AIRFLOW_DATA_DIR) / "startup-history.json")


@contextmanager
def _patched_environ(env: dict[str, str]) -> Iterator[None]:
    old_values = {key: os.environ.get(key) for key in env}
    os.environ.update(env)
    try:
        yield
    finally:
        for key, value in old_values.items():
            if value is None:
                os.environ.pop(key, None)
            else:
                os.environ[key] = value


def _clickhouse_datetime_literal(value: str) -> str:
    normalized = value.replace("T", " ").removesuffix("Z")
    if len(normalized) >= 6 and normalized[-6] in "+-" and normalized[-3] == ":":
        normalized = normalized[:-6]
    return normalized
