package websocket

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/kafka"
	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/fiskerinc/cloud-services/pkg/security"
	"github.com/gobwas/ws"
	"github.com/pkg/errors"
	"google.golang.org/protobuf/proto"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)

// NewHMISession serves as the constructor for HMI sessions
func NewHMISession(w http.ResponseWriter, r *http.Request, vin string, version string) (SessionInterface, error) {
	var s SessionInterface
	conn, _, _, err := ws.UpgradeHTTP(r, w)
	if err != nil {
		return s, errors.WithStack(err)
	}

	return &SessionHMI{
		Session: &Session{
			Websocket: conn,
			Type:      common.HMI,
			Version:   version,
			ID:        vin,
			epoch:     time.Now().UnixNano(),
		},
		InAuthentication: false,
	}, nil
}

// SessionHMI contains websocket info
type SessionHMI struct {
	*Session
	SessionID        string
	InAuthentication bool
}

// Authenticate returns id if proper authentication, else returns error
//
//	validates VIN inputted through "key" field of message
func (s *SessionHMI) Authenticate() error {
	s.InAuthentication = true
	defer func() { s.InAuthentication = false }()
	err := s.authenticate()

	data, _ := json.Marshal(AuthResponse{
		Handler: "verify",
		Data: AuthResponseData{
			Authenticated: err == nil,
		},
	})
	s.SendMsgToClient(data)
	if err != nil {
		errors.Errorf("Unable to send to session  %s", s.SessionID)
		return errors.WithStack(err)
	}

	return nil
}
func (s *SessionHMI) SendMsgToClient(message []byte) error {

	if !s.InAuthentication && len(s.SessionID) == 0 {
		return fmt.Errorf("session is not authorized, %s", s.SessionID)
	}
	return s.Session.SendMsgToClient(message)
}

func (s *SessionHMI) authenticate() error {
	s.InAuthentication = true
	defer func() { s.InAuthentication = false }()
	msg, _, err := s.Receive()
	if err != nil {
		errors.Errorf("unable to read socket %s", s.ID)
		return err
	}

	var m common.HMISessionMessage
	err = json.Unmarshal(msg, &m)
	if err != nil {
		return errors.WithStack(err)
	} else if m.Handler != "verify" {
		return errors.Errorf("incorrect auth handler specified %v", m.Handler)
	}

	switch {
	case m.Data.SessionID != "":
		salter, err := security.NewSalter(s.ID)
		if err != nil {
			errors.Errorf("unable to generate salt %s", s.ID)
			return err
		}
		err = salter.ValidateSessionID(m.Data.SessionID)
		if err != nil {
			errors.Errorf("unable to validate session %s", m.Data.SessionID)
			return err
		}

		s.SessionID = m.Data.SessionID
	case m.Data.Salt != "":
		s.SessionID = m.Data.Salt
	default:
		return ErrFailedAuthentication
	}

	return nil
}

// Listen to websocket session and use handler upon message received
func (s *SessionHMI) Listen(ctx context.Context, producer kafka.ProducerInterface) error {
	span, _ := tracer.StartSpanFromContext(ctx, "listen")
	defer span.Finish()

	key := s.Key()
	for {
		msg, op, err := s.Receive()
		if op == ws.OpClose {
			//	logger.At(logger.Info(), key, "conn").Msg("OpClose") //commented out because reported by the call in websocket.go
			return nil
		} else if err != nil {
			//	logger.At(logger.Info(), key, "conn").Err(err).Send() //commented out because reported by the call in websocket.go
			return err
		}

		err = s.Route(producer, msg)
		if err != nil {
			logger.At(logger.Warn(), key, "route").Err(err).Send()
		}
	}
}

// Route HMI messages
func (s *SessionHMI) Route(producer kafka.ProducerInterface, data []byte) error {
	var m common.MessageRawJSON
	err := m.Unmarshal(data)
	if err != nil {
		return err
	}

	if m.Verify == "ack"{
		// Throw out ACK messages
		return nil
	}

	key := s.Key()
	logger.Debug().
		Str("id", key).
		Str("handler", m.Handler).
		Int("size", len(data)).
		Msgf("received from %v %s", key, string(data))

	topic, ok := kafka.HMIHandlerTopics[m.Handler]

	if !ok {
		err = ErrInvalidHandler(m.Handler)
		logger.Err(err).Str("Byte Data", string(data)).Str("Parsed Data", string(m.Data)).Msg("No Handler Insights")
		return err
	}

	switch topic {
	case kafka.AttendantServiceGRPCKafka:
		valetData := common.AttendantRouteHMIGRPC(m)
		grpcData, err := proto.Marshal(valetData)
		if err != nil {
			logger.At(logger.Error(), key, "route").
				Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
			return errors.WithStack(err)
		}
		err = producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, key, grpcData, nil)
		grpcData = nil
		if err != nil {
			logger.At(logger.Error(), key, "route").
				Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
			return err
		}
	case kafka.DepotServiceGRPCKafka:
		valetData := common.DepotRouteHMIToGRPC(m)
		grpcData, err := proto.Marshal(valetData)
		if err != nil {
			logger.At(logger.Error(), key, "route").
				Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
			return errors.WithStack(err)
		}
		err = producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, grpcData, nil)
		grpcData = nil
		if err != nil {
			logger.At(logger.Error(), key, "route").
				Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
			return err
		}
	case kafka.ValetServiceGRPCKafka:
		valetData := common.ValetRouteHMIPayloadGRPC(m)
		grpcData, err := proto.Marshal(valetData)
		if err != nil {
			logger.At(logger.Error(), key, "route").
				Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
			return errors.WithStack(err)
		}
		err = producer.ProduceBinary(kafka.ValetServiceGRPCKafka, key, grpcData, nil)
		grpcData = nil
		if err != nil {
			logger.At(logger.Error(), key, "route").
				Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
			return err
		}
	default:
		err = producer.Produce(topic, key, m, nil)
	}
	if err != nil {
		return err
	}

	return nil
}

func (s *SessionHMI) Load(producer kafka.ProducerInterface) error {
	key := s.Key()

	hmiSession := &kafka_grpc.GRPC_DepotPayload_HmiSession{
		HmiSession: &kafka_grpc.HMISessionData{
			SessionId: s.SessionID,
		},
	}
	payload := kafka_grpc.GRPC_DepotPayload{
		Handler: "init",
		Data:    hmiSession,
	}
	binaryPayload, _ := proto.Marshal(&payload)
	err := producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, binaryPayload, nil)

	return err
}
