package geodb

import (
	_ "embed"
	"log"
	"net"
	"net/http"

	"imuslab.com/zoraxy/mod/database"
)

//go:embed geoipv4.csv
var geoipv4 []byte //Geodb dataset for ipv4

//go:embed geoipv6.csv
var geoipv6 []byte //Geodb dataset for ipv6

type Store struct {
	BlacklistEnabled bool
	WhitelistEnabled bool
	geodb            [][]string //Parsed geodb list
	geodbIpv6        [][]string //Parsed geodb list for ipv6

	geotrie     *trie
	geotrieIpv6 *trie

	//geoipCache sync.Map

	sysdb *database.Database
}

type CountryInfo struct {
	CountryIsoCode string
	ContinetCode   string
}

func NewGeoDb(sysdb *database.Database) (*Store, error) {
	parsedGeoData, err := parseCSV(geoipv4)
	if err != nil {
		return nil, err
	}

	parsedGeoDataIpv6, err := parseCSV(geoipv6)
	if err != nil {
		return nil, err
	}

	blacklistEnabled := false
	whitelistEnabled := false
	if sysdb != nil {
		err = sysdb.NewTable("blacklist-cn")
		if err != nil {
			return nil, err
		}

		err = sysdb.NewTable("blacklist-ip")
		if err != nil {
			return nil, err
		}

		err = sysdb.NewTable("whitelist-cn")
		if err != nil {
			return nil, err
		}

		err = sysdb.NewTable("whitelist-ip")
		if err != nil {
			return nil, err
		}

		err = sysdb.NewTable("blackwhitelist")
		if err != nil {
			return nil, err
		}

		sysdb.Read("blackwhitelist", "blacklistEnabled", &blacklistEnabled)
		sysdb.Read("blackwhitelist", "whitelistEnabled", &whitelistEnabled)
	} else {
		log.Println("Database pointer set to nil: Entering debug mode")
	}

	return &Store{
		BlacklistEnabled: blacklistEnabled,
		WhitelistEnabled: whitelistEnabled,
		geodb:            parsedGeoData,
		geotrie:          constrctTrieTree(parsedGeoData),
		geodbIpv6:        parsedGeoDataIpv6,
		geotrieIpv6:      constrctTrieTree(parsedGeoDataIpv6),
		sysdb:            sysdb,
	}, nil
}

func (s *Store) ToggleBlacklist(enabled bool) {
	s.sysdb.Write("blackwhitelist", "blacklistEnabled", enabled)
	s.BlacklistEnabled = enabled
}

func (s *Store) ToggleWhitelist(enabled bool) {
	s.sysdb.Write("blackwhitelist", "whitelistEnabled", enabled)
	s.WhitelistEnabled = enabled
}

func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
	cc := s.search(ipstring)
	return &CountryInfo{
		CountryIsoCode: cc,
		ContinetCode:   "",
	}, nil
}

func (s *Store) Close() {

}

/*
Check if a IP address is blacklisted, in either country or IP blacklist
IsBlacklisted default return is false (allow access)
*/
func (s *Store) IsBlacklisted(ipAddr string) bool {
	if !s.BlacklistEnabled {
		//Blacklist not enabled. Always return false
		return false
	}

	if ipAddr == "" {
		//Unable to get the target IP address
		return false
	}

	countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
	if err != nil {
		return false
	}

	if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) {
		return true
	}

	if s.IsIPBlacklisted(ipAddr) {
		return true
	}

	return false
}

/*
IsWhitelisted check if a given IP address is in the current
server's white list.

Note that the Whitelist default result is true even
when encountered error
*/
func (s *Store) IsWhitelisted(ipAddr string) bool {
	if !s.WhitelistEnabled {
		//Whitelist not enabled. Always return true (allow access)
		return true
	}

	if ipAddr == "" {
		//Unable to get the target IP address, assume ok
		return true
	}

	countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
	if err != nil {
		return true
	}

	if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) {
		return true
	}

	if s.IsIPWhitelisted(ipAddr) {
		return true
	}

	return false
}

// A helper function that check both blacklist and whitelist for access
// for both geoIP and ip / CIDR ranges
func (s *Store) AllowIpAccess(ipaddr string) bool {
	if s.IsBlacklisted(ipaddr) {
		return false
	}

	return s.IsWhitelisted(ipaddr)
}

func (s *Store) AllowConnectionAccess(conn net.Conn) bool {
	if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
		return s.AllowIpAccess(addr.IP.String())
	}
	return true
}

func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
	ipAddr := GetRequesterIP(r)
	if ipAddr == "" {
		return ""
	}
	countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
	if err != nil {
		return ""
	}

	return countryCode.CountryIsoCode
}
