package acl

import (
	"net"
	"strings"

	acl "github.com/yusing/go-proxy/internal/acl/types"
	"github.com/yusing/go-proxy/internal/gperr"
)

type matcher func(*acl.IPInfo) bool

const (
	MatcherTypeIP       = "ip"
	MatcherTypeCIDR     = "cidr"
	MatcherTypeTimeZone = "tz"
	MatcherTypeCountry  = "country"
)

var errMatcherFormat = gperr.Multiline().AddLines(
	"invalid matcher format, expect {type}:{value}",
	"Available types: ip|cidr|tz|country",
	"ip:127.0.0.1",
	"cidr:127.0.0.0/8",
	"tz:Asia/Shanghai",
	"country:GB",
)
var (
	errSyntax               = gperr.New("syntax error")
	errInvalidIP            = gperr.New("invalid IP")
	errInvalidCIDR          = gperr.New("invalid CIDR")
	errMaxMindNotConfigured = gperr.New("MaxMind not configured")
)

func (cfg *Config) parseMatcher(s string) (matcher, gperr.Error) {
	parts := strings.Split(s, ":")
	if len(parts) != 2 {
		return nil, errSyntax
	}

	switch parts[0] {
	case MatcherTypeIP:
		ip := net.ParseIP(parts[1])
		if ip == nil {
			return nil, errInvalidIP
		}
		return matchIP(ip), nil
	case MatcherTypeCIDR:
		_, net, err := net.ParseCIDR(parts[1])
		if err != nil {
			return nil, errInvalidCIDR
		}
		return matchCIDR(net), nil
	case MatcherTypeTimeZone:
		if cfg.MaxMind == nil {
			return nil, errMaxMindNotConfigured
		}
		return cfg.MaxMind.matchTimeZone(parts[1]), nil
	case MatcherTypeCountry:
		if cfg.MaxMind == nil {
			return nil, errMaxMindNotConfigured
		}
		return cfg.MaxMind.matchISOCode(parts[1]), nil
	default:
		return nil, errSyntax
	}
}

func matchIP(ip net.IP) matcher {
	return func(ip2 *acl.IPInfo) bool {
		return ip.Equal(ip2.IP)
	}
}

func matchCIDR(n *net.IPNet) matcher {
	return func(ip *acl.IPInfo) bool {
		return n.Contains(ip.IP)
	}
}

func (cfg *MaxMindConfig) matchTimeZone(tz string) matcher {
	return func(ip *acl.IPInfo) bool {
		city, ok := cfg.lookupCity(ip)
		if !ok {
			return false
		}
		return city.Location.TimeZone == tz
	}
}

func (cfg *MaxMindConfig) matchISOCode(iso string) matcher {
	return func(ip *acl.IPInfo) bool {
		city, ok := cfg.lookupCity(ip)
		if !ok {
			return false
		}
		return city.Country.IsoCode == iso
	}
}
