"""Open-Meteo historical weather client + simple road-risk heuristics.

Backwards-compatible API:
- fetch_weather(lat, lon, params=None, api_key=None)
- fetch_road_risk(lat, lon, extra_params=None, api_key=None, roadrisk_url=None)
- get_risk_score(lat, lon, **fetch_kwargs)
- compute_reroute(...)
- compute_index_and_reroute(...)
"""
import os
from typing import Tuple, Dict, Any, Optional, Callable, List
import requests
import heapq
import math
from datetime import date, timedelta

# Open-Meteo archive endpoint (no API key required)
BASE_ARCHIVE_URL = "https://archive-api.open-meteo.com/v1/archive"


def fetch_weather(lat: float, lon: float, params: Optional[dict] = None, api_key: Optional[str] = None) -> dict:
	"""Fetch historical weather from Open-Meteo archive API.

	Params may include 'start_date', 'end_date' (YYYY-MM-DD) and 'hourly' (comma-separated vars).
	Defaults to yesterday..today and hourly variables useful for road risk.
	(api_key parameter is accepted for compatibility but ignored.)
	"""
	if params is None:
		params = {}

	today = date.today()
	start = params.get("start_date", (today - timedelta(days=1)).isoformat())
	end = params.get("end_date", today.isoformat())

	hourly = params.get(
		"hourly",
		",".join(["temperature_2m", "relativehumidity_2m", "windspeed_10m", "precipitation", "weathercode"])
	)

	query = {
		"latitude": lat,
		"longitude": lon,
		"start_date": start,
		"end_date": end,
		"hourly": hourly,
		"timezone": params.get("timezone", "UTC"),
	}

	resp = requests.get(BASE_ARCHIVE_URL, params=query, timeout=15)
	resp.raise_for_status()
	return resp.json()


def fetch_road_risk(
	lat: float,
	lon: float,
	extra_params: Optional[dict] = None,
	api_key: Optional[str] = None,
	roadrisk_url: Optional[str] = None
) -> Tuple[dict, Dict[str, Any]]:
	"""
	Compute a simple road risk estimation using Open-Meteo historical weather.

	Returns (raw_data, features) where features includes 'road_risk_score' (float).
	api_key and roadrisk_url are accepted for backward compatibility but ignored.
	"""
	params = {}
	if extra_params:
		params.update(extra_params)

	# fetch weather via Open-Meteo archive
	try:
		data = fetch_weather(lat, lon, params=params)
	except Exception as e:
		features: Dict[str, Any] = {"road_risk_score": 0.0, "error": str(e)}
		return {}, features

	hourly = data.get("hourly", {}) if isinstance(data, dict) else {}

	def _arr_mean(key):
		arr = hourly.get(key)
		if isinstance(arr, list) and arr:
			valid = [float(x) for x in arr if x is not None]
			return sum(valid) / max(1, len(valid)) if valid else None
		return None

	def _arr_max(key):
		arr = hourly.get(key)
		if isinstance(arr, list) and arr:
			valid = [float(x) for x in arr if x is not None]
			return max(valid) if valid else None
		return None

	precip_mean = _arr_mean("precipitation")
	wind_mean = _arr_mean("windspeed_10m")
	wind_max = _arr_max("windspeed_10m")
	temp_mean = _arr_mean("temperature_2m")
	humidity_mean = _arr_mean("relativehumidity_2m")
	weathercodes = hourly.get("weathercode", [])

	# heuristic risk scoring:
	risk = 0.0
	if precip_mean is not None:
		risk += float(precip_mean) * 2.0
	if wind_mean is not None:
		risk += float(wind_mean) * 0.1
	if wind_max is not None and float(wind_max) > 15.0:
		risk += 1.0
	if humidity_mean is not None and float(humidity_mean) > 85.0:
		risk += 0.5
	try:
		# sample Open-Meteo weather codes that indicate precipitation/snow
		if any(int(wc) in (51, 61, 63, 65, 80, 81, 82, 71, 73, 75, 85, 86) for wc in weathercodes if wc is not None):
			risk += 1.0
	except Exception:
		pass

	if not math.isfinite(risk):
		risk = 0.0
	if risk < 0:
		risk = 0.0

	features: Dict[str, Any] = {
		"precipitation_mean": float(precip_mean) if precip_mean is not None else None,
		"wind_mean": float(wind_mean) if wind_mean is not None else None,
		"wind_max": float(wind_max) if wind_max is not None else None,
		"temp_mean": float(temp_mean) if temp_mean is not None else None,
		"humidity_mean": float(humidity_mean) if humidity_mean is not None else None,
		"road_risk_score": float(risk),
	}

	# include some raw metadata if present
	if "latitude" in data:
		features["latitude"] = data.get("latitude")
	if "longitude" in data:
		features["longitude"] = data.get("longitude")
	if "generationtime_ms" in data:
		features["generationtime_ms"] = data.get("generationtime_ms")

	return data, features


