// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package server

import (
	"errors"
	"fmt"
	"net"
	"time"

	"github.com/pion/stun/v3"
	"github.com/pion/turn/v4/internal/proto"
)

const (
	// See: https://tools.ietf.org/html/rfc5766#section-6.2 defines 3600 seconds recommendation.
	maximumAllocationLifetime = time.Hour
)

func buildAndSend(conn net.PacketConn, dst net.Addr, attrs ...stun.Setter) error {
	msg, err := stun.Build(attrs...)
	if err != nil {
		return err
	}
	_, err = conn.WriteTo(msg.Raw, dst)
	if errors.Is(err, net.ErrClosed) {
		return nil
	}

	return err
}

// Send a STUN packet and return the original error to the caller.
func buildAndSendErr(conn net.PacketConn, dst net.Addr, err error, attrs ...stun.Setter) error {
	if sendErr := buildAndSend(conn, dst, attrs...); sendErr != nil {
		err = fmt.Errorf("%w %v %v", errFailedToSendError, sendErr, err) //nolint:errorlint
	}

	return err
}

func buildMsg(
	transactionID [stun.TransactionIDSize]byte,
	msgType stun.MessageType,
	additional ...stun.Setter,
) []stun.Setter {
	return append([]stun.Setter{&stun.Message{TransactionID: transactionID}, msgType}, additional...)
}

func authenticateRequest(req Request, stunMsg *stun.Message, callingMethod stun.Method) (
	stun.MessageIntegrity,
	bool,
	error,
) {
	respondWithNonce := func(responseCode stun.ErrorCode) (stun.MessageIntegrity, bool, error) {
		nonce, err := req.NonceHash.Generate()
		if err != nil {
			return nil, false, err
		}

		return nil, false, buildAndSend(req.Conn, req.SrcAddr, buildMsg(stunMsg.TransactionID,
			stun.NewType(callingMethod, stun.ClassErrorResponse),
			&stun.ErrorCodeAttribute{Code: responseCode},
			stun.NewNonce(nonce),
			stun.NewRealm(req.Realm),
		)...)
	}

	if !stunMsg.Contains(stun.AttrMessageIntegrity) {
		return respondWithNonce(stun.CodeUnauthorized)
	}

	nonceAttr := &stun.Nonce{}
	usernameAttr := &stun.Username{}
	realmAttr := &stun.Realm{}
	badRequestMsg := buildMsg(
		stunMsg.TransactionID,
		stun.NewType(callingMethod, stun.ClassErrorResponse),
		&stun.ErrorCodeAttribute{Code: stun.CodeBadRequest},
	)

	// No Auth handler is set, server is running in STUN only mode
	// Respond with 400 so clients don't retry.
	if req.AuthHandler == nil {
		sendErr := buildAndSend(req.Conn, req.SrcAddr, badRequestMsg...)

		return nil, false, sendErr
	}

	if err := nonceAttr.GetFrom(stunMsg); err != nil {
		return nil, false, buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...)
	}

	// Assert Nonce is signed and is not expired.
	if err := req.NonceHash.Validate(nonceAttr.String()); err != nil {
		return respondWithNonce(stun.CodeStaleNonce)
	}

	if err := realmAttr.GetFrom(stunMsg); err != nil {
		return nil, false, buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...)
	} else if err := usernameAttr.GetFrom(stunMsg); err != nil {
		return nil, false, buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...)
	}

	ourKey, ok := req.AuthHandler(usernameAttr.String(), realmAttr.String(), req.SrcAddr)
	if !ok {
		return nil, false, buildAndSendErr(
			req.Conn,
			req.SrcAddr,
			fmt.Errorf("%w %s", errNoSuchUser, usernameAttr.String()),
			badRequestMsg...,
		)
	}

	if err := stun.MessageIntegrity(ourKey).Check(stunMsg); err != nil {
		genAuthEvent(req, stunMsg, callingMethod, false)

		return nil, false, buildAndSendErr(req.Conn, req.SrcAddr, err, badRequestMsg...)
	}

	genAuthEvent(req, stunMsg, callingMethod, true)

	return stun.MessageIntegrity(ourKey), true, nil
}

func genAuthEvent(req Request, stunMsg *stun.Message, callingMethod stun.Method, verdict bool) {
	if req.AllocationManager.EventHandler.OnAuth == nil {
		return
	}

	realmAttr := &stun.Realm{}
	if err := realmAttr.GetFrom(stunMsg); err != nil {
		return
	}

	usernameAttr := &stun.Username{}
	if err := usernameAttr.GetFrom(stunMsg); err != nil {
		return
	}

	transportAttr := &proto.RequestedTransport{}
	if err := transportAttr.GetFrom(stunMsg); err != nil {
		transportAttr = &proto.RequestedTransport{Protocol: proto.ProtoUDP}
	}

	req.AllocationManager.EventHandler.OnAuth(req.SrcAddr, req.Conn.LocalAddr(),
		transportAttr.Protocol.String(), usernameAttr.String(), realmAttr.String(),
		callingMethod.String(), verdict)
}

func allocationLifeTime(m *stun.Message) time.Duration {
	lifetimeDuration := proto.DefaultLifetime

	var lifetime proto.Lifetime
	if err := lifetime.GetFrom(m); err == nil {
		if lifetime.Duration < maximumAllocationLifetime {
			lifetimeDuration = lifetime.Duration
		}
	}

	return lifetimeDuration
}
