package webrtc

import (
	"net"
	"strings"

	"github.com/AlexxIT/go2rtc/internal/api/ws"
	"github.com/AlexxIT/go2rtc/pkg/core"
	"github.com/AlexxIT/go2rtc/pkg/webrtc"
	"github.com/AlexxIT/go2rtc/pkg/xnet"
	pion "github.com/pion/webrtc/v4"
)

type Address struct {
	host     string
	Port     string
	Network  string
	Priority uint32
}

var stuns []string

func (a *Address) Host() string {
	if a.host == "stun" {
		ip, err := webrtc.GetCachedPublicIP(stuns...)
		if err != nil {
			return ""
		}
		return ip.String()
	}
	return a.host
}

func (a *Address) Marshal() string {
	if host := a.Host(); host != "" {
		return webrtc.CandidateICE(a.Network, host, a.Port, a.Priority)
	}
	return ""
}

var addresses []*Address
var filters webrtc.Filters

func AddCandidate(network, address string) {
	if network == "" {
		AddCandidate("tcp", address)
		AddCandidate("udp", address)
		return
	}

	host, port, err := net.SplitHostPort(address)
	if err != nil {
		return
	}

	// start from 1, so manual candidates will be lower than built-in
	// and every next candidate will have a lower priority
	candidateIndex := 1 + len(addresses)

	priority := webrtc.CandidateHostPriority(network, candidateIndex)
	addresses = append(addresses, &Address{host, port, network, priority})
}

func GetCandidates() (candidates []string) {
	for _, address := range addresses {
		if candidate := address.Marshal(); candidate != "" {
			candidates = append(candidates, candidate)
		}
	}
	return
}

// FilterCandidate return true if candidate passed the check
func FilterCandidate(candidate *pion.ICECandidate) bool {
	if candidate == nil {
		return false
	}

	// remove any Docker-like IP from candidates
	if ip := net.ParseIP(candidate.Address); ip != nil && xnet.Docker.Contains(ip) {
		return false
	}

	// host candidate should be in the hosts list
	if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {
		if !core.Contains(filters.Candidates, candidate.Address) {
			return false
		}
	}

	if filters.Networks != nil {
		networkType := NetworkType(candidate.Protocol.String(), candidate.Address)
		if !core.Contains(filters.Networks, networkType) {
			return false
		}
	}

	return true
}

// NetworkType convert tcp/udp network to tcp4/tcp6/udp4/udp6
func NetworkType(network, host string) string {
	if strings.IndexByte(host, ':') >= 0 {
		return network + "6"
	} else {
		return network + "4"
	}
}

func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
	tr.WithContext(func(ctx map[any]any) {
		if candidates, ok := ctx["candidate"].([]string); ok {
			// process candidates that receive before this moment
			for _, candidate := range candidates {
				_ = cons.AddCandidate(candidate)
			}

			// remove already processed candidates
			delete(ctx, "candidate")
		}

		// set variable for process candidates after this moment
		ctx["webrtc"] = cons
	})

	for _, candidate := range GetCandidates() {
		log.Trace().Str("candidate", candidate).Msg("[webrtc] config")
		tr.Write(&ws.Message{Type: "webrtc/candidate", Value: candidate})
	}
}

func candidateHandler(tr *ws.Transport, msg *ws.Message) error {
	// process incoming candidate in sync function
	tr.WithContext(func(ctx map[any]any) {
		candidate := msg.String()
		log.Trace().Str("candidate", candidate).Msg("[webrtc] remote")

		if cons, ok := ctx["webrtc"].(*webrtc.Conn); ok {
			// if webrtc.Server already initialized - process candidate
			_ = cons.AddCandidate(candidate)
		} else {
			// or collect candidate and process it later
			list, _ := ctx["candidate"].([]string)
			ctx["candidate"] = append(list, candidate)
		}
	})

	return nil
}
