package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"
)

// free api for reverse geocoding based on IP
const revGeoURL = "http://ip-api.com/json/"

type Location struct {
	Lat         float64
	Lon         float64
	City        string
	State       string
	Country     string
	Zipcode     string
	DisplayName string
}

func NewLocation() *Location {
	return &Location{}
}

func (l *Location) UnmarshalJSON(b []byte) error {

	var aux struct {
		Lat         json.RawMessage `json:"lat"`
		Lon         json.RawMessage `json:"lon"`
		DisplayName string          `json:"display_name"`
		City        string          `json:"city"`
		State       string          `json:"state"`
		Country     string          `json:"country"`
		Zipcode     string          `json:"zipcode"`
		Address     struct {
			City    string `json:"city"`
			Town    string `json:"town"`
			Village string `json:"village"`
			Hamlet  string `json:"hamlet"`
			County  string `json:"county"`
			State   string `json:"state"`
			Country string `json:"country"`
			Zipcode string `json:"postcode"`
		} `json:"address"`
	}

	if err := json.Unmarshal(b, &aux); err != nil {
		return err
	}

	lat, err := parseJSONFloat(aux.Lat)
	if err != nil {
		return err
	}
	lon, err := parseJSONFloat(aux.Lon)
	if err != nil {
		return err
	}

	// copy over values
	l.Lat = lat
	l.Lon = lon
	l.City = firstNonEmpty(
		aux.Address.City,
		aux.Address.Town,
		aux.Address.Village,
		aux.Address.Hamlet,
		aux.Address.County,
		aux.City,
	)
	l.State = firstNonEmpty(aux.Address.State, aux.State)
	l.Country = firstNonEmpty(aux.Address.Country, aux.Country)
	l.Zipcode = firstNonEmpty(aux.Address.Zipcode, aux.Zipcode)
	l.DisplayName = firstNonEmpty(aux.DisplayName, aux.City)

	return nil
}

func SearchLocations(ctx context.Context, query, userAgent string) ([]Location, error) {
	queryURL := fmt.Sprintf(
		"https://nominatim.openstreetmap.org/search?q=%s&format=jsonv2&addressdetails=1&limit=10",
		url.QueryEscape(query),
	)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, queryURL, nil)
	if err != nil {
		return []Location{}, err
	}

	req.Header.Set("User-Agent", userAgent)

	client := &http.Client{
		Timeout: 15 * time.Second,
	}

	resp, err := client.Do(req)

	if err != nil {
		return []Location{}, err
	}
	defer resp.Body.Close()

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

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return []Location{}, err
	}

	var locations []Location

	if err := json.Unmarshal(b, &locations); err != nil {
		return []Location{}, err
	}

	return locations, nil
}

// func (l *Location) getCoords() error {
//
// 	res, err := http.Get(revGeoURL)
//
// 	if err != nil {
// 		return err
// 	}
//
// 	body, err := io.ReadAll(res.Body)
// 	res.Body.Close()
// 	if res.StatusCode > 299 {
// 		errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
// 		return errors.New(errMsg)
// 	}
//
// 	if err != nil {
// 		return err
// 	}
//
// 	// unmarshall response into struct
// 	if err := json.Unmarshal(body, l); err != nil {
// 		return err
// 	}
//
// 	l.Found = true
//
// 	return nil
// }
//
// func (l *Location) getStation() error {
// 	// generate url based on coords
// 	url := fmt.Sprintf("https://api.weather.gov/points/%f,%f", l.Lat, l.Lon)
//
// 	res, err := http.Get(url)
//
// 	if err != nil {
// 		return err
// 	}
//
// 	body, err := io.ReadAll(res.Body)
// 	res.Body.Close()
// 	if res.StatusCode > 299 {
// 		errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
// 		return errors.New(errMsg)
// 	}
//
// 	if err != nil {
// 		return err
// 	}
//
// 	// send another request to station URL to find closest
// 	stationURL := gjson.Get(string(body), "properties.observationStations")
//
// 	res, err = http.Get(stationURL.String())
//
// 	if err != nil {
// 		return err
// 	}
//
// 	body, err = io.ReadAll(res.Body)
// 	res.Body.Close()
// 	if res.StatusCode > 299 {
// 		errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
// 		return errors.New(errMsg)
// 	}
//
// 	if err != nil {
// 		return err
// 	}
//
// 	station := gjson.Get(string(body), "features.0.properties.stationIdentifier")
//
// 	if station.String() == "" {
// 		return errors.New("Station not found")
// 	}
//
// 	l.Station = station.String()
//
// 	return nil
// }

