package weather

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/enmanuel/agents/tools"
)

// NewWeather creates a get_weather tool that fetches current weather and forecast
// for a city using the Open-Meteo API (free, no API key required).
func NewWeather() tools.Tool {
	client := &http.Client{Timeout: 15 * time.Second}

	return tools.Tool{
		Def: tools.Def{
			Name:        "get_weather",
			Description: "Get current weather conditions and 3-day forecast for a city. Returns temperature, humidity, wind speed, and weather description.",
			Parameters: []tools.Param{
				{Name: "city", Type: "string", Description: "City name to look up (e.g. 'Madrid', 'New York', 'Tokyo')", Required: true},
			},
		},
		Exec: func(ctx context.Context, args map[string]any) tools.Result {
			city := tools.GetString(args, "city")
			if city == "" {
				return tools.Result{Err: fmt.Errorf("get_weather: city is required")}
			}

			// Step 1: Geocode city name to coordinates
			lat, lon, resolvedName, country, err := geocodeCity(ctx, client, city)
			if err != nil {
				return tools.Result{Err: fmt.Errorf("get_weather: geocoding failed: %w", err)}
			}

			// Step 2: Fetch weather data
			weather, err := fetchWeather(ctx, client, lat, lon)
			if err != nil {
				return tools.Result{Err: fmt.Errorf("get_weather: forecast failed: %w", err)}
			}

			// Step 3: Format output
			output := formatWeather(resolvedName, country, weather)
			return tools.Result{Output: output}
		},
	}
}

// geocodeCity resolves a city name to coordinates using Open-Meteo Geocoding API.
func geocodeCity(ctx context.Context, client *http.Client, city string) (lat, lon float64, name, country string, err error) {
	u := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1&language=es&format=json",
		url.QueryEscape(city))

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return 0, 0, "", "", err
	}

	resp, err := client.Do(req)
	if err != nil {
		return 0, 0, "", "", err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 32*1024))
	if err != nil {
		return 0, 0, "", "", err
	}

	var result struct {
		Results []struct {
			Name      string  `json:"name"`
			Country   string  `json:"country"`
			Latitude  float64 `json:"latitude"`
			Longitude float64 `json:"longitude"`
		} `json:"results"`
	}
	if err := json.Unmarshal(body, &result); err != nil {
		return 0, 0, "", "", fmt.Errorf("invalid geocoding response: %w", err)
	}
	if len(result.Results) == 0 {
		return 0, 0, "", "", fmt.Errorf("city %q not found", city)
	}

	r := result.Results[0]
	return r.Latitude, r.Longitude, r.Name, r.Country, nil
}

type weatherData struct {
	Current struct {
		Temperature float64 `json:"temperature_2m"`
		Humidity    int     `json:"relative_humidity_2m"`
		WindSpeed   float64 `json:"wind_speed_10m"`
		WeatherCode int     `json:"weather_code"`
		FeelsLike   float64 `json:"apparent_temperature"`
	} `json:"current"`
	Daily struct {
		Time         []string  `json:"time"`
		TempMax      []float64 `json:"temperature_2m_max"`
		TempMin      []float64 `json:"temperature_2m_min"`
		WeatherCode  []int     `json:"weather_code"`
		PrecipProb   []int     `json:"precipitation_probability_max"`
	} `json:"daily"`
}

// fetchWeather gets current conditions and 3-day forecast from Open-Meteo.
func fetchWeather(ctx context.Context, client *http.Client, lat, lon float64) (*weatherData, error) {
	u := fmt.Sprintf(
		"https://api.open-meteo.com/v1/forecast?latitude=%.4f&longitude=%.4f"+
			"&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m"+
			"&daily=temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max"+
			"&timezone=auto&forecast_days=3",
		lat, lon,
	)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, err
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API returned %d: %s", resp.StatusCode, body)
	}

	var data weatherData
	if err := json.Unmarshal(body, &data); err != nil {
		return nil, fmt.Errorf("invalid forecast response: %w", err)
	}
	return &data, nil
}

// formatWeather produces a human-readable weather summary.
func formatWeather(city, country string, w *weatherData) string {
	var b strings.Builder

	fmt.Fprintf(&b, "Tiempo en %s, %s\n\n", city, country)
	fmt.Fprintf(&b, "AHORA:\n")
	fmt.Fprintf(&b, "  Temperatura: %.1f C (sensacion termica: %.1f C)\n", w.Current.Temperature, w.Current.FeelsLike)
	fmt.Fprintf(&b, "  Humedad: %d%%\n", w.Current.Humidity)
	fmt.Fprintf(&b, "  Viento: %.1f km/h\n", w.Current.WindSpeed)
	fmt.Fprintf(&b, "  Condicion: %s\n", weatherCodeToText(w.Current.WeatherCode))

	if len(w.Daily.Time) > 0 {
		fmt.Fprintf(&b, "\nPREVISION:\n")
		for i, date := range w.Daily.Time {
			fmt.Fprintf(&b, "  %s: %.0f/%.0f C, %s",
				date, w.Daily.TempMin[i], w.Daily.TempMax[i],
				weatherCodeToText(w.Daily.WeatherCode[i]))
			if i < len(w.Daily.PrecipProb) {
				fmt.Fprintf(&b, ", prob. lluvia: %d%%", w.Daily.PrecipProb[i])
			}
			fmt.Fprintln(&b)
		}
	}

	return b.String()
}

// weatherCodeToText converts WMO weather codes to Spanish descriptions.
func weatherCodeToText(code int) string {
	switch {
	case code == 0:
		return "Despejado"
	case code == 1:
		return "Mayormente despejado"
	case code == 2:
		return "Parcialmente nublado"
	case code == 3:
		return "Nublado"
	case code >= 45 && code <= 48:
		return "Niebla"
	case code >= 51 && code <= 55:
		return "Llovizna"
	case code >= 56 && code <= 57:
		return "Llovizna helada"
	case code >= 61 && code <= 65:
		return "Lluvia"
	case code >= 66 && code <= 67:
		return "Lluvia helada"
	case code >= 71 && code <= 77:
		return "Nieve"
	case code >= 80 && code <= 82:
		return "Chubascos"
	case code >= 85 && code <= 86:
		return "Chubascos de nieve"
	case code >= 95 && code <= 99:
		return "Tormenta"
	default:
		return fmt.Sprintf("Codigo %d", code)
	}
}
