package common

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"sort"
	"strings"

	"github.com/nitezs/sub2sing-box/constant"
	"github.com/nitezs/sub2sing-box/model"
	"github.com/nitezs/sub2sing-box/parser"
	"github.com/nitezs/sub2sing-box/util"
	box "github.com/sagernet/sing-box"
	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing-box/include"
	"github.com/sagernet/sing-box/option"
	J "github.com/sagernet/sing/common/json"
)

var globalCtx = box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry())

func Convert(
	subscriptions []string,
	proxies []string,
	templatePath string,
	delete string,
	rename map[string]string,
	enableGroup bool,
	groupType string,
	sortKey string,
	sortType string,
	groupRules map[string][]string,
) (string, error) {
	result := ""
	var err error

	if groupType == "" {
		groupType = C.TypeSelector
	}

	outbounds, err := ConvertSubscriptionsToSProxy(subscriptions)
	if err != nil {
		return "", err
	}
	for _, proxy := range proxies {
		p, err := ConvertCProxyToSProxy(proxy)
		if err != nil {
			return "", err
		}
		outbounds = append(outbounds, p)
	}

	if delete != "" {
		outbounds, err = DeleteProxy(outbounds, delete)
		if err != nil {
			return "", err
		}
	}

	for k, v := range rename {
		outbounds, err = RenameProxy(outbounds, k, v)
		if err != nil {
			return "", err
		}
	}

	set := make(map[string]bool)
	deduplicatedOutbounds := make([]model.Outbound, 0)
	for _, p := range outbounds {
		jsonBytes, err := json.Marshal(p)
		if err != nil {
			return "", err
		}
		if _, exists := set[string(jsonBytes)]; !exists {
			set[string(jsonBytes)] = true
			deduplicatedOutbounds = append(deduplicatedOutbounds, p)
		}
	}
	outbounds = deduplicatedOutbounds

	tagSet := make(map[string]bool)
	for i, p := range outbounds {
		if _, exists := tagSet[p.Tag]; exists {
			count := 1
			for {
				newTag := fmt.Sprintf("%s %d", p.Tag, count)
				if _, exists := tagSet[newTag]; !exists {
					outbounds[i].Tag = newTag
					break
				} else {
					count++
				}
			}
		}
	}

	if enableGroup {
		outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType, groupRules)
	}
	if templatePath != "" {
		templateData, err := ReadTemplate(templatePath)
		if err != nil {
			return "", err
		}
		reg := regexp.MustCompile("\"<[A-Za-z]{2}>\"")
		group := false
		for _, v := range model.CountryEnglishName {
			if strings.Contains(templateData, v) {
				group = true
			}
		}
		if !enableGroup && (reg.MatchString(templateData) || strings.Contains(templateData, constant.AllCountryTags) || group) {
			outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType, groupRules)
		}
		var template model.Options
		if template, err = J.UnmarshalExtendedContext[model.Options](globalCtx, []byte(templateData)); err != nil {
			return "", err
		}
		for _, v := range template.Options.Outbounds {
			template.Outbounds = append(template.Outbounds, (model.Outbound)(v))
		}
		for _, v := range template.Options.Inbounds {
			template.Inbounds = append(template.Inbounds, (model.Inbound)(v))
		}
		for _, v := range template.Options.Endpoints {
			template.Endpoints = append(template.Endpoints, (model.Endpoint)(v))
		}
		result, err = MergeTemplate(outbounds, &template, groupRules)
		if err != nil {
			return "", err
		}
	} else {
		outboundJsons := make([]string, 0)
		for _, p := range outbounds {
			b, err := json.Marshal(p)
			if err != nil {
				return "", err
			}
			outboundJsons = append(outboundJsons, string(b))
		}
		result = fmt.Sprintf("[%s]", strings.Join(outboundJsons, ","))
		if err != nil {
			return "", err
		}
	}

	return string(result), nil
}