// for debugging
func (l *Location) String() string {

	return fmt.Sprintf("%s, %s %s (%f, %f)", l.City, l.State, l.Zipcode, l.Lat, l.Lon)
}

func (l *Location) Label() string {
	city := strings.TrimSpace(l.City)
	state := strings.TrimSpace(l.State)
	if city != "" && state != "" {
		return fmt.Sprintf("%s, %s", city, state)
	}
	if city != "" {
		return city
	}
	if l.DisplayName != "" {
		return l.DisplayName
	}
	return strings.TrimSpace(fmt.Sprintf("%s %s", city, l.State))
}

func SelectLocation(results []Location) (Location, error) {
	switch len(results) {
	case 0:
		return Location{}, fmt.Errorf("no locations found")
	case 1:
		return results[0], nil
	}

	if !hasTTY(os.Stdin, os.Stdout) {
		return Location{}, fmt.Errorf("multiple locations found; fzf requires a TTY, refine search")
	}
	if _, err := exec.LookPath("fzf"); err != nil {
		return Location{}, fmt.Errorf("multiple locations found; install fzf or refine search")
	}

	lines := make([]string, 0, len(results))
	for i, loc := range results {
		label := loc.Label()
		if loc.Country != "" && !strings.Contains(label, loc.Country) {
			label = fmt.Sprintf("%s, %s", label, loc.Country)
		}
		lines = append(lines, fmt.Sprintf("%d\t%s (%.4f, %.4f)", i+1, label, loc.Lat, loc.Lon))
	}

	cmd := exec.Command("fzf", "--prompt", "Select location> ", "--height", "40%", "--reverse")
	cmd.Stdin = strings.NewReader(strings.Join(lines, "\n"))
	var out bytes.Buffer
	cmd.Stdout = &out
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 130 {
			return Location{}, fmt.Errorf("location selection cancelled")
		}
		return Location{}, fmt.Errorf("fzf selection failed: %w", err)
	}

	selection := strings.TrimSpace(out.String())
	if selection == "" {
		return Location{}, fmt.Errorf("no location selected")
	}

	parts := strings.SplitN(selection, "\t", 2)
	idx, err := strconv.Atoi(strings.TrimSpace(parts[0]))
	if err != nil || idx < 1 || idx > len(results) {
		return Location{}, fmt.Errorf("invalid selection")
	}

	return results[idx-1], nil
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}

func parseJSONFloat(raw json.RawMessage) (float64, error) {
	if len(raw) == 0 {
		return 0, nil
	}

	var asFloat float64
	if err := json.Unmarshal(raw, &asFloat); err == nil {
		return asFloat, nil
	}

	var asString string
	if err := json.Unmarshal(raw, &asString); err != nil {
		return 0, err
	}

	value, err := strconv.ParseFloat(asString, 64)
	if err != nil {
		return 0, err
	}
	return value, nil
}

func hasTTY(in, out *os.File) bool {
	inStat, err := in.Stat()
	if err != nil {
		return false
	}
	outStat, err := out.Stat()
	if err != nil {
		return false
	}
	return (inStat.Mode()&os.ModeCharDevice) != 0 && (outStat.Mode()&os.ModeCharDevice) != 0
}
