"""
Flask backend — Карта шумового загрязнения (прототип v0.1)

Запуск:       python app.py
Порт:         5555 (переопределить: PORT=8080 python app.py)
Документация: /api/help
Калибровка:   noise_model.py → NOISE_ZONES

Зависимости:  flask, requests, python-dotenv
API ключ:     .env → FLIGHTRADAR24_API_KEY (без ключа — demo-режим)
"""

import os
import json
import gzip
import time
import logging
from pathlib import Path
from datetime import datetime, timezone

import orjson
from flask import Flask, jsonify, render_template_string, request, send_from_directory, Response
from dotenv import load_dotenv

from noise_model import process_flight_for_map, get_noise_config, calc_zone_radii_for_point
from density_model import compute_density
from air_corridors_model import compute_corridors
from flask_compress import Compress

load_dotenv()
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

app = Flask(__name__, static_folder="static")
Compress(app)

# ─────────────────────────────────────────────────────
# In-memory кэш данных полётов
# ─────────────────────────────────────────────────────

_flights_cache = None             # глобальный кэш сырых данных: {"flights": [...], "airports": {...}, ...}
_flights_normalized_cache = None  # кэш нормализованных рейсов (list of dicts)
_flights_response_gz = None       # pre-serialized gzip JSON байты для быстрой отдачи без фильтров
_flights_response_plain = None    # pre-serialized plain JSON байты (fallback)

# ─────────────────────────────────────────────────────
# Загрузка данных
# ─────────────────────────────────────────────────────

DATA_DIR = Path("data")
DATA_DIR.mkdir(exist_ok=True)
SAMPLE_DATA_FILE = DATA_DIR / "sample_flights.json"


AIRPORTS = {
    "SVO": {"lat": 55.9726, "lon": 37.4146, "name": "Шереметьево"},
    "DME": {"lat": 55.4088, "lon": 37.9063, "name": "Домодедово"},
    "VKO": {"lat": 55.5914, "lon": 37.2615, "name": "Внуково"},
    "ZIA": {"lat": 55.5531, "lon": 38.1500, "name": "Жуковский"},
}

def get_available_dates() -> list:
    """Список уникальных дат из всех файлов flights_*.json (сортировка по возрастанию)"""
    dates = set()
    for fp in DATA_DIR.glob("flights_*.json"):
        try:
            with open(fp, encoding="utf-8") as f:
                d = json.load(f)
            date = d.get("date", "")
            if date and d.get("flights"):  # только файлы с данными
                dates.add(date)
        except Exception:
            pass
    return sorted(dates)

def load_flight_data(date_from: str = None, date_to: str = None):
    """
    Загрузка данных рейсов с опциональным фильтром по датам.
    date_from / date_to — строки формата YYYY-MM-DD включительно.
    """
    airport_files = list(DATA_DIR.glob("flights_*.json"))

    if airport_files:
        all_flights = []
        for fp in sorted(airport_files):
            try:
                with open(fp, encoding="utf-8") as f:
                    d = json.load(f)
                file_date = d.get("date", "")
                # Фильтр по дате файла
                if date_from and file_date and file_date < date_from:
                    continue
                if date_to and file_date and file_date > date_to:
                    continue
                file_date = d.get("date", "")
                for fl in d.get("flights", []):
                    if not fl.get("date"):
                        fl["date"] = file_date
                    all_flights.append(fl)
            except Exception as e:
                logger.warning(f"Ошибка чтения {fp}: {e}")
        return {"flights": all_flights, "airports": AIRPORTS}

    # Fallback — единый файл
    if SAMPLE_DATA_FILE.exists():
        with open(SAMPLE_DATA_FILE, encoding="utf-8") as f:
            return json.load(f)
    return None


