package rule

import (
	"context"
	"encoding/json"
	"strconv"
	"strings"

	"github.com/v2fly/v2ray-core/v5/app/router"
	"github.com/v2fly/v2ray-core/v5/app/router/routercommon"
	"github.com/v2fly/v2ray-core/v5/common/net"
	"github.com/v2fly/v2ray-core/v5/infra/conf/cfgcommon"
)

//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen

func parseDomainRule(ctx context.Context, domain string) ([]*routercommon.Domain, error) {
	cfgEnv := cfgcommon.GetConfigureLoadingEnvironment(ctx)
	geoLoader := cfgEnv.GetGeoLoader()

	if strings.HasPrefix(domain, "geosite:") {
		list := domain[8:]
		if len(list) == 0 {
			return nil, newError("empty listname in rule: ", domain)
		}
		domains, err := geoLoader.LoadGeoSite(list)
		if err != nil {
			return nil, newError("failed to load geosite: ", list).Base(err)
		}

		return domains, nil
	}

	isExtDatFile := 0
	{
		const prefix = "ext:"
		if strings.HasPrefix(domain, prefix) {
			isExtDatFile = len(prefix)
		}
		const prefixQualified = "ext-domain:"
		if strings.HasPrefix(domain, prefixQualified) {
			isExtDatFile = len(prefixQualified)
		}
	}

	if isExtDatFile != 0 {
		kv := strings.Split(domain[isExtDatFile:], ":")
		if len(kv) != 2 {
			return nil, newError("invalid external resource: ", domain)
		}
		filename := kv[0]
		list := kv[1]
		domains, err := geoLoader.LoadGeoSiteWithAttr(filename, list)
		if err != nil {
			return nil, newError("failed to load external geosite: ", list, " from ", filename).Base(err)
		}

		return domains, nil
	}

	domainRule := new(routercommon.Domain)
	switch {
	case strings.HasPrefix(domain, "regexp:"):
		regexpVal := domain[7:]
		if len(regexpVal) == 0 {
			return nil, newError("empty regexp type of rule: ", domain)
		}
		domainRule.Type = routercommon.Domain_Regex
		domainRule.Value = regexpVal

	case strings.HasPrefix(domain, "domain:"):
		domainName := domain[7:]
		if len(domainName) == 0 {
			return nil, newError("empty domain type of rule: ", domain)
		}
		domainRule.Type = routercommon.Domain_RootDomain
		domainRule.Value = domainName

	case strings.HasPrefix(domain, "full:"):
		fullVal := domain[5:]
		if len(fullVal) == 0 {
			return nil, newError("empty full domain type of rule: ", domain)
		}
		domainRule.Type = routercommon.Domain_Full
		domainRule.Value = fullVal

	case strings.HasPrefix(domain, "keyword:"):
		keywordVal := domain[8:]
		if len(keywordVal) == 0 {
			return nil, newError("empty keyword type of rule: ", domain)
		}
		domainRule.Type = routercommon.Domain_Plain
		domainRule.Value = keywordVal

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

	default:
		domainRule.Type = routercommon.Domain_Plain
		domainRule.Value = domain
	}
	return []*routercommon.Domain{domainRule}, nil
}

