"""
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 math
import logging
import hashlib
from pathlib import Path
from datetime import datetime, timezone
from functools import lru_cache

import orjson
import psycopg2
import psycopg2.extras
from flask import Flask, jsonify, render_template_string, request, send_from_directory, Response
from dotenv import load_dotenv
from shapely.geometry import LineString

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()


def get_db_conn():
    """Подключение к PostgreSQL (fr24 БД)"""
    return psycopg2.connect(
        host=os.getenv("DB_HOST", "localhost"),
        port=int(os.getenv("DB_PORT", 5432)),
        dbname=os.getenv("DB_NAME", "fr24"),
        user=os.getenv("DB_USER", "fr24"),
        password=os.getenv("DB_PASSWORD", "change-me"),
    )

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"})


@app.route("/api/tracks", methods=["GET"])
def get_tracks():
    """Треки через fr24-api (proxy)"""
    import requests as req
    date_filter = request.args.get("date")
    limit = min(int(request.args.get("limit", 500)), 2000)

    fr24_api_base = os.getenv("FR24_API_BASE", "http://192.168.2.67:8080")
    params = {"limit": limit}
    if date_filter:
        params["date"] = date_filter
    else:
        params["minutes"] = 4320  # 3 days

    try:
        resp = req.get(f"{fr24_api_base}/api/tracks", params=params, timeout=10)
        resp.raise_for_status()
        data = resp.json()
        # fr24-api возвращает {ok: true, data: {type: FeatureCollection, ...}}
        geojson = data.get("data", data)
        return jsonify(geojson)
    except Exception as e:
        logger.error(f"/api/tracks proxy error: {e}")
        return jsonify({"error": str(e), "type": "FeatureCollection", "features": []}), 500


# ─────────────────────────────────────────────────────
# Tile endpoint — GeoJSON с LRU-кэшем и упрощением по зуму
# ─────────────────────────────────────────────────────

# --- LRU tile cache (в памяти, max 512 тайлов) ---
_tile_lru_cache: dict[tuple, bytes] = {}  # (z, x, y) → gzip-байты
def _tile_cache_lru(maxsize: int = 512):
    return _tile_lru_cache

def _tile_cache_get(z: int, x: int, y: int):
    key = (z, x, y)
    if key in _tile_lru_cache:
        # LRU: переместить в конец (access order)
        val = _tile_lru_cache.pop(key)
        _tile_lru_cache[key] = val
        return val
    return None

def _tile_cache_put(z: int, x: int, y: int, data: bytes, maxsize: int = 512):
    key = (z, x, y)
    if key in _tile_lru_cache:
        del _tile_lru_cache[key]
    # Удаляем самый старый если достигли лимита
    while len(_tile_lru_cache) >= maxsize:
        _tile_lru_cache.pop(next(iter(_tile_lru_cache)))
    _tile_lru_cache[key] = data


def _get_simplify_tolerance(z: int) -> float:
    """Tolerance для simplify (в градусах) по зуму.

    z < 7  — сильное упрощение
    z 7–9  — умеренное
    z >= 10 — лёгкое
    """
    if z >= 10:
        return 0.0005        # ~50 m — почти без изменений
    elif z >= 7:
        return 0.002 + (9 - z) * 0.001   # 0.002–0.005 (~200–500 m)
    elif z >= 5:
        return 0.01 + (7 - z) * 0.015   # 0.01–0.04 (~1–4 km)
    else:
        return 0.08          # очень сильное для z<5


# --- Cache для упрощённых треков: (z, track_id_md5) → simplified points list ---
_simplify_cache: dict[tuple, list] = {}
_SIMPLIFY_CACHE_MAXSIZE = 4096

def _simplify_cache_get(z: int, track_id_hash: str):
    key = (z, track_id_hash)
    if key in _simplify_cache:
        val = _simplify_cache.pop(key)
        _simplify_cache[key] = val
        return val
    return None

def _simplify_cache_put(z: int, track_id_hash: str, simplified_pts: list, maxsize: int = _SIMPLIFY_CACHE_MAXSIZE):
    key = (z, track_id_hash)
    if key in _simplify_cache:
        del _simplify_cache[key]
    while len(_simplify_cache) >= maxsize:
        _simplify_cache.pop(next(iter(_simplify_cache)))
    _simplify_cache[key] = simplified_pts


def _tile_bounds(z: int, x: int, y: int) -> dict:
    """Возвращает bounds тайла (lat/lon) + запас (buffer ~10%)."""
    def _tile2lon(tx, tz):
        return tx / (2.0**tz) * 360.0 - 180.0
    def _tile2lat(ty, tz):
        # Web Mercator inverse: lat = atan(sinh(π - 2π·y/2^z))
        n = math.pi - 2.0 * math.pi * ty / (2.0**tz)
        return math.degrees(math.atan(math.sinh(n)))

    west  = _tile2lon(x, z)
    south = _tile2lat(y + 1, z)
    east  = _tile2lon(x + 1, z)
    north = _tile2lat(y, z)

    # Небольшой буфер чтобы не обрезать треки на границах
    buf_lat = max(abs(north - south) * 0.05, 0.01)
    buf_lon = max(abs(east - west) * 0.05, 0.01)

    return {
        "west":  west - buf_lon,
        "east":  east + buf_lon,
        "south": south - buf_lat,
        "north": north + buf_lat,
    }


def _load_track_file(filepath: Path) -> list:
    """Загружает трек из JSON файла, возвращает list of point dicts."""
    with open(filepath, encoding="utf-8") as f:
        data = json.load(f)
    return data


def _simplify_track_points(points: list, tolerance: float) -> list:
    """Упрощает трек через Shapely simplify, возвращает подмножество точек."""
    if len(points) < 3:
        return points

    coords = [(p["lon"], p["lat"]) for p in points]
    try:
        line = LineString(coords)
        simplified = line.simplify(tolerance, preserve_topology=True)
        simplified_coords = list(simplified.coords)

        # Находим ближайшие оригинальные точки для каждой упрощённой координаты
        result = []
        for sx, sy in simplified_coords:
            # Берём ближайшую оригинальную точку
            best = min(points,
                       key=lambda p: (p["lon"] - sx)**2 + (p["lat"] - sy)**2)
            result.append(best)

        # Убедимся что первая и последняя точки на месте
        if result and result[0] != points[0]:
            result.insert(0, points[0])
        if result and result[-1] != points[-1]:
            result.append(points[-1])

        return result
    except Exception:
        # Если simplify упал — отдаём как есть
        return points


@app.route("/api/tiles/tracks/<int:z>/<int:x>/<int:y>.geojson", methods=["GET"])
def get_tracks_tile(z: int, x: int, y: int):
    """
    GeoJSON tile с треками для MapLibre/Leaflet.

    - LRU-кэш по (z, x, y)
    - Упрощение геометрии через Shapely по уровню зума
    - Кэш упрощения по (z, track_id)
    - Cache-Control для HTTP-кэширования
    """
    # Проверяем кэш целого тайла
    cached = _tile_cache_get(z, x, y)
    if cached is not None:
        accept_enc = request.headers.get("Accept-Encoding", "")
        if "gzip" in accept_enc:
            return Response(cached, status=200, headers={
                "Content-Type": "application/json",
                "Content-Encoding": "gzip",
                "Cache-Control": f"public, max-age={3600}",
            })
        # Если клиент не поддерживает gzip, разжимаем из кэша
        try:
            plain = gzip.decompress(cached)
        except Exception:
            plain = cached
        return Response(plain, status=200, headers={
            "Content-Type": "application/json",
            "Cache-Control": f"public, max-age={3600}",
        })

    # Определяем tolerance по зуму
    tolerance = _get_simplify_tolerance(z)
    bounds = _tile_bounds(z, x, y)

    features = []

    # Сканируем все треки из кэшей
    for cache_dir_name in ["cache", "cache2"]:
        cache_dir = DATA_DIR / cache_dir_name
        if not cache_dir.exists():
            continue
        for track_file in sorted(cache_dir.glob("*.json")):
            track_id = track_file.stem  # track_XXXXXXX

            # --- Кэш упрощения по (z, track_id) ---
            track_id_hash = hashlib.md5(track_file.name.encode()).hexdigest()[:12]
            simplified_pts = _simplify_cache_get(z, track_id_hash)

            if simplified_pts is None:
                # Загружаем и упрощаем
                raw_pts = _load_track_file(track_file)
                if len(raw_pts) < 2:
                    continue
                if tolerance < 0.0005:
                    simplified_pts = raw_pts
                else:
                    simplified_pts = _simplify_track_points(raw_pts, tolerance)
                _simplify_cache_put(z, track_id_hash, simplified_pts)

            # --- Проверка пересечения с тайлом ---
            pts_in_tile = []
            for p in simplified_pts:
                if (bounds["west"] <= p["lon"] <= bounds["east"] and
                    bounds["south"] <= p["lat"] <= bounds["north"]):
                    pts_in_tile.append(p)

            if len(pts_in_tile) < 2:
                continue

            # Собираем Feature
            coords = [[p["lon"], p["lat"]] for p in pts_in_tile]
            props = {
                "track_id": track_id,
                "callsign": pts_in_tile[0].get("callsign", ""),
                "points_count": len(pts_in_tile),
            }
            features.append({
                "type": "Feature",
                "properties": props,
                "geometry": {
                    "type": "LineString",
                    "coordinates": coords,
                },
            })

    tile_data = {
        "type": "FeatureCollection",
        "features": features,
        "tile": {"z": z, "x": x, "y": y},
        "generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
    }

    raw = orjson.dumps(tile_data)
    gz = gzip.compress(raw, compresslevel=6)

    # Сохраняем в LRU-кэш
    _tile_cache_put(z, x, y, gz)

    accept_enc = request.headers.get("Accept-Encoding", "")
    if "gzip" in accept_enc:
        return Response(gz, status=200, headers={
            "Content-Type": "application/json",
            "Content-Encoding": "gzip",
            "Cache-Control": f"public, max-age={3600}",
        })
    return Response(raw, status=200, headers={
        "Content-Type": "application/json",
        "Cache-Control": f"public, max-age={3600}",
    })


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)