def _load_all_flights():
    """
    Загружает все данные рейсов с диска, нормализует и сохраняет в глобальный кэш.
    Также пре-сериализует полный ответ в gzip JSON для быстрой отдачи без фильтров.
    Вызывается при старте Flask и при ?refresh=1.
    """
    global _flights_cache, _flights_normalized_cache, _flights_response_gz, _flights_response_plain
    logger.info("📦 Загрузка данных полётов в кэш...")
    t0 = time.time()
    _flights_cache = load_flight_data()
    t1 = time.time()
    logger.info(f"   Файлы прочитаны за {t1 - t0:.2f} сек")

    if _flights_cache:
        logger.info("⚙️  Нормализация рейсов (вычисление шумовых данных)...")
        normalized = [normalize_flight_for_map(f) for f in _flights_cache.get("flights", [])]
        _flights_normalized_cache = normalized
        t2 = time.time()
        logger.info(f"   Нормализовано {len(normalized)} рейсов за {t2 - t1:.2f} сек")

        # Пре-сериализация полного ответа (без фильтров) для быстрой отдачи
        logger.info("🗜️  Пре-сериализация и сжатие ответа...")
        full_response = {
            "flights": normalized,
            "count": len(normalized),
            "filters": {"limit": len(normalized), "min_alt": 0, "max_alt": 99999, "type": "all"},
            "data_source": "sample" if not os.getenv("FLIGHTRADAR24_API_KEY") else "api",
            "generated_at": _flights_cache.get("generated_at", ""),
        }
        plain_bytes = orjson.dumps(full_response)
        _flights_response_plain = plain_bytes
        _flights_response_gz = gzip.compress(plain_bytes, compresslevel=1)  # level=1 — быстро
        t3 = time.time()
        logger.info(
            f"✅ Кэш готов: {len(normalized)} рейсов | "
            f"plain={len(plain_bytes)//1024}KB gz={len(_flights_response_gz)//1024}KB | "
            f"итого {t3 - t0:.2f} сек"
        )

    return _flights_cache


def normalize_flight_for_map(flight: dict) -> dict:
    """Нормализует рейс с точками трека для отображения на карте"""
    result = {
        "id": flight.get("id", ""),
        "callsign": flight.get("callsign", ""),
        "flight_number": flight.get("flight_number", ""),
        "aircraft_type": flight.get("aircraft_type", ""),
        "airline": flight.get("airline", ""),
        "origin": flight.get("origin", ""),
        "destination": flight.get("destination", ""),
        "registration": flight.get("registration", ""),
        "type": flight.get("type", ""),
        "date": flight.get("date", ""),
        "dep_scheduled": flight.get("dep_scheduled", ""),
        "points": [],
    }

    for point in flight.get("points", []):
        alt_ft = point.get("altitude", 0)
        alt_m  = round(alt_ft / 3.28084)
        processed = process_flight_for_map({
            "altitude": alt_ft,
            "aircraft_type": flight.get("aircraft_type", "default"),
        })
        result["points"].append({
            "lat":        point["lat"],
            "lon":        point["lon"],
            "altitude":   alt_ft,
            "altitude_m": alt_m,
            "speed":      point.get("speed", 0),
            "heading":    point.get("heading", 0),
            "timestamp":  point.get("timestamp", ""),
            "noise_db":   processed["noise_db"],
            "noise_color": processed["noise_color"],
            "noise_level": processed["noise_level"],
            # Горизонтальные радиусы зон для данной высоты (теорема Пифагора)
            # d_inner/d_outer — реальные км на земле
            "zone_radii": calc_zone_radii_for_point(alt_m),
        })

    # Для отображения — берём самую «шумную» точку (самую низкую по высоте)
    if result["points"]:
        min_alt_point = min(result["points"], key=lambda p: p["altitude"] if p["altitude"] > 0 else 99999)
        result["max_noise_db"] = min_alt_point["noise_db"]
        result["max_noise_level"] = min_alt_point["noise_level"]

    return result


# ─────────────────────────────────────────────────────
# API Endpoints
# ─────────────────────────────────────────────────────

@app.route("/")
def index():
    """Главная страница с картой"""
    return send_from_directory(".", "index.html")


