package geodata

import (
	"context"
	"runtime"
	"strings"
	"sync"

	"github.com/xtls/xray-core/common/errors"
	"github.com/xtls/xray-core/common/geodata/strmatcher"
	"github.com/xtls/xray-core/common/utils"
)

type DomainMatcher interface {
	// Match returns the indices of all rules that match the input domain.
	// The returned slice is owned by the caller and may be safely modified.
	// Note: the slice may contain duplicates and the order is unspecified.
	Match(input string) []uint32

	MatchAny(input string) bool
}

type DomainMatcherFactory interface {
	BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
}

type MphDomainMatcherFactory struct {
	sync.Mutex
	shared *utils.WeakCacheMap[string, strmatcher.MphValueMatcher]
}

func buildDomainRulesKey(rules []*DomainRule) string {
	var sb strings.Builder
	cache := false
	for _, r := range rules {
		switch v := r.Value.(type) {
		case *DomainRule_Custom:
			sb.WriteString(v.Custom.Type.String())
			sb.WriteString(":")
			sb.WriteString(v.Custom.Value)
			sb.WriteString(",")
		case *DomainRule_Geosite:
			cache = true
			sb.WriteString(v.Geosite.File)
			sb.WriteString(":")
			sb.WriteString(v.Geosite.Code)
			sb.WriteString("@")
			sb.WriteString(v.Geosite.Attrs)
			sb.WriteString(",")
		default:
			panic("unknown domain rule type")
		}
	}
	if !cache {
		return ""
	}
	return sb.String()
}

// BuildMatcher implements DomainMatcherFactory.
func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
	if len(rules) == 0 {
		return nil, errors.New("empty domain rule list")
	}
	key := buildDomainRulesKey(rules)
	if key != "" {
		f.Lock()
		defer f.Unlock()
		if g, ok := f.shared.Load(key); ok {
			errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules")
			return g, nil
		}
		errors.LogDebug(context.Background(), "geodata mph domain matcher cache MISS for ", len(rules), " rules")
	}
	g := strmatcher.NewMphValueMatcher()
	for i, r := range rules {
		switch v := r.Value.(type) {
		case *DomainRule_Custom:
			m, err := parseDomain(v.Custom)
			if err != nil {
				return nil, err
			}
			g.Add(m, uint32(i))
		case *DomainRule_Geosite:
			domains, err := loadSiteWithAttrs(v.Geosite.File, v.Geosite.Code, v.Geosite.Attrs)
			if err != nil {
				return nil, err
			}
			for j, d := range domains {
				domains[j] = nil // peak mem
				m, err := parseDomain(d)
				if err != nil {
					errors.LogError(context.Background(), "ignore invalid geosite entry in ", v.Geosite.File, ":", v.Geosite.Code, " at index ", j, ", ", err)
					continue
				}
				g.Add(m, uint32(i))
			}
		default:
			panic("unknown domain rule type")
		}
	}
	if err := g.Build(); err != nil {
		return nil, err
	}
	if key != "" {
		f.shared.Store(key, g)
	}
	return g, nil
}

type CompactDomainMatcherFactory struct {
	sync.Mutex
	shared *utils.WeakCacheMap[string, strmatcher.LinearAnyMatcher]
}

func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
	key := rule.File + ":" + rule.Code + "@" + rule.Attrs

	f.Lock()
	defer f.Unlock()

	if s, ok := f.shared.Load(key); ok {
		errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
		return s, nil
	}
	errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)

	s := strmatcher.NewLinearAnyMatcher()
	domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
	if err != nil {
		return nil, err
	}
	for i, d := range domains {
		domains[i] = nil // peak mem
		m, err := parseDomain(d)
		if err != nil {
			errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
			continue
		}
		s.Add(m)
	}
	f.shared.Store(key, s)
	return s, err
}

// BuildMatcher implements DomainMatcherFactory.
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
	if len(rules) == 0 {
		return nil, errors.New("empty domain rule list")
	}
	compact := &CompactDomainMatcher{
		matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
		values:   make([]uint32, 0, len(rules)),
	}
	for i, r := range rules {
		switch v := r.Value.(type) {
		case *DomainRule_Custom:
			m, err := parseDomain(v.Custom)
			if err != nil {
				return nil, err
			}
			if compact.custom == nil {
				compact.custom = strmatcher.NewLinearValueMatcher()
			}
			compact.custom.Add(m, uint32(i))
		case *DomainRule_Geosite:
			m, err := f.getOrCreateFrom(v.Geosite)
			if err != nil {
				return nil, err
			}
			compact.matchers = append(compact.matchers, m)
			compact.values = append(compact.values, uint32(i))
		default:
			panic("unknown domain rule type")
		}
	}
	return compact, nil
}

type CompactDomainMatcher struct {
	custom   strmatcher.ValueMatcher
	matchers []strmatcher.MatcherSet
	values   []uint32
}

// Match implements DomainMatcher.
func (c *CompactDomainMatcher) Match(input string) []uint32 {
	var result []uint32
	if c.custom != nil {
		result = append(result, c.custom.Match(input)...)
	}
	for i, m := range c.matchers {
		if m.MatchAny(input) {
			result = append(result, c.values[i])
		}
	}
	return result
}

// MatchAny implements DomainMatcher.
func (c *CompactDomainMatcher) MatchAny(input string) bool {
	if c.custom != nil && c.custom.MatchAny(input) {
		return true
	}
	for _, m := range c.matchers {
		if m.MatchAny(input) {
			return true
		}
	}
	return false
}

func parseDomain(d *Domain) (strmatcher.Matcher, error) {
	if d == nil {
		return nil, errors.New("domain must not be nil")
	}
	switch d.Type {
	case Domain_Substr:
		return strmatcher.Substr.New(strings.ToLower(d.Value))
	case Domain_Regex:
		return strmatcher.Regex.New(d.Value)
	case Domain_Domain:
		return strmatcher.Domain.New(strings.ToLower(d.Value))
	case Domain_Full:
		return strmatcher.Full.New(strings.ToLower(d.Value))
	default:
		return nil, errors.New("unknown domain type: ", d.Type)
	}
}

func newDomainMatcherFactory() DomainMatcherFactory {
	switch runtime.GOOS {
	case "ios", "android":
		return &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
	default:
		return &MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}
	}
}
