import tkinter as tk
from tkinter import ttk, messagebox
from PIL import Image, ImageTk
import requests
from io import BytesIO
import math
import time
from datetime import datetime
import threading
from pathlib import Path
import random


class RateLimiter:
    def __init__(self, max_per_second=1):
        self.min_interval = 1.0 / max_per_second
        self.last_request = 0

    def wait(self):
        elapsed = time.time() - self.last_request
        if elapsed < self.min_interval:
            time.sleep(self.min_interval - elapsed)
        self.last_request = time.time()


class WeatherMapApp:
    def __init__(self, root):
        self.root = root
        self.root.title("SatForecast")
        self.root.geometry("1200x800")

        # API ключи
        self.owm_api_key = "cc437a23f6a217cd5859bd2d4dc9b6f8"

        # Rate limiter
        self.rate_limiter = RateLimiter(max_per_second=2)

        # Настройки карты
        self.lon = 37.6173
        self.lat = 55.7558
        self.zoom = 8
        self.dragging = False
        self.drag_start_x = 0
        self.drag_start_y = 0

        # Кэш тайлов
        self.cache_dir = Path("tile_cache")
        self.cache_dir.mkdir(exist_ok=True)

        # Маркеры
        self.markers = [
            {"name": "Москва", "lat": 55.7558, "lon": 37.6173},
            {"name": "СПб", "lat": 59.9343, "lon": 30.3351},
        ]

        # Настройка интерфейса
        self.setup_ui()

        # Загрузка данных
        self.load_map()
        self.load_forecast()

    def setup_ui(self):
        # Основной контейнер
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill='both', expand=True)

        # Левая панель управления
        control_panel = ttk.Frame(main_frame, width=300)
        control_panel.pack(side='left', fill='y', padx=5, pady=5)
        control_panel.pack_propagate(False)

        # Поиск города
        ttk.Label(control_panel, text="Поиск города:").pack(pady=5)
        search_frame = ttk.Frame(control_panel)
        search_frame.pack(fill='x', padx=5)

        self.city_entry = ttk.Entry(search_frame)
        self.city_entry.pack(side='left', fill='x', expand=True)
        self.city_entry.bind('<Return>', lambda e: self.search_city())

        ttk.Button(search_frame, text="🔍", width=3,
                   command=self.search_city).pack(side='left')

        # Список городов
        ttk.Label(control_panel, text="Избранные города:").pack(pady=5)
        self.city_listbox = tk.Listbox(control_panel, height=8)
        self.city_listbox.pack(fill='x', padx=5)
        self.city_listbox.bind('<<ListboxSelect>>', self.on_city_select)

        for marker in self.markers:
            self.city_listbox.insert(tk.END, marker['name'])

        # Управление маркерами
        marker_frame = ttk.Frame(control_panel)
        marker_frame.pack(fill='x', padx=5, pady=5)

        ttk.Button(marker_frame, text="Добавить маркер",
                   command=self.add_current_location).pack(fill='x')
        ttk.Button(marker_frame, text="Удалить маркер",
                   command=self.remove_marker).pack(fill='x')

        # Слои карты
        ttk.Label(control_panel, text="Слой карты:").pack(pady=5)

        layers = [
            ("Спутник (ESRI)", "esri_sat"),
            ("Спутник (Google)", "google_sat"),
            ("OpenStreetMap", "osm"),
            ("CartoDB", "cartodb"),
            ("Температура", "temp"),
            ("Облачность", "clouds"),
            ("Осадки", "precipitation")
        ]

        self.layer_var = tk.StringVar(value="esri_sat")
        for text, value in layers:
            ttk.Radiobutton(control_panel, text=text, variable=self.layer_var,
                            value=value, command=self.load_map).pack(anchor='w', padx=20)

        # Прогноз погоды
        ttk.Label(control_panel, text="Прогноз погоды:").pack(pady=10)
        self.forecast_text = tk.Text(control_panel, height=12, width=35)
        self.forecast_text.pack(fill='both', expand=True, padx=5)

        # Карта
        map_frame = ttk.Frame(main_frame)
        map_frame.pack(side='right', fill='both', expand=True)

        self.canvas = tk.Canvas(map_frame, bg='#1a1a2e')
        self.canvas.pack(fill='both', expand=True)

        # Привязка событий
        self.canvas.bind('<Button-1>', self.start_drag)
        self.canvas.bind('<B1-Motion>', self.drag_map)
        self.canvas.bind('<ButtonRelease-1>', self.end_drag)
        self.canvas.bind('<MouseWheel>', self.mouse_zoom)

        # Кнопки масштаба
        zoom_frame = ttk.Frame(map_frame)
        zoom_frame.place(relx=1.0, rely=0.5, anchor='e', x=-10)

        ttk.Button(zoom_frame, text="+", width=3,
                   command=lambda: self.change_zoom(1)).pack()
        ttk.Button(zoom_frame, text="-", width=3,
                   command=lambda: self.change_zoom(-1)).pack()

        # Статус бар
        self.status_label = ttk.Label(self.root, text="Готово")
        self.status_label.pack(side='bottom', fill='x')

    def get_cache_path(self, layer, x, y, zoom):
        cache_subdir = self.cache_dir / layer / str(zoom) / str(x)
        cache_subdir.mkdir(parents=True, exist_ok=True)
        return cache_subdir / f"{y}.png"

    def download_tile_cached(self, url, layer, x, y, zoom):
        cache_path = self.get_cache_path(layer, x, y, zoom)

        # Проверяем кэш
        if cache_path.exists():
            max_age = 3 * 3600 if "sat" in layer else 24 * 3600
            if datetime.now().timestamp() - cache_path.stat().st_mtime < max_age:
                try:
                    return Image.open(cache_path)
                except:
                    pass

        try:
            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
                'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
                'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
                'Referer': 'https://www.openstreetmap.org/',
                'Sec-Fetch-Dest': 'image',
                'Sec-Fetch-Mode': 'no-cors',
                'Sec-Fetch-Site': 'cross-site',
            }

            self.rate_limiter.wait()

            response = requests.get(url, headers=headers, timeout=15)

            if response.status_code == 200:
                content_type = response.headers.get('content-type', '')
                if 'image' in content_type or len(response.content) > 1000:
                    img = Image.open(BytesIO(response.content))
                    img.save(cache_path)
                    print(f"✓ {layer}: {x},{y} zoom={zoom}")
                    return img
                else:
                    print(f"✗ Не изображение: {url[:80]}")
            else:
                print(f"✗ {response.status_code}: {url[:80]}")

        except Exception as e:
            print(f"✗ Ошибка: {e}")

        # Возвращаем из кэша
        if cache_path.exists():
            try:
                return Image.open(cache_path)
            except:
                pass

        return None

    def get_tile_url(self, x, y, zoom):
        """Рабочие бесплатные URL для тайлов"""
        layer = self.layer_var.get()

        if layer == "esri_sat":
            # ESRI спутниковые снимки (бесплатно, без ключа)
            return f"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{zoom}/{y}/{x}"

        elif layer == "google_sat":
            # Google Maps спутник (бесплатно для некоммерческого использования)
            # Используем разные поддомены для распределения нагрузки
            subdomain = random.choice(['0', '1', '2', '3'])
            return f"https://mt{subdomain}.google.com/vt/lyrs=s&x={x}&y={y}&z={zoom}"

        elif layer == "osm":
            # OpenStreetMap
            subdomain = random.choice(['a', 'b', 'c'])
            return f"https://{subdomain}.tile.openstreetmap.org/{zoom}/{x}/{y}.png"

        elif layer == "cartodb":
            # CartoDB Light (красивый стиль)
            return f"https://a.basemaps.cartocdn.com/light_all/{zoom}/{x}/{y}.png"

        elif layer == "temp":
            return f"https://tile.openweathermap.org/map/temp_new/{zoom}/{x}/{y}.png?appid={self.owm_api_key}"

        elif layer == "clouds":
            return f"https://tile.openweathermap.org/map/clouds_new/{zoom}/{x}/{y}.png?appid={self.owm_api_key}"

        elif layer == "precipitation":
            return f"https://tile.openweathermap.org/map/precipitation_new/{zoom}/{x}/{y}.png?appid={self.owm_api_key}"

        return None

    def lat_lon_to_tile(self, lat, lon, zoom):
        lat_rad = math.radians(lat)
        n = 2.0 ** zoom
        xtile = (lon + 180.0) / 360.0 * n
        ytile = (1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n
        return xtile, ytile

    def load_map(self):
        threading.Thread(target=self._load_map_thread, daemon=True).start()

    def _load_map_thread(self):
        self.root.after(0, lambda: self.status_label.config(text="Загрузка карты..."))

        self.canvas.delete("tile")

        center_x, center_y = self.lat_lon_to_tile(self.lat, self.lon, self.zoom)
        center_x_int = int(center_x)
        center_y_int = int(center_y)

        tiles = {}
        for dx in range(-1, 2):
            for dy in range(-1, 2):
                x = center_x_int + dx
                y = center_y_int + dy

                url = self.get_tile_url(x, y, self.zoom)
                if url:
                    layer = self.layer_var.get()
                    img = self.download_tile_cached(url, layer, x, y, self.zoom)

                    if img:
                        tiles[(dx, dy)] = ImageTk.PhotoImage(img)

        self.root.after(0, self._display_tiles, tiles, center_x, center_y)
        self.root.after(0, lambda: self.status_label.config(
            text=f"Загружено тайлов: {len(tiles)}/9"))

    def _display_tiles(self, tiles, center_x, center_y):
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()

        if canvas_width < 100:
            canvas_width = 800
            canvas_height = 600

        tile_size = 256

        for (dx, dy), photo in tiles.items():
            x_offset = (center_x - int(center_x)) * tile_size
            y_offset = (center_y - int(center_y)) * tile_size

            px = canvas_width / 2 + dx * tile_size - x_offset
            py = canvas_height / 2 + dy * tile_size - y_offset

            self.canvas.create_image(px, py, image=photo, anchor='nw', tags="tile")

        self.draw_markers()
        self.canvas.tile_images = tiles

    def draw_markers(self):
        self.canvas.delete("marker")

        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()

        for marker in self.markers:
            marker_x, marker_y = self.lat_lon_to_tile(
                marker['lat'], marker['lon'], self.zoom)

            center_x, center_y = self.lat_lon_to_tile(self.lat, self.lon, self.zoom)

            tile_size = 256
            px = canvas_width / 2 + (marker_x - center_x) * tile_size
            py = canvas_height / 2 + (marker_y - center_y) * tile_size

            # Красивый маркер
            self.canvas.create_oval(px - 10, py - 10, px + 10, py + 10,
                                    fill='#ff4444', outline='white', width=2, tags="marker")
            self.canvas.create_oval(px - 3, py - 3, px + 3, py + 3,
                                    fill='white', tags="marker")
            self.canvas.create_text(px, py - 20, text=marker['name'],
                                    fill='white', font=('Arial', 10, 'bold'),
                                    tags="marker")

    def start_drag(self, event):
        self.dragging = True
        self.drag_start_x = event.x
        self.drag_start_y = event.y

    def drag_map(self, event):
        if not self.dragging:
            return

        dx = event.x - self.drag_start_x
        dy = event.y - self.drag_start_y

        self.canvas.move("tile", dx, dy)
        self.canvas.move("marker", dx, dy)

        tile_size = 256
        n = 2.0 ** self.zoom

        self.lon -= dx / tile_size * 360.0 / n
        self.lat += dy / tile_size * 180.0 / n

        self.drag_start_x = event.x
        self.drag_start_y = event.y

        if abs(dx) > tile_size / 2 or abs(dy) > tile_size / 2:
            self.load_map()

    def end_drag(self, event):
        self.dragging = False
        self.load_map()

    def mouse_zoom(self, event):
        if event.delta > 0:
            self.change_zoom(1)
        else:
            self.change_zoom(-1)

    def change_zoom(self, delta):
        new_zoom = self.zoom + delta
        if 2 <= new_zoom <= 18:
            self.zoom = new_zoom
            self.load_map()

    def search_city(self):
        city = self.city_entry.get().strip()
        if not city:
            return
        threading.Thread(target=self._search_city_thread, args=(city,), daemon=True).start()

    def _search_city_thread(self, city):
        try:
            url = "https://nominatim.openstreetmap.org/search"
            params = {
                "q": city,
                "format": "json",
                "limit": 1
            }
            headers = {
                "User-Agent": "SatForecast/1.0"
            }

            response = requests.get(url, params=params, headers=headers)
            data = response.json()

            if data:
                lat = float(data[0]['lat'])
                lon = float(data[0]['lon'])
                self.root.after(0, lambda: self.set_location(lat, lon, city))
            else:
                self.root.after(0, lambda: messagebox.showinfo("Поиск", "Город не найден"))
        except Exception as e:
            self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e)))

    def set_location(self, lat, lon, name):
        self.lat = lat
        self.lon = lon

        if not any(m['name'] == name for m in self.markers):
            self.markers.append({"name": name, "lat": lat, "lon": lon})
            self.city_listbox.insert(tk.END, name)

        self.load_map()
        self.load_forecast()

    def on_city_select(self, event):
        selection = self.city_listbox.curselection()
        if selection:
            index = selection[0]
            if index < len(self.markers):
                marker = self.markers[index]
                self.set_location(marker['lat'], marker['lon'], marker['name'])

    def add_current_location(self):
        name = f"Точка {len(self.markers) + 1}"
        self.markers.append({"name": name, "lat": self.lat, "lon": self.lon})
        self.city_listbox.insert(tk.END, name)
        self.draw_markers()

    def remove_marker(self):
        selection = self.city_listbox.curselection()
        if selection:
            index = selection[0]
            if index < len(self.markers):
                del self.markers[index]
                self.city_listbox.delete(index)
                self.load_map()

    def load_forecast(self):
        threading.Thread(target=self._load_forecast_thread, daemon=True).start()

    def _load_forecast_thread(self):
        try:
            # Open-Meteo (бесплатно, без ключа)
            url = "https://api.open-meteo.com/v1/forecast"
            params = {
                "latitude": self.lat,
                "longitude": self.lon,
                "hourly": "temperature_2m,precipitation,wind_speed_10m,cloud_cover",
                "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum",
                "timezone": "auto",
                "forecast_days": 3
            }

            response = requests.get(url, params=params)
            data = response.json()

            forecast_text = f"📍 Прогноз для координат:\n{self.lat:.2f}, {self.lon:.2f}\n\n"

            if 'daily' in data:
                forecast_text += "📅 По дням:\n"
                for i in range(3):
                    date = data['daily']['time'][i]
                    temp_max = data['daily']['temperature_2m_max'][i]
                    temp_min = data['daily']['temperature_2m_min'][i]
                    precip = data['daily']['precipitation_sum'][i]

                    weather_emoji = "☀️" if precip == 0 else "🌧️"
                    if temp_max > 30:
                        weather_emoji = "🔥"
                    elif temp_max < 0:
                        weather_emoji = "❄️"

                    forecast_text += f"{date}: {weather_emoji} {temp_min}°C...{temp_max}°C, 💧{precip}мм\n"

            if 'hourly' in data:
                forecast_text += "\n🕐 Ближайшие часы:\n"
                current_hour = datetime.now().hour
                for i in range(current_hour, min(current_hour + 8, len(data['hourly']['time']))):
                    time = data['hourly']['time'][i].split('T')[1]
                    temp = data['hourly']['temperature_2m'][i]
                    wind = data['hourly']['wind_speed_10m'][i]
                    clouds = data['hourly']['cloud_cover'][i]

                    cloud_emoji = "☁️" if clouds > 50 else "🌤️" if clouds > 20 else "☀️"
                    forecast_text += f"{time}: {cloud_emoji} {temp}°C, 💨{wind}м/с\n"

            self.root.after(0, self._update_forecast_display, forecast_text)
        except Exception as e:
            self.root.after(0, lambda: self.status_label.config(text=f"Ошибка прогноза: {e}"))

    def _update_forecast_display(self, text):
        self.forecast_text.delete(1.0, tk.END)
        self.forecast_text.insert(1.0, text)


def main():
    root = tk.Tk()
    app = WeatherMapApp(root)

    style = ttk.Style()
    style.theme_use('clam')

    def on_closing():
        if messagebox.askokcancel("Выход", "Закрыть приложение?"):
            root.destroy()

    root.protocol("WM_DELETE_WINDOW", on_closing)
    root.mainloop()


if __name__ == "__main__":
    main()