package nominatim

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
)

type Nominatim struct {
	siteName string
}

func New(siteName string) (*Nominatim, error) {
	if siteName == "" {
		return nil, fmt.Errorf("`siteName` must be defined.")
	}

	return &Nominatim{
		siteName: siteName,
	}, nil
}

// Search for coordinates based on q
// API endpoint: https://nominatim.openstreetmap.org/search
func (n *Nominatim) Search(q string) (*SearchResults, error) {
	url := fmt.Sprintf("https://nominatim.openstreetmap.org/search?q=%s&format=jsonv2", url.QueryEscape(q))

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	req.Header.Set("User-Agent", n.siteName)

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

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s", resp.Status)
	}

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

	result := SearchResults{}
	err = json.Unmarshal(body, &result)
	if err != nil {
		return nil, err
	}

	return &result, nil
}

// Reverse search for location based on lat and lon
// API endpoint: https://nominatim.openstreetmap.org/reverse
func (n *Nominatim) Reverse(lat, lon float64) (*ReverseResult, error) {
	url := fmt.Sprintf("https://nominatim.openstreetmap.org/reverse?lat=%.16f&lon=%.16f&format=jsonv2", lat, lon)

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	req.Header.Set("User-Agent", n.siteName)

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

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s", resp.Status)
	}

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

	result := ReverseResult{}
	err = json.Unmarshal(body, &result)
	if err != nil {
		return nil, err
	}

	if allFieldsNil(result) {
		return nil, fmt.Errorf("No result")
	}

	return &result, nil
}

type LookupResult struct {
	Coordinates
	Location string `json:"location"`
}

// Lookup location coordinates based on q
func (n *Nominatim) Lookup(q string) (*LookupResult, error) {
	r, err := n.Search(q)

	if err != nil {
		return nil, err
	}

	if len(*r) == 0 {
		return nil, fmt.Errorf("no result")
	}

	firstResult := (*r)[0]
	lon, err := strconv.ParseFloat(*firstResult.Longitude, 64)
	if err != nil {
		return nil, err
	}

	lat, err := strconv.ParseFloat(*firstResult.Latitude, 64)
	if err != nil {
		return nil, err
	}

	return &LookupResult{
		Location: *firstResult.Name,
		Coordinates: Coordinates{
			Latitude:  lat,
			Longitude: lon,
		},
	}, nil
}

// Lookup location name based on lat and lon
func (n *Nominatim) ReverseLookup(lat, lon float64) (*LookupResult, error) {
	r, err := n.Reverse(lat, lon)

	if err != nil {
		return nil, err
	}

	return &LookupResult{
		Location: *r.Name,
		Coordinates: Coordinates{
			Latitude:  lat,
			Longitude: lon,
		},
	}, nil
}

// Check if all fields on s is nil
func allFieldsNil(s interface{}) bool {
	v := reflect.ValueOf(s)

	// Check if the passed value is a struct
	if v.Kind() != reflect.Struct {
		return false
	}

	// Iterate through all fields in the struct
	for i := 0; i < v.NumField(); i++ {
		field := v.Field(i)

		// Check if the field is a pointer and is nil
		if field.Kind() == reflect.Ptr && !field.IsNil() {
			return false
		}
	}
	return true
}