package tgtest

import (
	"context"
	"math"

	"github.com/go-faster/errors"

	"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
	"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
	"go.mau.fi/mautrix-telegram/pkg/gotd/mt"
	"go.mau.fi/mautrix-telegram/pkg/gotd/proto"
	"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
	"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
)

const (
	// MessageServerResponse is a message type of RPC calls result.
	MessageServerResponse = proto.MessageServerResponse
	// MessageFromServer is a message type of server-side updates.
	MessageFromServer = proto.MessageFromServer
)

// Send sends given message to user session k.
// Parameter t denotes MTProto message type. It should be MessageServerResponse or MessageFromServer.
func (s *Server) Send(ctx context.Context, k Session, t proto.MessageType, message bin.Encoder) error {
	conn, ok := s.users.getConnection(k.ID)
	if !ok {
		return errors.Errorf("send %T: invalid key: connection %s not found", message, k.AuthKey.String())
	}

	var b bin.Buffer
	if err := message.Encode(&b); err != nil {
		return errors.Wrap(err, "encode")
	}

	data := crypto.EncryptedMessageData{
		SessionID:              k.ID,
		MessageDataLen:         int32(b.Len()),
		MessageDataWithPadding: b.Copy(),
		MessageID:              s.msgID.New(t),
	}

	err := s.cipher.Encrypt(k.AuthKey, data, &b)
	if err != nil {
		return errors.Wrap(err, "encrypt")
	}

	ctx, cancel := context.WithTimeout(ctx, s.writeTimeout)
	defer cancel()

	if err := conn.Send(ctx, &b); err != nil {
		return errors.Wrap(err, "send")
	}

	return nil
}

func (s *Server) sendReq(req *Request, t proto.MessageType, encoder bin.Encoder) error {
	return s.Send(req.RequestCtx, req.Session, t, encoder)
}

// SendResult sends RPC answer using msg as result.
func (s *Server) SendResult(req *Request, msg bin.Encoder) error {
	var buf bin.Buffer

	if err := msg.Encode(&buf); err != nil {
		return errors.Wrap(err, "encode result")
	}

	if err := s.sendReq(req, proto.MessageServerResponse, &proto.Result{
		RequestMessageID: req.MsgID,
		Result:           buf.Raw(),
	}); err != nil {
		return errors.Wrapf(err, "send result [%T]", msg)
	}

	return nil
}

// SendGZIP sends RPC answer and packs it into proto.GZIP.
func (s *Server) SendGZIP(req *Request, msg bin.Encoder) error {
	var buf bin.Buffer

	if err := msg.Encode(&buf); err != nil {
		return errors.Wrap(err, "encode gzip data")
	}

	return s.SendResult(req, proto.GZIP{Data: buf.Buf})
}

// SendErr sends RPC answer using given error as result.
func (s *Server) SendErr(req *Request, e *tgerr.Error) error {
	return s.SendResult(req, &mt.RPCError{
		ErrorCode:    e.Code,
		ErrorMessage: e.Message,
	})
}

// SendBool sends RPC answer using given bool as result.
// Usually used in methods without explicit response.
func (s *Server) SendBool(req *Request, r bool) error {
	var msg tg.BoolClass = &tg.BoolTrue{}
	if !r {
		msg = &tg.BoolFalse{}
	}
	return s.SendResult(req, msg)
}

// SendVector sends RPC answer using given vector as result.
func (s *Server) SendVector(req *Request, msgs ...bin.Encoder) error {
	return s.SendResult(req, &genericVector{Elems: msgs})
}

// sendSessionCreated sends mt.NewSessionCreated `new_session_created` notification.
func (s *Server) sendSessionCreated(ctx context.Context, k Session, serverSalt int64) error {
	if err := s.Send(ctx, k, proto.MessageFromServer, &mt.NewSessionCreated{
		FirstMsgID: s.msgID.New(proto.MessageFromClient),
		ServerSalt: serverSalt,
	}); err != nil {
		return errors.Wrap(err, "send sessionCreated")
	}

	return nil
}

// SendPong sends response for mt.PingRequest request.
func (s *Server) SendPong(req *Request, pingID int64) error {
	if err := s.sendReq(req, proto.MessageServerResponse, &mt.Pong{
		MsgID:  req.MsgID,
		PingID: pingID,
	}); err != nil {
		return errors.Wrap(err, "send pong")
	}

	return nil
}

// SendEternalSalt sends response for mt.GetFutureSaltsRequest.
// It sends an `eternal` salt, which valid until maximum possible date.
func (s *Server) SendEternalSalt(req *Request) error {
	return s.SendFutureSalts(req, mt.FutureSalt{
		ValidSince: 1,
		ValidUntil: math.MaxInt32,
		Salt:       10,
	})
}

// SendFutureSalts sends response for mt.GetFutureSaltsRequest.
func (s *Server) SendFutureSalts(req *Request, salts ...mt.FutureSalt) error {
	if err := s.Send(req.RequestCtx, req.Session, proto.MessageServerResponse, &mt.FutureSalts{
		ReqMsgID: req.MsgID,
		Now:      int(s.clock.Now().Unix()),
		Salts:    salts,
	}); err != nil {
		return errors.Wrap(err, "send future salts")
	}

	return nil
}

// SendUpdates sends given updates to user session k.
func (s *Server) SendUpdates(ctx context.Context, k Session, updates ...tg.UpdateClass) error {
	if len(updates) == 0 {
		return nil
	}

	if err := s.Send(ctx, k, proto.MessageFromServer, &tg.Updates{
		Updates: updates,
		Date:    int(s.clock.Now().Unix()),
	}); err != nil {
		return errors.Wrap(err, "send updates")
	}

	return nil
}

// SendAck sends acknowledgment for received message.
func (s *Server) SendAck(ctx context.Context, k Session, ids ...int64) error {
	if err := s.Send(ctx, k, proto.MessageFromServer, &mt.MsgsAck{MsgIDs: ids}); err != nil {
		return errors.Wrap(err, "send ack")
	}

	return nil
}

// ForceDisconnect forcibly disconnect user from server.
// It deletes MTProto session (session_id), but not auth key.
func (s *Server) ForceDisconnect(k Session) {
	s.users.deleteConnection(k.ID)
}