func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, sortType string, groupRules map[string][]string) []model.Outbound {
	newGroup := make(map[string]model.Outbound)
	groupRulesRegexps := make(map[string][]*regexp.Regexp)
	for k, v := range groupRules {
		for _, rule := range v {
			groupRulesRegexps[k] = append(groupRulesRegexps[k], regexp.MustCompile(rule))
		}
	}
	for _, p := range proxies {
		if p.Type != C.TypeSelector && p.Type != C.TypeURLTest {
			country := model.GetContryName(p.Tag)
			for k, rules := range groupRulesRegexps {
				for _, rule := range rules {
					if rule.MatchString(p.Tag) {
						country = k
						break
					}
				}
			}
			if group, ok := newGroup[country]; ok {
				AppendOutbound(&group, p.Tag)
				newGroup[country] = group
			} else {
				if groupType == C.TypeSelector {
					newGroup[country] = model.Outbound{
						Tag:  country,
						Type: groupType,
						Options: option.SelectorOutboundOptions{
							Outbounds:                 []string{p.Tag},
							InterruptExistConnections: true,
						},
					}
				} else if groupType == C.TypeURLTest {
					newGroup[country] = model.Outbound{
						Tag:  country,
						Type: groupType,
						Options: option.URLTestOutboundOptions{
							Outbounds:                 []string{p.Tag},
							InterruptExistConnections: true,
						},
					}
				}
			}
		}
	}
	var groups []model.Outbound
	for _, p := range newGroup {
		groups = append(groups, p)
	}
	if sortType != "" {
		if sortType == "asc" {
			switch sortKey {
			case "tag":
				sort.Sort(model.SortByTag(groups))
			case "num":
				sort.Sort(model.SortByNumber(groups))
			default:
				sort.Sort(model.SortByTag(groups))
			}
		} else {
			switch sortKey {
			case "tag":
				sort.Sort(sort.Reverse(model.SortByTag(groups)))
			case "num":
				sort.Sort(sort.Reverse(model.SortByNumber(groups)))
			default:
				sort.Sort(sort.Reverse(model.SortByTag(groups)))
			}
		}
	}
	return append(proxies, groups...)
}

func ReadTemplate(template string) (string, error) {
	var data string
	var err error
	isNetworkFile, _ := regexp.MatchString(`^https?://`, template)
	if isNetworkFile {
		data, err = util.Fetch(template, 3)
		if err != nil {
			return "", err
		}
		return data, nil
	} else {
		if !strings.Contains(template, string(filepath.Separator)) {
			path := filepath.Join("templates", template)
			if _, err := os.Stat(path); err == nil {
				template = path
			}
		}
		dataBytes, err := os.ReadFile(template)
		if err != nil {
			return "", err
		}
		return string(dataBytes), nil
	}
}