func toCidrList(ctx context.Context, ips cfgcommon.StringList) ([]*routercommon.GeoIP, error) {
	cfgEnv := cfgcommon.GetConfigureLoadingEnvironment(ctx)
	geoLoader := cfgEnv.GetGeoLoader()

	var geoipList []*routercommon.GeoIP
	var customCidrs []*routercommon.CIDR

	for _, ip := range ips {
		if strings.HasPrefix(ip, "geoip:") {
			country := ip[6:]
			isReverseMatch := false
			if strings.HasPrefix(ip, "geoip:!") {
				country = ip[7:]
				isReverseMatch = true
			}
			if len(country) == 0 {
				return nil, newError("empty country name in rule")
			}
			geoip, err := geoLoader.LoadGeoIP(country)
			if err != nil {
				return nil, newError("failed to load geoip: ", country).Base(err)
			}

			geoipList = append(geoipList, &routercommon.GeoIP{
				CountryCode:  strings.ToUpper(country),
				Cidr:         geoip,
				InverseMatch: isReverseMatch,
			})

			continue
		}

		isExtDatFile := 0
		{
			const prefix = "ext:"
			if strings.HasPrefix(ip, prefix) {
				isExtDatFile = len(prefix)
			}
			const prefixQualified = "ext-ip:"
			if strings.HasPrefix(ip, prefixQualified) {
				isExtDatFile = len(prefixQualified)
			}
		}

		if isExtDatFile != 0 {
			kv := strings.Split(ip[isExtDatFile:], ":")
			if len(kv) != 2 {
				return nil, newError("invalid external resource: ", ip)
			}

			filename := kv[0]
			country := kv[1]
			if len(filename) == 0 || len(country) == 0 {
				return nil, newError("empty filename or empty country in rule")
			}

			isInverseMatch := false
			if strings.HasPrefix(country, "!") {
				country = country[1:]
				isInverseMatch = true
			}
			geoip, err := geoLoader.LoadIP(filename, country)
			if err != nil {
				return nil, newError("failed to load geoip: ", country, " from ", filename).Base(err)
			}

			geoipList = append(geoipList, &routercommon.GeoIP{
				CountryCode:  strings.ToUpper(filename + "_" + country),
				Cidr:         geoip,
				InverseMatch: isInverseMatch,
			})

			continue
		}

		ipRule, err := ParseIP(ip)
		if err != nil {
			return nil, newError("invalid IP: ", ip).Base(err)
		}
		customCidrs = append(customCidrs, ipRule)
	}

	if len(customCidrs) > 0 {
		geoipList = append(geoipList, &routercommon.GeoIP{
			Cidr: customCidrs,
		})
	}

	return geoipList, nil
}

func parseFieldRule(ctx context.Context, msg json.RawMessage) (*router.RoutingRule, error) {
	type RawFieldRule struct {
		RouterRule
		Domain     *cfgcommon.StringList  `json:"domain"`
		Domains    *cfgcommon.StringList  `json:"domains"`
		IP         *cfgcommon.StringList  `json:"ip"`
		Port       *cfgcommon.PortList    `json:"port"`
		Network    *cfgcommon.NetworkList `json:"network"`
		SourceIP   *cfgcommon.StringList  `json:"source"`
		SourcePort *cfgcommon.PortList    `json:"sourcePort"`
		User       *cfgcommon.StringList  `json:"user"`
		InboundTag *cfgcommon.StringList  `json:"inboundTag"`
		Protocols  *cfgcommon.StringList  `json:"protocol"`
		Attributes string                 `json:"attrs"`
	}
	rawFieldRule := new(RawFieldRule)
	err := json.Unmarshal(msg, rawFieldRule)
	if err != nil {
		return nil, err
	}

	rule := new(router.RoutingRule)
	switch {
	case len(rawFieldRule.OutboundTag) > 0:
		rule.TargetTag = &router.RoutingRule_Tag{
			Tag: rawFieldRule.OutboundTag,
		}
	case len(rawFieldRule.BalancerTag) > 0:
		rule.TargetTag = &router.RoutingRule_BalancingTag{
			BalancingTag: rawFieldRule.BalancerTag,
		}
	default:
		return nil, newError("neither outboundTag nor balancerTag is specified in routing rule")
	}

	if rawFieldRule.DomainMatcher != "" {
		rule.DomainMatcher = rawFieldRule.DomainMatcher
	}

	if rawFieldRule.Domain != nil {
		for _, domain := range *rawFieldRule.Domain {
			rules, err := parseDomainRule(ctx, domain)
			if err != nil {
				return nil, newError("failed to parse domain rule: ", domain).Base(err)
			}
			rule.Domain = append(rule.Domain, rules...)
		}
	}

	if rawFieldRule.Domains != nil {
		for _, domain := range *rawFieldRule.Domains {
			rules, err := parseDomainRule(ctx, domain)
			if err != nil {
				return nil, newError("failed to parse domain rule: ", domain).Base(err)
			}
			rule.Domain = append(rule.Domain, rules...)
		}
	}

	if rawFieldRule.IP != nil {
		geoipList, err := toCidrList(ctx, *rawFieldRule.IP)
		if err != nil {
			return nil, err
		}
		rule.Geoip = geoipList
	}

	if rawFieldRule.Port != nil {
		rule.PortList = rawFieldRule.Port.Build()
	}

	if rawFieldRule.Network != nil {
		rule.Networks = rawFieldRule.Network.Build()
	}

	if rawFieldRule.SourceIP != nil {
		geoipList, err := toCidrList(ctx, *rawFieldRule.SourceIP)
		if err != nil {
			return nil, err
		}
		rule.SourceGeoip = geoipList
	}

	if rawFieldRule.SourcePort != nil {
		rule.SourcePortList = rawFieldRule.SourcePort.Build()
	}

	if rawFieldRule.User != nil {
		for _, s := range *rawFieldRule.User {
			rule.UserEmail = append(rule.UserEmail, s)
		}
	}

	if rawFieldRule.InboundTag != nil {
		for _, s := range *rawFieldRule.InboundTag {
			rule.InboundTag = append(rule.InboundTag, s)
		}
	}

	if rawFieldRule.Protocols != nil {
		for _, s := range *rawFieldRule.Protocols {
			rule.Protocol = append(rule.Protocol, s)
		}
	}

	if len(rawFieldRule.Attributes) > 0 {
		rule.Attributes = rawFieldRule.Attributes
	}

	return rule, nil
}

