package webrtc

import (
	"encoding/json"
	"errors"
	"fmt"
	"hash/crc32"
	"net"
	"strconv"
	"strings"
	"time"

	"github.com/AlexxIT/go2rtc/pkg/core"
	"github.com/pion/ice/v4"
	"github.com/pion/sdp/v3"
	"github.com/pion/stun/v3"
	"github.com/pion/webrtc/v4"
)

func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media) {
	// 1. Sort medias, so video will always be before audio
	// 2. Ignore application media from Hass default lovelace card
	// 3. Ignore media without direction (inactive media)
	// 4. Inverse media direction (because it is remote peer medias list)
	for _, kind := range []string{core.KindVideo, core.KindAudio} {
		for _, md := range descriptions {
			if md.MediaName.Media != kind {
				continue
			}

			media := core.UnmarshalMedia(md)
			switch media.Direction {
			case core.DirectionSendRecv:
				media.Direction = core.DirectionRecvonly
				medias = append(medias, media)

				media = media.Clone()
				media.Direction = core.DirectionSendonly

			case core.DirectionRecvonly:
				media.Direction = core.DirectionSendonly

			case core.DirectionSendonly:
				media.Direction = core.DirectionRecvonly

			case "":
				continue
			}

			// skip non-media codecs to avoid confusing users in info and logs
			media.Codecs = SkipNonMediaCodecs(media.Codecs)

			medias = append(medias, media)
		}
	}

	return
}

func SkipNonMediaCodecs(input []*core.Codec) (output []*core.Codec) {
	for _, codec := range input {
		switch codec.Name {
		case "RTX", "RED", "ULPFEC", "FLEXFEC-03":
			continue
		case "CN", "TELEPHONE-EVENT":
			continue // https://datatracker.ietf.org/doc/html/rfc7874
		}
		// VP8, VP9, H264, H265, AV1
		// OPUS, G722, PCMU, PCMA
		output = append(output, codec)
	}
	return
}

// WithResampling - will add for consumer: PCMA/0, PCMU/0, PCM/0, PCML/0
// so it can add resampling for PCMA/PCMU and repack for PCM/PCML
func WithResampling(medias []*core.Media) []*core.Media {
	for _, media := range medias {
		if media.Kind != core.KindAudio || media.Direction != core.DirectionSendonly {
			continue
		}

		var pcma, pcmu, pcm, pcml *core.Codec

		for _, codec := range media.Codecs {
			switch codec.Name {
			case core.CodecPCMA:
				if codec.ClockRate != 0 {
					pcma = codec
				} else {
					pcma = nil
				}
			case core.CodecPCMU:
				if codec.ClockRate != 0 {
					pcmu = codec
				} else {
					pcmu = nil
				}
			case core.CodecPCM:
				pcm = codec
			case core.CodecPCML:
				pcml = codec
			}
		}

		if pcma != nil {
			pcma = pcma.Clone()
			pcma.ClockRate = 0 // reset clock rate so will match any
			media.Codecs = append(media.Codecs, pcma)
		}
		if pcmu != nil {
			pcmu = pcmu.Clone()
			pcmu.ClockRate = 0
			media.Codecs = append(media.Codecs, pcmu)
		}
		if pcma != nil && pcm == nil {
			pcm = pcma.Clone()
			pcm.Name = core.CodecPCM
			media.Codecs = append(media.Codecs, pcm)
		}
		if pcma != nil && pcml == nil {
			pcml = pcma.Clone()
			pcml.Name = core.CodecPCML
			media.Codecs = append(media.Codecs, pcml)
		}
	}

	return medias
}

func NewCandidate(network, address string) (string, error) {
	i := strings.LastIndexByte(address, ':')
	if i < 0 {
		return "", errors.New("wrong candidate: " + address)
	}
	host, port := address[:i], address[i+1:]

	i, err := strconv.Atoi(port)
	if err != nil {
		return "", err
	}

	config := &ice.CandidateHostConfig{
		Network:   network,
		Address:   host,
		Port:      i,
		Component: ice.ComponentRTP,
	}

	if network == "tcp" {
		config.TCPType = ice.TCPTypePassive
	}

	cand, err := ice.NewCandidateHost(config)
	if err != nil {
		return "", err
	}

	return "candidate:" + cand.Marshal(), nil
}

func LookupIP(address string) (string, error) {
	if strings.HasPrefix(address, "stun:") {
		ip, err := GetCachedPublicIP()
		if err != nil {
			return "", err
		}
		return ip.String() + address[4:], nil
	}

	if IsIP(address) {
		return address, nil
	}

	i := strings.IndexByte(address, ':')
	ips, err := net.LookupIP(address[:i])
	if err != nil {
		return "", err
	}
	if len(ips) == 0 {
		return "", fmt.Errorf("can't resolve: %s", address)
	}

	return ips[0].String() + address[i:], nil
}