def _haversine_km(a_lat: float, a_lon: float, b_lat: float, b_lon: float) -> float:
	# returns distance in kilometers
	R = 6371.0
	lat1, lon1, lat2, lon2 = map(math.radians, (a_lat, a_lon, b_lat, b_lon))
	dlat = lat2 - lat1
	dlon = lon2 - lon1
	h = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
	return 2 * R * math.asin(min(1.0, math.sqrt(h)))


def risk_to_index(risk_score: float, max_risk: float = 10.0, num_bins: int = 10) -> int:
	"""
	Map a numeric risk_score to an integer index 1..num_bins (higher => more risky).
	"""
	if risk_score is None:
		return 1
	r = float(risk_score)
	if r <= 0:
		return 1
	if r >= max_risk:
		return num_bins
	bin_width = max_risk / float(num_bins)
	return int(r // bin_width) + 1


def get_risk_score(lat: float, lon: float, **fetch_kwargs) -> float:
	"""Wrapper: calls fetch_road_risk and returns features['road_risk_score'] (float)."""
	_, features = fetch_road_risk(lat, lon, extra_params=fetch_kwargs)
	return float(features.get("road_risk_score", 0.0))


def compute_reroute(
    start_lat: float,
    start_lon: float,
    end_lat: float,
    end_lon: float,
    risk_provider: Callable[[float, float], float] = None,
    n_lat: int = 20,
    n_lon: int = 20,
    distance_weight: float = 0.1,
    max_calls: Optional[int] = None
) -> Dict[str, Any]:
	"""
	Plan a path from (start_lat, start_lon) to (end_lat, end_lon) that avoids risky areas.
	Uses Dijkstra's algorithm over a lat/lon grid with cost = avg risk + distance_weight * distance.
	"""
	if risk_provider is None:
		risk_provider = lambda lat, lon: get_risk_score(lat, lon)

	min_lat = min(start_lat, end_lat)
	max_lat = max(start_lat, end_lat)
	min_lon = min(start_lon, end_lon)
	max_lon = max(start_lon, end_lon)

	lat_padding = (max_lat - min_lat) * 0.2
	lon_padding = (max_lon - min_lon) * 0.2
	min_lat -= lat_padding
	max_lat += lat_padding
	min_lon -= lon_padding
	max_lon += lon_padding

	lat_step = (max_lat - min_lat) / (n_lat - 1) if n_lat > 1 else 0.0
	lon_step = (max_lon - min_lon) / (n_lon - 1) if n_lon > 1 else 0.0

	coords = []
	for i in range(n_lat):
		for j in range(n_lon):
			coords.append((min_lat + i * lat_step, min_lon + j * lon_step))

	risks = []
	calls = 0
	for lat, lon in coords:
		if max_calls is not None and calls >= max_calls:
			risks.append(float('inf'))
			continue
		try:
			risk = risk_provider(lat, lon)
		except Exception:
			risk = float('inf')
		risks.append(float(risk))
		calls += 1

	def idx(i, j):
		return i * n_lon + j

	def find_closest(lat, lon):
		i = round((lat - min_lat) / (lat_step if lat_step != 0 else 1e-9))
		j = round((lon - min_lon) / (lon_step if lon_step != 0 else 1e-9))
		i = max(0, min(n_lat - 1, i))
		j = max(0, min(n_lon - 1, j))
		return idx(i, j)

	start_idx = find_closest(start_lat, start_lon)
	end_idx = find_closest(end_lat, end_lon)

	N = len(coords)
	dist = [math.inf] * N
	prev = [None] * N
	dist[start_idx] = 0.0
	pq = [(0.0, start_idx)]

	while pq:
		cost, u = heapq.heappop(pq)
		if cost > dist[u]:
			continue
		if u == end_idx:
			break

		ui, uj = u // n_lon, u % n_lon
		for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):
			vi, vj = ui + di, uj + dj
			if 0 <= vi < n_lat and 0 <= vj < n_lon:
				v = idx(vi, vj)
				if math.isinf(risks[v]) or math.isinf(risks[u]):
					continue
				lat_u, lon_u = coords[u]
				lat_v, lon_v = coords[v]
				d_km = _haversine_km(lat_u, lon_u, lat_v, lon_v)
				edge_cost = (risks[u] + risks[v]) / 2 + distance_weight * d_km
				new_cost = cost + edge_cost
				if new_cost < dist[v]:
					dist[v] = new_cost
					prev[v] = u
					heapq.heappush(pq, (new_cost, v))

	if math.isinf(dist[end_idx]):
		return {
			"reroute_needed": False,
			"reason": "no_path_found",
			"start_coord": (start_lat, start_lon),
			"end_coord": (end_lat, end_lon),
			"calls_made": calls
		}

	path_indices = []
	u = end_idx
	while u is not None:
		path_indices.append(u)
		u = prev[u]
	path_indices.reverse()

	path_coords = [coords[i] for i in path_indices]
	return {
		"reroute_needed": True,
		"start_coord": (start_lat, start_lon),
		"end_coord": (end_lat, end_lon),
		"path": path_coords,
		"total_cost": dist[end_idx],
		"start_risk": risks[start_idx],
		"end_risk": risks[end_idx],
		"calls_made": calls,
		"grid_shape": (n_lat, n_lon)
	}

