package webrtc

import (
	"encoding/base64"
	"encoding/json"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

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

const MimeSDP = "application/sdp"

var sessions = map[string]*webrtc.Conn{}

func syncHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "POST":
		query := r.URL.Query()
		if query.Get("src") != "" {
			// WHEP or JSON SDP or raw SDP exchange
			outputWebRTC(w, r)
		} else if query.Get("dst") != "" {
			// WHIP SDP exchange
			inputWebRTC(w, r)
		} else {
			http.Error(w, "", http.StatusBadRequest)
		}

	case "PATCH":
		// TODO: WHEP/WHIP
		http.Error(w, "", http.StatusMethodNotAllowed)

	case "DELETE":
		if id := r.URL.Query().Get("id"); id != "" {
			if conn, ok := sessions[id]; ok {
				delete(sessions, id)
				_ = conn.Close()
			} else {
				http.Error(w, "", http.StatusNotFound)
			}
		} else {
			http.Error(w, "", http.StatusBadRequest)
		}

	case "OPTIONS":
		w.WriteHeader(http.StatusNoContent)

	default:
		http.Error(w, "", http.StatusMethodNotAllowed)
	}
}

// outputWebRTC support API depending on Content-Type:
// 1. application/json - receive {"type":"offer","sdp":"v=0\r\n..."} and response {"type":"answer","sdp":"v=0\r\n..."}
// 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP)
// 3. other - receive/response raw SDP
func outputWebRTC(w http.ResponseWriter, r *http.Request) {
	u := r.URL.Query().Get("src")
	stream := streams.Get(u)
	if stream == nil {
		http.Error(w, api.StreamNotFound, http.StatusNotFound)
		return
	}

	mediaType := r.Header.Get("Content-Type")
	if mediaType != "" {
		mediaType, _, _ = strings.Cut(mediaType, ";")
		mediaType = strings.ToLower(strings.TrimSpace(mediaType))
	}

	var offer string

	switch mediaType {
	case "application/json":
		var desc pion.SessionDescription
		if err := json.NewDecoder(r.Body).Decode(&desc); err != nil {
			log.Error().Err(err).Caller().Send()
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		offer = desc.SDP

	case "application/x-www-form-urlencoded":
		if err := r.ParseForm(); err != nil {
			log.Error().Err(err).Caller().Send()
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		offerB64 := r.Form.Get("data")
		b, err := base64.StdEncoding.DecodeString(offerB64)
		if err != nil {
			log.Error().Err(err).Caller().Send()
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		offer = string(b)

	default:
		body, err := io.ReadAll(r.Body)
		if err != nil {
			log.Error().Err(err).Caller().Send()
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		offer = string(body)
	}

	var desc string

	switch mediaType {
	case "application/json":
		desc = "webrtc/json"
	case MimeSDP:
		desc = "webrtc/whep"
	default:
		desc = "webrtc/post"
	}

	answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
	if err != nil {
		log.Error().Err(err).Caller().Send()
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	switch mediaType {
	case "application/json":
		w.Header().Set("Content-Type", mediaType)

		v := pion.SessionDescription{
			Type: pion.SDPTypeAnswer, SDP: answer,
		}
		err = json.NewEncoder(w).Encode(v)

	case "application/x-www-form-urlencoded":
		w.Header().Set("Content-Type", mediaType)
		answerB64 := base64.StdEncoding.EncodeToString([]byte(answer))
		_, err = w.Write([]byte(answerB64))

	case MimeSDP:
		w.Header().Set("Content-Type", mediaType)
		w.WriteHeader(http.StatusCreated)

		_, err = w.Write([]byte(answer))

	default:
		w.Header().Set("Content-Type", mediaType)

		_, err = w.Write([]byte(answer))
	}

	if err != nil {
		log.Error().Err(err).Caller().Send()
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func inputWebRTC(w http.ResponseWriter, r *http.Request) {
	dst := r.URL.Query().Get("dst")
	stream := streams.Get(dst)
	if stream == nil {
		http.Error(w, api.StreamNotFound, http.StatusNotFound)
		return
	}

	// 1. Get offer
	offer, err := io.ReadAll(r.Body)
	if err != nil {
		log.Error().Err(err).Caller().Send()
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	log.Trace().Msgf("[webrtc] WHIP offer\n%s", offer)

	pc, err := PeerConnection(false)
	if err != nil {
		log.Error().Err(err).Caller().Send()
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// create new webrtc instance
	prod := webrtc.NewConn(pc)
	prod.Mode = core.ModePassiveProducer
	prod.Protocol = "http"
	prod.UserAgent = r.UserAgent()

	if err = prod.SetOffer(string(offer)); err != nil {
		log.Warn().Err(err).Caller().Send()
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	answer, err := prod.GetCompleteAnswer(GetCandidates(), FilterCandidate)
	if err != nil {
		log.Warn().Err(err).Caller().Send()
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	log.Trace().Msgf("[webrtc] WHIP answer\n%s", answer)

	id := strconv.FormatInt(time.Now().UnixNano(), 36)
	sessions[id] = prod

	prod.Listen(func(msg any) {
		switch msg := msg.(type) {
		case pion.PeerConnectionState:
			if msg == pion.PeerConnectionStateClosed {
				stream.RemoveProducer(prod)
				delete(sessions, id)
			}
		}
	})

	stream.AddProducer(prod)

	w.Header().Set("Content-Type", MimeSDP)
	w.Header().Set("Location", "webrtc?id="+id)
	w.WriteHeader(http.StatusCreated)

	if _, err = w.Write([]byte(answer)); err != nil {
		log.Warn().Err(err).Caller().Send()
		return
	}
}