func MergeTemplate(outbounds []model.Outbound, template *model.Options, groupRules map[string][]string) (string, error) {
	var err error
	proxyTags := make([]string, 0)
	groupTags := make([]string, 0)
	groups := make(map[string]model.Outbound)
	rulesKeys := make([]string, 0)
	for k := range groupRules {
		rulesKeys = append(rulesKeys, k)
	}
	for _, p := range outbounds {
		if slices.Contains(rulesKeys, p.Tag) || model.IsCountryGroup(p.Tag) {
			groupTags = append(groupTags, p.Tag)
			reg := regexp.MustCompile("[A-Za-z]{2}")
			country := reg.FindString(p.Tag)
			groups[country] = p
		} else {
			proxyTags = append(proxyTags, p.Tag)
		}
	}
	reg := regexp.MustCompile("<[A-Za-z]{2}>")
	for i, o := range template.Outbounds {
		outbound := (model.Outbound)(o)
		if outbound.Type == C.TypeSelector || outbound.Type == C.TypeURLTest {
			var parsedOutbound []string = make([]string, 0)
			for _, o := range GetOutbounds(&outbound) {
				if o == constant.AllProxyTags {
					parsedOutbound = append(parsedOutbound, proxyTags...)
				} else if o == constant.AllCountryTags {
					parsedOutbound = append(parsedOutbound, groupTags...)
				} else if reg.MatchString(o) {
					country := strings.ToUpper(strings.Trim(reg.FindString(o), "<>"))
					if group, ok := groups[country]; ok {
						parsedOutbound = append(parsedOutbound, GetOutbounds(&group)...)
					}
				} else {
					parsedOutbound = append(parsedOutbound, o)
				}
			}
			SetOutbounds(&template.Outbounds[i], parsedOutbound)
		}
	}
	template.Outbounds = append(template.Outbounds, outbounds...)

	for i := range template.DNS.Rules {
		if template.DNS.Rules[i].Type == "" {
			template.DNS.Rules[i].Type = C.RuleTypeDefault
		}
	}

	data, err := json.Marshal(template)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func ConvertCProxyToSProxy(proxy string) (model.Outbound, error) {
	for prefix, parseFunc := range parser.ParserMap {
		if strings.HasPrefix(proxy, prefix) {
			proxy, err := parseFunc(proxy)
			if err != nil {
				return model.Outbound{}, err
			}
			return proxy, nil
		}
	}
	return model.Outbound{}, errors.New("unknown proxy format")
}

func ConvertSubscriptionsToSProxy(urls []string) ([]model.Outbound, error) {
	proxyList := make([]model.Outbound, 0)
	for _, url := range urls {
		data, err := util.Fetch(url, 3)
		if err != nil {
			return nil, err
		}
		proxy := data
		if !strings.Contains(data, "://") {
			proxy, err = util.DecodeBase64(data)
		}
		if err != nil {
			return nil, err
		}
		proxies := strings.Split(proxy, "\n")
		for _, p := range proxies {
			for prefix, parseFunc := range parser.ParserMap {
				if strings.HasPrefix(p, prefix) {
					proxy, err := parseFunc(p)
					if err != nil {
						return nil, err
					}
					proxyList = append(proxyList, proxy)
				}
			}
		}
	}
	return proxyList, nil
}

func DeleteProxy(proxies []model.Outbound, regex string) ([]model.Outbound, error) {
	reg, err := regexp.Compile(regex)
	if err != nil {
		return nil, err
	}
	var newProxies []model.Outbound
	for _, p := range proxies {
		if !reg.MatchString(p.Tag) {
			newProxies = append(newProxies, p)
		}
	}
	return newProxies, nil
}

func RenameProxy(proxies []model.Outbound, regex string, replaceText string) ([]model.Outbound, error) {
	reg, err := regexp.Compile(regex)
	if err != nil {
		return nil, err
	}
	for i, p := range proxies {
		if reg.MatchString(p.Tag) {
			proxies[i].Tag = reg.ReplaceAllString(p.Tag, replaceText)
		}
	}
	return proxies, nil
}

func SetOutbounds(outbound *model.Outbound, outbounds []string) {
	switch v := outbound.Options.(type) {
	case option.SelectorOutboundOptions:
		v.Outbounds = outbounds
		outbound.Options = v
	case option.URLTestOutboundOptions:
		v.Outbounds = outbounds
		outbound.Options = v
	case *option.SelectorOutboundOptions:
		v.Outbounds = outbounds
		outbound.Options = v
	case *option.URLTestOutboundOptions:
		v.Outbounds = outbounds
		outbound.Options = v
	}
}

func AppendOutbound(outbound *model.Outbound, outboundTag string) {
	switch v := outbound.Options.(type) {
	case option.SelectorOutboundOptions:
		v.Outbounds = append(v.Outbounds, outboundTag)
		outbound.Options = v
	case option.URLTestOutboundOptions:
		v.Outbounds = append(v.Outbounds, outboundTag)
		outbound.Options = v
	case *option.SelectorOutboundOptions:
		v.Outbounds = append(v.Outbounds, outboundTag)
		outbound.Options = v
	case *option.URLTestOutboundOptions:
		v.Outbounds = append(v.Outbounds, outboundTag)
		outbound.Options = v
	}
}

func GetOutbounds(outbound *model.Outbound) []string {
	switch v := outbound.Options.(type) {
	case option.SelectorOutboundOptions:
		return v.Outbounds
	case option.URLTestOutboundOptions:
		return v.Outbounds
	case *option.SelectorOutboundOptions:
		return v.Outbounds
	case *option.URLTestOutboundOptions:
		return v.Outbounds
	}
	return nil
}