# def compute_index(lat: float,
#                   lon: float,
#                   max_risk: float = 10.0,
#                   num_bins: int = 10,
#                   risk_provider: Optional[Callable[[float, float], float]] = None) -> int:
#     """
#     Computes and returns an index based on road risk and accident information.
    
#     Args:
#         lat: Latitude of the location.
#         lon: Longitude of the location.
#         max_risk: Maximum possible road risk score.
#         num_bins: Number of bins to divide the risk range into.
#         reroute_kwargs: Optional dictionary passed to reroute logic.
#         risk_provider: Optional custom risk provider function.

#     Returns:
#         An integer index (1 to num_bins) based on computed road risk.
#     """
#     # If a risk provider is not provided, use a default one
#     if risk_provider is None:
#         risk_provider = lambda lat, lon: get_risk_score(lat, lon)

#     # Fetch road risk score using the provided risk provider
#     road_risk = float(risk_provider(lat, lon))
    
#     # Compute the index based on road risk score and the max_risk, num_bins parameters
#     # The formula will divide the risk score into `num_bins` bins
#     # The index will be a number between 1 and num_bins based on the risk score

#     # Normalize the risk score to be between 0 and max_risk
#     normalized_risk = min(road_risk, max_risk) / max_risk

#     # Compute the index based on the normalized risk score
#     index = int(normalized_risk * num_bins)

#     # Ensure the index is within the expected range of 1 to num_bins
#     return max(1, min(index + 1, num_bins))  # Adding 1 because index is 0-based