package geodata

import (
	"strconv"
	"strings"

	"github.com/xtls/xray-core/common/errors"
	"github.com/xtls/xray-core/common/net"
)

const (
	DefaultGeoIPDat   = "geoip.dat"
	DefaultGeoSiteDat = "geosite.dat"
)

func ParseIPRules(rules []string) ([]*IPRule, error) {
	var ipRules []*IPRule

	for i, r := range rules {
		r, reverse := cutReversePrefix(r)

		if strings.HasPrefix(r, "geoip:") {
			r = "ext:" + DefaultGeoIPDat + ":" + r[len("geoip:"):]
		}

		prefix := 0
		for _, ext := range [...]string{"ext:", "ext-ip:"} {
			if strings.HasPrefix(r, ext) {
				prefix = len(ext)
				break
			}
		}

		var rule isIPRule_Value
		var err error
		if prefix > 0 {
			rule, err = parseGeoIPRule(r[prefix:], reverse)
		} else {
			rule, err = parseCustomIPRule(r, reverse)
		}
		if err != nil {
			return nil, errors.New("illegal ip rule: ", rules[i]).Base(err)
		}
		ipRules = append(ipRules, &IPRule{Value: rule})
	}

	return ipRules, nil
}

func cutReversePrefix(s string) (string, bool) {
	reverse := false
	for strings.HasPrefix(s, "!") {
		s = s[1:]
		reverse = !reverse
	}
	return s, reverse
}

func parseGeoIPRule(rule string, reverse bool) (*IPRule_Geoip, error) {
	file, code, ok := strings.Cut(rule, ":")
	if !ok {
		return nil, errors.New("syntax error")
	}

	if file == "" {
		return nil, errors.New("empty file")
	}

	code, codeReverse := cutReversePrefix(code)
	reverse = reverse != codeReverse
	if code == "" {
		return nil, errors.New("empty code")
	}
	code = strings.ToUpper(code)

	if err := checkFile(file, code); err != nil {
		return nil, err
	}

	return &IPRule_Geoip{
		Geoip: &GeoIPRule{
			File:         file,
			Code:         code,
			ReverseMatch: reverse,
		},
	}, nil
}

func parseCustomIPRule(rule string, reverse bool) (*IPRule_Custom, error) {
	cidr, err := parseCIDR(rule)
	if err != nil {
		return nil, err
	}
	return &IPRule_Custom{
		Custom: &CIDRRule{
			Cidr:         cidr,
			ReverseMatch: reverse,
		},
	}, nil
}

func parseCIDR(s string) (*CIDR, error) {
	ipStr, prefixStr, _ := strings.Cut(s, "/")

	ipAddr := net.ParseAddress(ipStr)

	var maxPrefix uint32
	switch ipAddr.Family() {
	case net.AddressFamilyIPv4:
		maxPrefix = 32
	case net.AddressFamilyIPv6:
		maxPrefix = 128
	default:
		return nil, errors.New("unsupported address family")
	}

	prefixBits := maxPrefix
	if prefixStr != "" {
		parsedPrefix, err := strconv.ParseUint(prefixStr, 10, 32)
		if err != nil {
			return nil, errors.New("invalid CIDR prefix length: ", prefixStr).Base(err)
		}
		prefixBits = uint32(parsedPrefix)
	}
	if prefixBits > maxPrefix {
		return nil, errors.New("CIDR prefix length ", prefixBits, " exceeds max ", maxPrefix)
	}

	return &CIDR{
		Ip:     []byte(ipAddr.IP()),
		Prefix: prefixBits,
	}, nil
}

func ParseDomainRule(r string, defaultType Domain_Type) (*DomainRule, error) {
	if strings.HasPrefix(r, "geosite:") {
		r = "ext:" + DefaultGeoSiteDat + ":" + r[len("geosite:"):]
	}

	prefix := 0
	for _, ext := range [...]string{"ext:", "ext-domain:"} {
		if strings.HasPrefix(r, ext) {
			prefix = len(ext)
			break
		}
	}

	var rule isDomainRule_Value
	var err error
	if prefix > 0 {
		rule, err = parseGeoSiteRule(r[prefix:])
	} else {
		rule, err = parseCustomDomainRule(r, defaultType)
	}
	if err != nil {
		return nil, errors.New("illegal domain rule: ", r).Base(err)
	}
	return &DomainRule{Value: rule}, nil
}

func ParseDomainRules(rules []string, defaultType Domain_Type) ([]*DomainRule, error) {
	var domainRules []*DomainRule

	for i, r := range rules {
		if strings.HasPrefix(r, "geosite:") {
			r = "ext:" + DefaultGeoSiteDat + ":" + r[len("geosite:"):]
		}

		prefix := 0
		for _, ext := range [...]string{"ext:", "ext-domain:"} {
			if strings.HasPrefix(r, ext) {
				prefix = len(ext)
				break
			}
		}

		var rule isDomainRule_Value
		var err error
		if prefix > 0 {
			rule, err = parseGeoSiteRule(r[prefix:])
		} else {
			rule, err = parseCustomDomainRule(r, defaultType)
		}
		if err != nil {
			return nil, errors.New("illegal domain rule: ", rules[i]).Base(err)
		}
		domainRules = append(domainRules, &DomainRule{Value: rule})
	}

	return domainRules, nil
}

func parseGeoSiteRule(rule string) (*DomainRule_Geosite, error) {
	file, codeWithAttrs, ok := strings.Cut(rule, ":")
	if !ok {
		return nil, errors.New("syntax error")
	}

	if file == "" {
		return nil, errors.New("empty file")
	}

	if strings.HasSuffix(codeWithAttrs, "@") || strings.Contains(codeWithAttrs, "@@") {
		return nil, errors.New("empty attr")
	}
	code, attrs, _ := strings.Cut(codeWithAttrs, "@")

	if code == "" {
		return nil, errors.New("empty code")
	}
	code = strings.ToUpper(code)

	if err := checkFile(file, code); err != nil {
		return nil, err
	}

	return &DomainRule_Geosite{
		Geosite: &GeoSiteRule{
			File:  file,
			Code:  code,
			Attrs: strings.ToLower(attrs),
		},
	}, nil
}

func parseCustomDomainRule(rule string, defaultType Domain_Type) (*DomainRule_Custom, error) {
	domain := new(Domain)

	switch {
	case strings.HasPrefix(rule, "regexp:"):
		domain.Type = Domain_Regex
		domain.Value = rule[7:]

	case strings.HasPrefix(rule, "domain:"):
		domain.Type = Domain_Domain
		domain.Value = rule[7:]

	case strings.HasPrefix(rule, "full:"):
		domain.Type = Domain_Full
		domain.Value = rule[5:]

	case strings.HasPrefix(rule, "keyword:"):
		domain.Type = Domain_Substr
		domain.Value = rule[8:]

	case strings.HasPrefix(rule, "dotless:"):
		domain.Type = Domain_Regex
		switch substr := rule[8:]; {
		case substr == "":
			domain.Value = "^[^.]*$"
		case !strings.Contains(substr, "."):
			domain.Value = "^[^.]*" + substr + "[^.]*$"
		default:
			return nil, errors.New("substr in dotless rule should not contain a dot")
		}

	default:
		domain.Type = defaultType
		domain.Value = rule
	}

	return &DomainRule_Custom{
		Custom: domain,
	}, nil
}
