// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package service

import (
	"context"
	"errors"
	"net/netip"

	"github.com/dennwc/iters"
	"github.com/twitchtv/twirp"

	"github.com/livekit/protocol/livekit"
	"github.com/livekit/protocol/logger"
	"github.com/livekit/protocol/rpc"
	"github.com/livekit/protocol/sip"
)

// matchSIPTrunk finds a SIP Trunk definition matching the request.
// Returns nil if no rules matched or an error if there are conflicting definitions.
func (s *IOInfoService) matchSIPTrunk(ctx context.Context, trunkID string, call *rpc.SIPCall) (*livekit.SIPInboundTrunkInfo, error) {
	if s.ss == nil {
		return nil, ErrSIPNotConnected
	}
	if trunkID != "" {
		// This is a best-effort optimization. Fallthrough to listing trunks if it doesn't work.
		if tr, err := s.ss.LoadSIPInboundTrunk(ctx, trunkID); err == nil {
			tr, err = sip.MatchTrunkIter(iters.Slice([]*livekit.SIPInboundTrunkInfo{tr}), call)
			if err == nil {
				return tr, nil
			}
		}
	}
	it := s.SelectSIPInboundTrunk(ctx, call.To.User)
	return sip.MatchTrunkIter(it, call)
}

func (s *IOInfoService) SelectSIPInboundTrunk(ctx context.Context, called string) iters.Iter[*livekit.SIPInboundTrunkInfo] {
	it := livekit.ListPageIter(s.ss.ListSIPInboundTrunk, &livekit.ListSIPInboundTrunkRequest{
		Numbers: []string{called},
	})
	return iters.PagesAsIter(ctx, it)
}

// matchSIPDispatchRule finds the best dispatch rule matching the request parameters. Returns an error if no rule matched.
// Trunk parameter can be nil, in which case only wildcard dispatch rules will be effective (ones without Trunk IDs).
func (s *IOInfoService) matchSIPDispatchRule(ctx context.Context, trunk *livekit.SIPInboundTrunkInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*livekit.SIPDispatchRuleInfo, error) {
	if s.ss == nil {
		return nil, ErrSIPNotConnected
	}
	var trunkID string
	if trunk != nil {
		trunkID = trunk.SipTrunkId
	}
	// Trunk can still be nil here in case none matched or were defined.
	// This is still fine, but only in case we'll match exactly one wildcard dispatch rule.
	it := s.SelectSIPDispatchRule(ctx, trunkID)
	return sip.MatchDispatchRuleIter(trunk, it, req)
}

func (s *IOInfoService) SelectSIPDispatchRule(ctx context.Context, trunkID string) iters.Iter[*livekit.SIPDispatchRuleInfo] {
	var trunkIDs []string
	if trunkID != "" {
		trunkIDs = []string{trunkID}
	}
	it := livekit.ListPageIter(s.ss.ListSIPDispatchRule, &livekit.ListSIPDispatchRuleRequest{
		TrunkIds: trunkIDs,
	})
	return iters.PagesAsIter(ctx, it)
}

func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.EvaluateSIPDispatchRulesRequest) (*rpc.EvaluateSIPDispatchRulesResponse, error) {
	call := req.SIPCall()
	log := logger.GetLogger()
	log = log.WithValues("toUser", call.To.User, "fromUser", call.From.User, "src", call.SourceIp)
	if call.SourceIp == "" {
		log.Warnw("source address is not set", nil)
		// TODO: return error in the next release
	}
	_, err := netip.ParseAddr(call.SourceIp)
	if call.SourceIp != "" && err != nil {
		log.Errorw("cannot parse source IP", err)
		return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
	}
	trunk, err := s.matchSIPTrunk(ctx, req.SipTrunkId, call)
	if err != nil {
		return nil, err
	}
	trunkID := ""
	if trunk != nil {
		trunkID = trunk.SipTrunkId
	}
	log = log.WithValues("sipTrunk", trunkID)
	if trunk != nil {
		log.Debugw("SIP trunk matched")
	} else {
		log.Debugw("No SIP trunk matched")
	}
	best, err := s.matchSIPDispatchRule(ctx, trunk, req)
	if err != nil {
		if e := (*sip.ErrNoDispatchMatched)(nil); errors.As(err, &e) {
			return &rpc.EvaluateSIPDispatchRulesResponse{
				SipTrunkId: trunkID,
				Result:     rpc.SIPDispatchResult_DROP,
			}, nil
		}
		return nil, err
	}
	log.Debugw("SIP dispatch rule matched", "sipRule", best.SipDispatchRuleId)
	resp, err := sip.EvaluateDispatchRule("", trunk, best, req)
	if err != nil {
		return nil, err
	}
	resp.Upgrade()
	resp.SipTrunkId = trunkID
	return resp, err
}

func (s *IOInfoService) GetSIPTrunkAuthentication(ctx context.Context, req *rpc.GetSIPTrunkAuthenticationRequest) (*rpc.GetSIPTrunkAuthenticationResponse, error) {
	call := req.SIPCall()
	log := logger.GetLogger()
	log = log.WithValues("toUser", call.To.User, "fromUser", call.From.User, "src", call.SourceIp)
	if call.SourceIp == "" {
		log.Warnw("source address is not set", nil)
		// TODO: return error in the next release
	}
	_, err := netip.ParseAddr(call.SourceIp)
	if call.SourceIp != "" && err != nil {
		log.Errorw("cannot parse source IP", err)
		return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
	}
	trunk, err := s.matchSIPTrunk(ctx, "", call)
	if err != nil {
		return nil, err
	}
	if trunk == nil {
		log.Debugw("No SIP trunk matched for auth", "sipTrunk", "")
		return &rpc.GetSIPTrunkAuthenticationResponse{}, nil
	}
	log.Debugw("SIP trunk matched for auth", "sipTrunk", trunk.SipTrunkId)
	return sip.InboundTrunkAuthPrompt(trunk)
}
