package main

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

type ForecastPeriod struct {
	StartTime time.Time `json:"start_time"`
	IsDaytime bool      `json:"is_daytime"`
	Temp      int       `json:"temperature"`
	Unit      string    `json:"unit"`
	Summary   string    `json:"summary"`
	Pop       *int      `json:"pop,omitempty"`
}

type NWSClient struct {
	client    *http.Client
	userAgent string
}

func NewNWSClient(userAgent string) *NWSClient {
	return &NWSClient{
		client:    &http.Client{Timeout: 15 * time.Second},
		userAgent: userAgent,
	}
}

func (c *NWSClient) FetchForecast(ctx context.Context, loc Location, unit string) (ForecastPeriod, error) {
	pointsURL := fmt.Sprintf("https://api.weather.gov/points/%.4f,%.4f", loc.Lat, loc.Lon)
	var pointsResp struct {
		Properties struct {
			ForecastHourly string `json:"forecastHourly"`
		} `json:"properties"`
	}

	if err := c.getJSON(ctx, pointsURL, &pointsResp); err != nil {
		return ForecastPeriod{}, err
	}
	if pointsResp.Properties.ForecastHourly == "" {
		return ForecastPeriod{}, fmt.Errorf("missing forecast hourly URL from NWS")
	}

	var forecastResp struct {
		Properties struct {
			Periods []struct {
				StartTime time.Time `json:"startTime"`
				IsDaytime bool      `json:"isDaytime"`
				Temp      int       `json:"temperature"`
				Unit      string    `json:"temperatureUnit"`
				Summary   string    `json:"shortForecast"`
				Pop       struct {
					Value *int `json:"value"`
				} `json:"probabilityOfPrecipitation"`
			} `json:"periods"`
		} `json:"properties"`
	}

	if err := c.getJSON(ctx, pointsResp.Properties.ForecastHourly, &forecastResp); err != nil {
		return ForecastPeriod{}, err
	}
	if len(forecastResp.Properties.Periods) == 0 {
		return ForecastPeriod{}, fmt.Errorf("no forecast periods returned by NWS")
	}

	period := forecastResp.Properties.Periods[0]
	finalUnit := period.Unit
	finalTemp := period.Temp
	if unit == "C" && strings.EqualFold(period.Unit, "F") {
		finalTemp = int((float64(period.Temp) - 32) * 5.0 / 9.0)
		finalUnit = "C"
	}

	return ForecastPeriod{
		StartTime: period.StartTime,
		IsDaytime: period.IsDaytime,
		Temp:      finalTemp,
		Unit:      finalUnit,
		Summary:   period.Summary,
		Pop:       period.Pop.Value,
	}, nil
}

func (c *NWSClient) getJSON(ctx context.Context, url string, target any) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return err
	}
	req.Header.Set("User-Agent", c.userAgent)
	req.Header.Set("Accept", "application/geo+json")

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

	if resp.StatusCode > 299 {
		return fmt.Errorf("request failed with status %d", resp.StatusCode)
	}

	decoder := json.NewDecoder(resp.Body)
	if err := decoder.Decode(target); err != nil {
		return err
	}
	return nil
}

func FormatOutput(loc Location, period ForecastPeriod, includeDesc, pretty bool) string {
	conditionText := titleCase(period.Summary)
	temp := formatTemperature(period, pretty)
	popText, popIcon := formatPop(period, pretty)

	if pretty {
		conditionIcon := iconForForecast(period.Summary, period.IsDaytime)
		condition := conditionIcon
		if includeDesc {
			condition = fmt.Sprintf("%s %s", conditionIcon, conditionText)
		}

		parts := []string{
			condition,
			temp,
			popIcon + popText,
			"in",
			loc.Label(),
		}
		return joinParts(parts...)
	}

	condition := conditionText
	if includeDesc {
		condition = fmt.Sprintf("%s", conditionText)
	}

	parts := []string{
		condition,
		temp,
		popText,
		"in",
		loc.Label(),
	}
	return joinParts(parts...)
}

func titleCase(input string) string {
	parts := strings.Fields(strings.ToLower(input))
	for i, part := range parts {
		if part == "and" || part == "or" || part == "the" || part == "a" || part == "an" {
			continue
		}
		parts[i] = strings.ToUpper(part[:1]) + part[1:]
	}
	return strings.Join(parts, " ")
}

func iconForForecast(summary string, isDay bool) string {
	text := strings.ToLower(summary)

	if strings.Contains(text, "thunder") {
		return "\ue31d"
	}
	if isPrecip(text) {
		if strings.Contains(text, "snow") || strings.Contains(text, "sleet") || strings.Contains(text, "flurr") {
			return "\ue31a"
		}
		return "\ue318"
	}
	if isMostlySunny(text) {
		if isDay {
			return "\ue30c"
		}
		return "\ue379"
	}
	if isPartlySunny(text) {
		if isDay {
			return "\ue30c"
		}
		return "\ue379"
	}
	if isClear(text) {
		if isDay {
			return "\ue30d"
		}
		return "\ue32b"
	}
	if isCloudy(text) {
		return "\ue312"
	}
	if isDay {
		return "\ue30d"
	}
	return "\ue32b"
}

func formatPop(period ForecastPeriod, pretty bool) (string, string) {
	popValue := 0
	if period.Pop != nil {
		popValue = *period.Pop
	}

	if pretty {
		icon := precipitationIcon(period.Summary, period.IsDaytime)
		return fmt.Sprintf(" %d%%", popValue), icon
	}

	return fmt.Sprintf("%d%%", popValue), ""
}

func precipitationIcon(summary string, isDay bool) string {
	text := strings.ToLower(summary)
	if strings.Contains(text, "snow") || strings.Contains(text, "sleet") || strings.Contains(text, "flurr") {
		return "\uf2dc"
	}
	if strings.Contains(text, "rain") || strings.Contains(text, "shower") || strings.Contains(text, "drizzle") {
		return "\ue371"
	}
	return "\ue371"
}

func isPrecip(text string) bool {
	return strings.Contains(text, "rain") ||
		strings.Contains(text, "shower") ||
		strings.Contains(text, "drizzle") ||
		strings.Contains(text, "snow") ||
		strings.Contains(text, "sleet") ||
		strings.Contains(text, "flurr") ||
		strings.Contains(text, "ice") ||
		strings.Contains(text, "hail")
}

func isClear(text string) bool {
	return strings.Contains(text, "clear") || strings.Contains(text, "sunny")
}

func isMostlySunny(text string) bool {
	return strings.Contains(text, "mostly sunny") || strings.Contains(text, "mostly clear")
}

func isPartlySunny(text string) bool {
	return strings.Contains(text, "partly sunny") || strings.Contains(text, "partly cloudy")
}

func isCloudy(text string) bool {
	return strings.Contains(text, "cloudy") || strings.Contains(text, "overcast")
}

func formatTemperature(period ForecastPeriod, pretty bool) string {
	unit := strings.ToUpper(strings.TrimSpace(period.Unit))
	if unit == "" {
		unit = "F"
	}

	return fmt.Sprintf("%d°%s", period.Temp, unit)
}

func joinParts(parts ...string) string {
	trimmed := make([]string, 0, len(parts))
	for _, part := range parts {
		if strings.TrimSpace(part) == "" {
			continue
		}
		trimmed = append(trimmed, part)
	}
	return strings.Join(trimmed, " ")
}