func ParseRule(ctx context.Context, msg json.RawMessage) (*router.RoutingRule, error) {
	rawRule := new(RouterRule)
	err := json.Unmarshal(msg, rawRule)
	if err != nil {
		return nil, newError("invalid router rule").Base(err)
	}
	if strings.EqualFold(rawRule.Type, "field") {
		fieldrule, err := parseFieldRule(ctx, msg)
		if err != nil {
			return nil, newError("invalid field rule").Base(err)
		}
		return fieldrule, nil
	}

	return nil, newError("unknown router rule type: ", rawRule.Type)
}

func ParseIP(s string) (*routercommon.CIDR, error) {
	var addr, mask string
	i := strings.Index(s, "/")
	if i < 0 {
		addr = s
	} else {
		addr = s[:i]
		mask = s[i+1:]
	}
	ip := net.ParseAddress(addr)
	switch ip.Family() {
	case net.AddressFamilyIPv4:
		bits := uint32(32)
		if len(mask) > 0 {
			bits64, err := strconv.ParseUint(mask, 10, 32)
			if err != nil {
				return nil, newError("invalid network mask for router: ", mask).Base(err)
			}
			bits = uint32(bits64)
		}
		if bits > 32 {
			return nil, newError("invalid network mask for router: ", bits)
		}
		return &routercommon.CIDR{
			Ip:     []byte(ip.IP()),
			Prefix: bits,
		}, nil
	case net.AddressFamilyIPv6:
		bits := uint32(128)
		if len(mask) > 0 {
			bits64, err := strconv.ParseUint(mask, 10, 32)
			if err != nil {
				return nil, newError("invalid network mask for router: ", mask).Base(err)
			}
			bits = uint32(bits64)
		}
		if bits > 128 {
			return nil, newError("invalid network mask for router: ", bits)
		}
		return &routercommon.CIDR{
			Ip:     []byte(ip.IP()),
			Prefix: bits,
		}, nil
	default:
		return nil, newError("unsupported address for router: ", s)
	}
}

func ParseDomainRule(ctx context.Context, domain string) ([]*routercommon.Domain, error) {
	return parseDomainRule(ctx, domain)
}

func ToCidrList(ctx context.Context, ips cfgcommon.StringList) ([]*routercommon.GeoIP, error) {
	return toCidrList(ctx, ips)
}

type RouterRule struct {
	Type        string `json:"type"`
	OutboundTag string `json:"outboundTag"`
	BalancerTag string `json:"balancerTag"`

	DomainMatcher string `json:"domainMatcher"`
}