@app.route("/api/flights", methods=["GET"])
def get_flights():
    """
    Получить список рейсов с шумовыми характеристиками

    Query params:
      - limit: int (default 50)
      - min_alt: int (фильтр по минимальной высоте)
      - max_alt: int (фильтр по максимальной высоте)
      - type: departure|arrival|all (default all)
    """
    global _flights_cache, _flights_normalized_cache, _flights_response_gz, _flights_response_plain

    limit     = request.args.get("limit", None)
    min_alt   = int(request.args.get("min_alt", 0))
    max_alt   = int(request.args.get("max_alt", 99999))
    flight_type = request.args.get("type", "all")
    airport   = request.args.get("airport", "all")  # SVO / DME / VKO / all
    date_from = request.args.get("date_from", None)
    date_to   = request.args.get("date_to",   None)
    refresh   = request.args.get("refresh", "0") == "1"

    # Принудительное обновление кэша
    if refresh or _flights_cache is None or _flights_normalized_cache is None:
        _load_all_flights()

    if not _flights_cache:
        return jsonify({"error": "Данные не загружены. Запустите generate_sample_data.py"}), 404

    # ── Быстрый путь: нет фильтров → отдаём пре-сериализованный gzip кэш ──
    no_filters = (
        limit is None
        and min_alt == 0
        and max_alt >= 99999
        and flight_type == "all"
        and airport == "all"
        and date_from is None
        and date_to is None
    )
    if no_filters and _flights_response_gz is not None:
        accept_enc = request.headers.get("Accept-Encoding", "")
        if "gzip" in accept_enc:
            return Response(
                _flights_response_gz,
                status=200,
                headers={
                    "Content-Type": "application/json",
                    "Content-Encoding": "gzip",
                    "Cache-Control": "no-cache",
                },
            )
        else:
            return Response(
                _flights_response_plain,
                status=200,
                headers={"Content-Type": "application/json"},
            )

    # ── Путь с фильтрами ──
    limit_int = int(limit) if limit is not None else len(_flights_normalized_cache)

    # Фильтр по дате — fallback на диск (нечастый сценарий)
    if date_from or date_to:
        data = load_flight_data(date_from=date_from, date_to=date_to)
        if not data:
            return jsonify({"error": "Данные не загружены"}), 404
        raw_flights = data.get("flights", [])
        if flight_type != "all":
            raw_flights = [f for f in raw_flights if f.get("type") == flight_type]
        if airport != "all":
            raw_flights = [f for f in raw_flights
                           if f.get("orig_icao") == f"UU{airport[1:]}" or
                              f.get("dest_icao") == f"UU{airport[1:]}" or
                              f.get("airport") == airport]
        result = []
        for flight in raw_flights[:limit_int]:
            normalized = normalize_flight_for_map(flight)
            normalized["points"] = [
                p for p in normalized["points"]
                if min_alt <= p["altitude"] <= max_alt
            ]
            if normalized["points"]:
                result.append(normalized)
        return Response(
            orjson.dumps({
                "flights": result,
                "count": len(result),
                "filters": {"limit": limit_int, "min_alt": min_alt, "max_alt": max_alt, "type": flight_type},
                "data_source": "sample" if not os.getenv("FLIGHTRADAR24_API_KEY") else "api",
                "generated_at": data.get("generated_at", ""),
            }),
            status=200,
            headers={"Content-Type": "application/json"},
        )

    # Фильтрация нормализованного кэша
    flights = _flights_normalized_cache

    if flight_type != "all":
        flights = [f for f in flights if f.get("type") == flight_type]

    if airport != "all":
        flights = [f for f in flights
                   if f.get("origin") == airport or f.get("destination") == airport]

    result = []
    for flight in flights[:limit_int]:
        filtered_points = [
            p for p in flight["points"]
            if min_alt <= p["altitude"] <= max_alt
        ]
        if filtered_points:
            result.append({**flight, "points": filtered_points})

    return Response(
        orjson.dumps({
            "flights": result,
            "count": len(result),
            "filters": {
                "limit": limit_int,
                "min_alt": min_alt,
                "max_alt": max_alt,
                "type": flight_type,
            },
            "data_source": "sample" if not os.getenv("FLIGHTRADAR24_API_KEY") else "api",
            "generated_at": _flights_cache.get("generated_at", ""),
        }),
        status=200,
        headers={"Content-Type": "application/json"},
    )


@app.route("/api/airports", methods=["GET"])
def get_airports():
    """Список аэропортов в регионе"""
    data = _flights_cache if _flights_cache is not None else load_flight_data()
    airports = data.get("airports", {}) if data else {}
    return jsonify({"airports": airports})


@app.route("/api/stats", methods=["GET"])
def get_stats():
    """Статистика по загруженным данным"""
    data = _flights_cache if _flights_cache is not None else load_flight_data()
    if not data:
        return jsonify({"error": "Данные не загружены"}), 404

    flights = data.get("flights", [])
    all_points = [p for f in flights for p in f.get("points", [])]

    # Подсчёт шумовых зон по уровням
    noise_counts = {"Критический": 0, "Высокий": 0, "Средний": 0, "Низкий": 0}
    for point in all_points:
        alt = point.get("altitude", 0)
        if alt < 3000:
            noise_counts["Критический"] += 1
        elif alt < 10000:
            noise_counts["Высокий"] += 1
        elif alt < 25000:
            noise_counts["Средний"] += 1
        else:
            noise_counts["Низкий"] += 1

    return jsonify({
        "flights_total": len(flights),
        "departures": sum(1 for f in flights if f.get("type") == "departure"),
        "arrivals": sum(1 for f in flights if f.get("type") == "arrival"),
        "points_total": len(all_points),
        "noise_distribution": noise_counts,
        "region": data.get("region", ""),
        "data_source": "demo" if not os.getenv("FLIGHTRADAR24_API_KEY") else "api",
    })