// GetPublicIP example from https://github.com/pion/stun
func GetPublicIP(address string) (net.IP, error) {
	conn, err := net.Dial("udp", address)
	if err != nil {
		return nil, err
	}

	c, err := stun.NewClient(conn)
	if err != nil {
		return nil, err
	}

	if err = conn.SetDeadline(time.Now().Add(time.Second * 3)); err != nil {
		return nil, err
	}

	var res stun.Event

	message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
	if err = c.Do(message, func(e stun.Event) { res = e }); err != nil {
		return nil, err
	}
	if err = c.Close(); err != nil {
		return nil, err
	}

	if res.Error != nil {
		return nil, res.Error
	}

	var xorAddr stun.XORMappedAddress
	if err = xorAddr.GetFrom(res.Message); err != nil {
		return nil, err
	}

	return xorAddr.IP, nil
}

var cachedIP net.IP
var cachedTS time.Time

func GetCachedPublicIP(stuns ...string) (net.IP, error) {
	if now := time.Now(); now.After(cachedTS) {
		for _, addr := range stuns {
			if ip, _ := GetPublicIP(addr); ip != nil {
				cachedIP = ip
				cachedTS = now.Add(time.Minute * 5)
				return ip, nil
			}
		}
	}
	if cachedIP == nil {
		return nil, errors.New("webrtc: can't get public IP")
	}
	return cachedIP, nil
}

func IsIP(host string) bool {
	for _, i := range host {
		if i >= 'A' {
			return false
		}
	}
	return true
}

func MimeType(codec *core.Codec) string {
	switch codec.Name {
	case core.CodecH264:
		return webrtc.MimeTypeH264
	case core.CodecH265:
		return webrtc.MimeTypeH265
	case core.CodecVP8:
		return webrtc.MimeTypeVP8
	case core.CodecVP9:
		return webrtc.MimeTypeVP9
	case core.CodecAV1:
		return webrtc.MimeTypeAV1
	case core.CodecPCMU:
		return webrtc.MimeTypePCMU
	case core.CodecPCMA:
		return webrtc.MimeTypePCMA
	case core.CodecOpus:
		return webrtc.MimeTypeOpus
	case core.CodecG722:
		return webrtc.MimeTypeG722
	}
	panic("not implemented")
}

func CandidateICE(network, host, port string, priority uint32) string {
	// 1. Foundation
	// 2. Component, always 1 because RTP
	// 3. "udp" or "tcp"
	// 4. Priority
	// 5. Host - IP4 or IP6 or domain name
	// 6. Port
	// 7. "typ host"
	foundation := crc32.ChecksumIEEE([]byte("host" + host + network + "4"))
	s := fmt.Sprintf("candidate:%d 1 %s %d %s %s typ host", foundation, network, priority, host, port)
	if network == "tcp" {
		return s + " tcptype passive"
	}
	return s
}

// Priority = type << 24 + local << 8 + component
// https://www.rfc-editor.org/rfc/rfc8445#section-5.1.2.1

const PriorityHostUDP uint32 = 0x001F_FFFF |
	126<<24 | // udp host
	7<<21 // udp
const PriorityHostTCPPassive uint32 = 0x001F_FFFF |
	99<<24 | // tcp host
	4<<21 // tcp passive

// CandidateHostPriority (lower indexes has a higher priority)
func CandidateHostPriority(network string, index int) uint32 {
	switch network {
	case "udp":
		return PriorityHostUDP - uint32(index)
	case "tcp":
		return PriorityHostTCPPassive - uint32(index)
	}
	return 0
}

func UnmarshalICEServers(b []byte) ([]webrtc.ICEServer, error) {
	type ICEServer struct {
		URLs       any    `json:"urls"`
		Username   string `json:"username,omitempty"`
		Credential string `json:"credential,omitempty"`
	}

	var src []ICEServer
	if err := json.Unmarshal(b, &src); err != nil {
		return nil, err
	}

	var dst []webrtc.ICEServer
	for i := range src {
		srv := webrtc.ICEServer{
			Username:   src[i].Username,
			Credential: src[i].Credential,
		}

		switch v := src[i].URLs.(type) {
		case []any:
			for _, u := range v {
				if s, ok := u.(string); ok {
					srv.URLs = append(srv.URLs, s)
				}
			}
		case string:
			srv.URLs = []string{v}
		}

		dst = append(dst, srv)
	}

	return dst, nil
}
