// Package geoip provides IP address geolocation features.
package geoip

import (
	"errors"
	"fmt"
	"net"
	"net/http"
	"strings"

	"code.nonshy.com/nonshy/website/pkg/config"
	"code.nonshy.com/nonshy/website/pkg/log"
	"code.nonshy.com/nonshy/website/pkg/utility"
	"github.com/oschwald/geoip2-golang"
)

// Insights returns structured GeoIP insights useful for the Location tab of the settings page.
type Insights struct {
	CountryCode  string
	CountryName  string
	Subdivisions []string
	City         string
	PostalCode   string
	Latitude     float64
	Longitude    float64
	FlagEmoji    string
}

// IsZero checks if the insights are unpopulated.
func (i Insights) IsZero() bool {
	return i.CountryCode == ""
}

// String pretty prints the insights for front-end display.
func (i Insights) String() string {
	var parts = []string{
		i.CountryName,
		strings.Join(i.Subdivisions, ", "),
		i.City,
	}
	if i.PostalCode != "" {
		parts = append(parts, "Postal Code "+i.PostalCode)
	}
	parts = append(parts, fmt.Sprintf("Lat: %f; Long: %f", i.Latitude, i.Longitude))
	return strings.Join(parts, "; ")
}

// Short prints a short summary string of the insights.
func (i Insights) Short() string {
	var parts = []string{
		i.CountryName,
		strings.Join(i.Subdivisions, ", "),
		i.City,
	}
	return strings.Join(parts, "; ")
}

// Medium prints a summary including country flag emoji with all fields except lat/long.
func (i Insights) Medium() string {
	var parts = []string{
		strings.Join([]string{i.FlagEmoji, i.CountryCode}, " "),
	}
	parts = append(parts, i.Short())
	return strings.Join(parts, "; ")
}

// GetRequestInsights returns structured insights based on the current HTTP request.
func GetRequestInsights(r *http.Request) (Insights, error) {
	var (
		addr = utility.IPAddress(r)
		ip   = net.ParseIP(addr)
	)
	return GetInsights(ip)
}

// GetInsights returns structured insights based on an IP address.
func GetInsights(ip net.IP) (Insights, error) {
	city, err := GetCity(ip)
	if err != nil {
		return Insights{}, err
	}

	// Country flag emoji.
	emoji, err := CountryFlagEmoji(city.Country.IsoCode)
	if err != nil {
		emoji = "🏴‍☠️"
	}

	var result = Insights{
		City:         city.City.Names["en"],
		CountryCode:  city.Country.IsoCode,
		CountryName:  city.Country.Names["en"],
		Subdivisions: []string{},
		PostalCode:   city.Postal.Code,
		Latitude:     city.Location.Latitude,
		Longitude:    city.Location.Longitude,
		FlagEmoji:    emoji,
	}
	for _, sub := range city.Subdivisions {
		if name, ok := sub.Names["en"]; ok {
			result.Subdivisions = append(result.Subdivisions, name)
		}
	}

	return result, nil
}

type InsightsMap map[string]Insights

func (i InsightsMap) Get(key string) Insights {
	return i[key]
}

// MapInsights returns a hash map of IP address (strings) to their Insights.
func MapInsights(addrs []string) InsightsMap {
	var result = map[string]Insights{}
	for _, addr := range addrs {
		if _, ok := result[addr]; ok {
			continue
		}

		ip := net.ParseIP(addr)
		insights, err := GetInsights(ip)
		if err != nil {
			log.Error("MapInsights(%s): %s", addr, err)
		}
		result[addr] = insights
	}
	return result
}

// GetRequestCity returns the GeoIP City result for the current HTTP request.
func GetRequestCity(r *http.Request) (*geoip2.City, error) {
	var (
		addr = utility.IPAddress(r)
		ip   = net.ParseIP(addr)
	)
	return GetCity(ip)
}

// GetRequestCountryFlag returns the country flag based on the current HTTP request IP address.
func GetRequestCountryFlag(r *http.Request) (string, error) {
	city, err := GetRequestCity(r)
	if err != nil {
		// If the remote addr is localhost (local dev testing), default to US flag.
		if addr := utility.IPAddress(r); addr == "127.0.0.1" || addr == "::1" {
			return CountryFlagEmoji("US")
		}

		return "", err
	}

	return CountryFlagEmoji(city.Country.IsoCode)
}

// GetRequestCountryFlagWithCode returns the flag joined with the country code by a space (like CountryFlagEmojiWithCode).
func GetRequestCountryFlagWithCode(r *http.Request) (string, error) {
	city, err := GetRequestCity(r)
	if err != nil {
		// If the remote addr is localhost (local dev testing), default to US flag.
		if addr := utility.IPAddress(r); addr == "127.0.0.1" || addr == "::1" {
			return CountryFlagEmojiWithCode("US")
		}

		return "", err
	}

	return CountryFlagEmojiWithCode(city.Country.IsoCode)
}

// GetChatFlagEmoji returns a specialized country flag emoji string for the BareRTC chat room.
//
// This will include the country flag emoji along with the country and territory/state name.
func GetChatFlagEmoji(r *http.Request) (string, error) {
	city, err := GetRequestCity(r)
	if err != nil {
		// If the remote addr is localhost (local dev testing), default to US flag and only "US" code.
		if addr := utility.IPAddress(r); addr == "127.0.0.1" || addr == "::1" {
			return CountryFlagEmojiWithCode("US")
		}

		return "", err
	}

	// Codes to attach (state, country, etc.)
	emoji, err := CountryFlagEmoji(city.Country.IsoCode)
	if err != nil {
		return emoji, err
	}

	// The components of text location part of the string.
	var flags = []string{}

	// The country. Name or ISO code?
	if name, ok := city.Country.Names["en"]; ok {
		flags = append(flags, name)
	} else {
		flags = append(flags, city.Country.IsoCode)
	}

	// Subdivisions (states)
	if len(city.Subdivisions) > 0 {
		// Stop at just one subdivision. This will be US states
		// and general regions, but without getting too specific
		// for UK users especially where the subdivisions can hone
		// in on their city of 1,000 population!
		sub := city.Subdivisions[0]

		// Can we get its name?
		if name, ok := sub.Names["en"]; ok {
			flags = append(flags, name)
		} else {
			flags = append(flags, sub.IsoCode)
		}
	}

	return emoji + " " + strings.Join(flags, ", "), nil
}

// GetCity queries the GeoIP database for city information for an IP address.
func GetCity(ip net.IP) (*geoip2.City, error) {
	db, err := geoip2.Open(config.GeoIPPath)
	if err != nil {
		return nil, err
	}

	return db.City(ip)
}

// CountryFlagEmoji returns the emoji sequence for a country flag based on
// the two-letter country code.
func CountryFlagEmoji(alpha2 string) (string, error) {
	if len(alpha2) != 2 {
		return "", errors.New("country code must be two letters long")
	}

	alpha2 = strings.ToLower(alpha2)

	var (
		flagBaseIndex = '\U0001F1E6' - 'a'
		box           = func(ch byte) string {
			return string(rune(ch) + flagBaseIndex)
		}
	)

	return string(box(alpha2[0]) + box(alpha2[1])), nil
}

// CountryFlagEmojiWithCode returns a string consisting of the country flag, a space, and the alpha2 code passed in.
func CountryFlagEmojiWithCode(alpha2 string) (string, error) {
	if emoji, err := CountryFlagEmoji(alpha2); err != nil {
		return emoji, err
	} else {
		return emoji + " " + alpha2, nil
	}
}