@app.route("/api/usage", methods=["GET"])
def get_api_usage():
    """Использование кредитов API"""
    api_key = os.getenv("FLIGHTRADAR24_API_KEY")
    if not api_key:
        return jsonify({"mode": "demo", "message": "API ключ не настроен, используются тестовые данные"})

    try:
        from fr24_client import FR24Client
        client = FR24Client(api_key)
        usage = client.get_usage()
        return jsonify(usage)
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/live", methods=["GET"])
def get_live_flights():
    """Live позиции самолётов (требует API ключ)"""
    api_key = os.getenv("FLIGHTRADAR24_API_KEY")
    if not api_key:
        return jsonify({"error": "Требуется API ключ для live данных"}), 403

    try:
        from fr24_client import FR24Client
        client = FR24Client(api_key)
        data = client.get_live_flights()
        flights = client._normalize_flights(data)

        result = [process_flight_for_map(f) for f in flights]
        return jsonify({"flights": result, "count": len(result), "mode": "live"})
    except Exception as e:
        logger.error(f"Live data error: {e}")
        return jsonify({"error": str(e)}), 500


@app.route("/api/noise-config", methods=["GET"])
def get_noise_config_endpoint():
    """
    Параметры модели шума — зоны и высотные коэффициенты.
    Фронтенд читает этот endpoint при старте и строит зоны по этим данным.
    Для калибровки редактируй noise_model.py (раздел КАЛИБРОВОЧНЫЕ ПАРАМЕТРЫ).
    """
    return jsonify(get_noise_config())


@app.route("/api/dates", methods=["GET"])
def get_dates():
    """Список доступных дат с данными"""
    dates = get_available_dates()
    return jsonify({"dates": dates, "count": len(dates)})


@app.route("/api/density", methods=["GET"])
def get_density():
    """
    Сетка плотности пролётов над регионом.

    Query params:
      - refresh=1  принудительный пересчёт (игнорировать кэш)
    """
    import gzip as gzip_module
    date_from = request.args.get("date_from", None)
    date_to   = request.args.get("date_to",   None)
    refresh   = request.args.get("refresh", "0") == "1"
    cache_max_age_sec = 3600  # 1 час

    # Ключ кэша зависит от диапазона дат
    cache_key  = f"{date_from or 'all'}_{date_to or 'all'}"
    cache_file    = DATA_DIR / f"density_cache_{cache_key}.json"
    cache_gz_file = DATA_DIR / f"density_cache_{cache_key}.json.gz"

    def rebuild():
        result = compute_density(DATA_DIR, date_from=date_from, date_to=date_to)
        raw = json.dumps(result, ensure_ascii=False).encode("utf-8")
        # Сохраняем plain и gzip
        with open(cache_file, "wb") as f:
            f.write(raw)
        with gzip_module.open(cache_gz_file, "wb") as f:
            f.write(raw)
        return raw

    # Проверяем кэш
    if not refresh and cache_gz_file.exists():
        age = time.time() - cache_gz_file.stat().st_mtime
        if age < cache_max_age_sec:
            # Отдаём gzip напрямую
            accept_enc = request.headers.get("Accept-Encoding", "")
            if "gzip" in accept_enc:
                with open(cache_gz_file, "rb") as f:
                    gz_data = f.read()
                from flask import Response
                return Response(gz_data, status=200,
                    headers={
                        "Content-Type": "application/json",
                        "Content-Encoding": "gzip",
                        "Cache-Control": "no-cache",
                    })
            else:
                with open(cache_file, "rb") as f:
                    return Response(f.read(), status=200,
                        headers={"Content-Type": "application/json"})

    raw = rebuild()
    accept_enc = request.headers.get("Accept-Encoding", "")
    if "gzip" in accept_enc:
        with open(cache_gz_file, "rb") as f:
            gz_data = f.read()
        from flask import Response
        return Response(gz_data, status=200,
            headers={
                "Content-Type": "application/json",
                "Content-Encoding": "gzip",
                "Cache-Control": "no-cache",
            })
    return Response(raw, status=200, headers={"Content-Type": "application/json"})


@app.route("/api/help", methods=["GET"])
def api_help():
    """Документация API"""
    return jsonify({
        "endpoints": {
            "GET /": "Карта шумового загрязнения (веб-интерфейс)",
            "GET /api/flights": "Список рейсов с шумовыми данными",
            "GET /api/airports": "Аэропорты региона",
            "GET /api/stats": "Статистика по данным",
            "GET /api/usage": "Использование кредитов API",
            "GET /api/live": "Live позиции (требует API ключ)",
        },
        "flight_filters": {
            "limit": "Максимальное количество рейсов (default: 50)",
            "min_alt": "Минимальная высота в футах (default: 0)",
            "max_alt": "Максимальная высота в футах (default: 50000)",
            "type": "Тип: departure|arrival|all (default: all)",
        },
        "noise_model": {
            "description": "Уровень шума обратно пропорционален высоте",
            "levels": {
                "Критический": "< 3000 ft",
                "Высокий": "3000–10000 ft",
                "Средний": "10000–25000 ft",
                "Низкий": "> 25000 ft",
            },
        },
    })


@app.route("/api/air-corridors", methods=["GET"])
def api_air_corridors():
    """
    Воздушные коридоры — полосы реального разброса треков по маршруту.

    Query params:
      - date_from  — YYYY-MM-DD (опционально)
      - date_to    — YYYY-MM-DD (опционально)
      - refresh=1  — принудительный пересчёт кэша
    """
    import gzip as gzip_module

    date_from = request.args.get("date_from", None)
    date_to   = request.args.get("date_to",   None)
    refresh   = request.args.get("refresh", "0") == "1"
    cache_max_age_sec = 3600  # 1 час

    cache_key     = f"{date_from or 'all'}_{date_to or 'all'}"
    cache_file    = DATA_DIR / f"air_corridors_{cache_key}.json"
    cache_gz_file = DATA_DIR / f"air_corridors_{cache_key}.json.gz"

    def rebuild():
        data = load_flight_data(date_from=date_from, date_to=date_to)
        flights = data.get("flights", []) if data else []
        corridors = compute_corridors(flights)

        result = {
            "corridors": corridors,
            "total_corridors": len(corridors),
            "flights_analyzed": len(flights),
            "date_from": date_from or "",
            "date_to": date_to or "",
            "generated_at": datetime.now(timezone.utc).isoformat(),
        }
        raw = json.dumps(result, ensure_ascii=False).encode("utf-8")
        with open(cache_file, "wb") as f:
            f.write(raw)
        with gzip_module.open(cache_gz_file, "wb") as f:
            f.write(raw)
        return raw

    # Проверяем кэш
    if not refresh and cache_gz_file.exists():
        age = time.time() - cache_gz_file.stat().st_mtime
        if age < cache_max_age_sec:
            accept_enc = request.headers.get("Accept-Encoding", "")
            if "gzip" in accept_enc:
                with open(cache_gz_file, "rb") as f:
                    gz_data = f.read()
                return Response(gz_data, status=200,
                    headers={
                        "Content-Type": "application/json",
                        "Content-Encoding": "gzip",
                        "Cache-Control": "no-cache",
                    })
            else:
                with open(cache_file, "rb") as f:
                    return Response(f.read(), status=200,
                        headers={"Content-Type": "application/json"})

    raw = rebuild()
    accept_enc = request.headers.get("Accept-Encoding", "")
    if "gzip" in accept_enc:
        with open(cache_gz_file, "rb") as f:
            gz_data = f.read()
        return Response(gz_data, status=200,
            headers={
                "Content-Type": "application/json",
                "Content-Encoding": "gzip",
                "Cache-Control": "no-cache",
            })
    return Response(raw, status=200, headers={"Content-Type": "application/json"})


if __name__ == "__main__":
    port = int(os.getenv("PORT", 5555))
    debug = os.getenv("DEBUG", "true").lower() == "true"
    logger.info(f"🚀 Запуск сервера на http://localhost:{port}")
    logger.info(f"   API ключ: {'настроен' if os.getenv('FLIGHTRADAR24_API_KEY') else 'НЕ настроен (demo режим)'}")
    # Предзагрузка данных в кэш до старта сервера
    _load_all_flights()
    app.run(host="0.0.0.0", port=port, debug=debug)
