package websocket

import (
	"compress/flate"
	"context"
	"encoding/json"
	"fmt"
	"github.com/fiskerinc/cloud-services/services/gateway/sloppy"	
	"io"
	"net"
	"net/http"
	"strings"
	"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/utils"
	"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
	"github.com/fiskerinc/cloud-services/pkg/validator"
	"google.golang.org/protobuf/proto"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"

	"github.com/gobwas/ws"
	"github.com/gobwas/ws/wsflate"
	"github.com/gobwas/ws/wsutil"
	"github.com/pkg/errors"
)

var deadline = time.Duration(envtool.GetEnvInt("WS_TIMEOUT", 30)) * time.Second

// SessionInterface provides methods for connection
type SessionInterface interface {
	Authenticate() error
	Key() string

	SendMsgToClient(message []byte) error
	Receive() ([]byte, ws.OpCode, error)
	Listen(context.Context, kafka.ProducerInterface) error

	Load(kafka.ProducerInterface) error
	Teardown(kafka.ProducerInterface) error

	Close() error
	GetWebsocket() net.Conn
	GetIP() string
	GetType() string
	IsDevice(device common.Device) bool
	GetID() string
	GetUUID() int64
	GetVIN() (string)

	SkipTeardown(skip bool)
}

// NewSecureSession creates session w/ websocket based off user-agent
// given in HTTP request
//
//	ex: "Fisker Ocean T.Rex 1.2.3.4 abc123" - T.Rex
//	ex: "HMI 2.0.0.0" - HMI
func NewSecureSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) {
	var s SessionInterface

	// HERE, get the vin and block the request

	vin, err := utils.ParseVINFromRequest(r)
	if err != nil {
		logger.At(logger.Error(), "no vin from request", "conn").Send()
		return s, err
	}
	ok := validator.ValidateVINSimple(vin)
	if !ok {
		logger.Error().Str("type", "conn").Str("VIN", vin).Msg("NewSecureSession failed to validate vin")
		return s, errors.Errorf("%s failed to validate VIN", vin)
	}
	vin = strings.ToUpper(vin)
	if !sloppy.GetVINBlocker().IsVINAllowed(vin){
		return s, errors.Errorf("%s is not an allowed VIN, please contact support", vin)
	}

	device, version := ParseDeviceAndVersionFromRequest(r)

	switch device {
	case "fisker":
		logger.At(logger.Info(), "1:"+vin, "conn")

		iccid, err := ParseICCIDFromRequest(r)
		if err != nil {
			logger.At(logger.Warn(), "1:"+vin, "conn").
				Err(errors.WithMessagef(err, "failed to parse ICCID from request %s", vin)).Send()
		}
		s, err = NewTRexSession(w, r, vin, version, iccid)
		if err != nil {
			logger.At(logger.Warn(), "1:"+vin, "conn").
				Err(errors.WithMessagef(err, "failed to create Trex session %s", vin)).Send()
			return s, err
		}
		logger.At(logger.Info(), "1:"+vin, "conn").Send()
	case "hmi":
		s, err = NewHMISession(w, r, vin, version)
		if err != nil {
			logger.At(logger.Warn(), "2:"+vin, "conn").
				Err(errors.WithMessagef(err, "failed to create HMI session %s", vin)).Send()
			return s, err
		}
		logger.At(logger.Info(), "2:"+vin, "conn").Send()
	default:
		return s, ErrFailedToLoad
	}

	return s, nil
}

// NewInsecureSession creates session w/ websocket based off user-agent
// given in HTTP request
//
//	ex: "Mobile 1.2.3.4" - Mobile
func NewInsecureSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) {
	var s SessionInterface
	var err error

	device, version := ParseDeviceAndVersionFromRequest(r)

	switch device {
	case "mobile", "android", "ios":
		s, err = NewMobileSession(w, r, version)
		if err != nil {
			return s, err
		}
		logger.At(logger.Info(), "3: "+s.GetID(), "conn").Send()
	default:
		return s, ErrFailedToLoad
	}

	return s, nil
}

// NewSession is used when device is unknown
func NewSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) {
	var s SessionInterface

	conn, _, _, err := ws.UpgradeHTTP(r, w)
	if err != nil {
		return s, errors.WithStack(err)
	}

	return &Session{
		Websocket: conn,
		Type:      common.Unknown,
		epoch:     time.Now().UnixNano(),
	}, nil
}

// Session contains websocket info
type Session struct {
	Websocket    net.Conn
	Type         common.Device
	ID           string // used for key generation to kafka
	Version      string
	epoch        int64
	skipteardown bool
}

// Authenticate returns id if proper authentication, else returns error
func (s *Session) Authenticate() error {
	msg, _, err := s.Receive()
	if err != nil {
		return err
	}

	var ae AuthEvent
	err = json.Unmarshal(msg, &ae)
	if err != nil {
		return errors.WithStack(err)
	}

	authenticated, err := AuthenticateRequest(ae)
	if err != nil {
		return err
	} else if !authenticated {
		return errors.New("failed authentication")
	}

	s.ID = ae.Key
	return nil
}

// Key generates key based on type of session and ID
func (s *Session) Key() string {
	if s.Type == common.Unknown {
		return s.ID
	}

	return s.Type.Key(s.ID)
}

// SendMsgToClient: Send a message to client
func (s *Session) SendMsgToClient(message []byte) error {
	vin := s.GetVIN()
	logger.Debug().Str("type", s.GetType()).Str("VIN", vin).Int64("SessionID", s.GetUUID()).Str("value", string(message)).Msg("SendMsgToClient")
	err := wsutil.WriteServerMessage(s.Websocket, ws.OpText, message)
	if err != nil {
		err = errors.WithStack(err)
	}
	return err
}

func (s *Session) extendDeadline() error {
	return s.Websocket.SetDeadline(time.Now().Add(deadline))
}

func (s *Session) receive(postFrame func() error) ([]byte, ws.OpCode, error) {
	var (
		err error
		h   ws.Header
		msg wsflate.MessageState
	)

	// Using nil as a source io.Reader since we will Reset() it in the loop
	// below.
	fr := wsflate.NewReader(nil, func(r io.Reader) wsflate.Decompressor {
		return flate.NewReader(r)
	})

	controlHandler := wsutil.ControlFrameHandler(s.Websocket, ws.StateServerSide)
	rd := wsutil.Reader{
		Source:         s.Websocket,
		State:          ws.StateServerSide | ws.StateExtended,
		OnIntermediate: controlHandler,
		Extensions:     []wsutil.RecvExtension{&msg},
	}

	for {
		h, err = rd.NextFrame()
		if err != nil {
			return nil, h.OpCode, err
		}

		if postFrame != nil {
			err = postFrame()
			if err != nil {
				return nil, 0, err
			}
		}

		if h.OpCode.IsControl() {
			if err := controlHandler(h, &rd); err != nil {
				return nil, h.OpCode, err
			}
			continue
		}

		var src io.Reader = &rd
		if msg.IsCompressed() {
			fr.Reset(&rd)
			src = fr
		}

		data, err := io.ReadAll(src)
		if err != nil {
			return nil, h.OpCode, err
		}

		return data, h.OpCode, err
	}
}

func (s *Session) Receive() ([]byte, ws.OpCode, error) {
	return s.receive(nil)
}

// Listen to websocket session and use handler upon message received
func (s *Session) 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(), "Socket:Listen::EOF closing session ", key).Msg("OpClose")
			return nil
		} else if err != nil {
			logger.At(logger.Error(), "Socket:Listen::err during receiving session ", key).Err(err).Send()
			return err
		}
		err = s.Route(producer, msg)
		if err != nil {
			logger.At(logger.Warn(), "Socket:Listen:: failed route session ", key).Err(err).Send()
		}
	}
}

// Route messages
//   - this allows other structs to override the behavior of messages received
func (s *Session) Route(producer kafka.ProducerInterface, data []byte) error {
	var e common.EventRawJSON
	err := e.Unmarshal(data)
	if err != nil {
		return errors.WithStack(err)
	}

	key := s.Key()
	return producer.Produce(e.Topic, key, e.Payload, nil)
}

// Load the session - distributes messages to system notifying of new connection
func (s *Session) Load(producer kafka.ProducerInterface) error {
	key := s.Key()
	logger.At(logger.Info(), "Session::Load connection start notification", key).
		Msgf("session.Load %s", key)

	payload := kafka_grpc.GRPC_DepotPayload{
		Handler: "init",
	}
	binaryPayload, _ := proto.Marshal(&payload)
	err := producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, binaryPayload, nil)

	return err
}

// Teardown the session - distributes messages to system notifying of removed connection
func (s *Session) Teardown(producer kafka.ProducerInterface) error {
	// Go to send del message to depot service if connection was a duplicate
	if s.skipteardown {
		return nil
	}

	key := s.Key()
	logger.At(logger.Debug(), "Session::Teardown: Notify services ", key).
		Msgf("session.Teardown %s", key)

	payload := kafka_grpc.GRPC_DepotPayload{
		Handler: "del",
	}
	binaryPayload, _ := proto.Marshal(&payload)
	err := producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, binaryPayload, nil)

	return err
}

// Close the session
func (s *Session) Close() error {
	key := s.Key()
	logger.At(logger.Debug(), "Session:Close connection for ", key)
	return s.Websocket.Close()
}

// GetWebsocket returns session's websocket
func (s *Session) GetWebsocket() net.Conn {
	return s.Websocket
}

// GetIP returns session's websocket's IP
func (s *Session) GetIP() string {
	return s.Websocket.RemoteAddr().String()
}

// GetType returns Device type in string form
func (s *Session) GetType() string {
	return s.Type.String()
}

func (s *Session) IsDevice(device common.Device) bool {
	return s.Type == device
}

// GetID returns ID of session (not to be mistaken with key)
func (s *Session) GetID() string {
	return s.ID
}

// GetUUID returns a unique identifier for the session
func (s *Session) GetUUID() int64 {
	return s.epoch
}

func (s *Session) GetVIN() (vin string) {
	// For somereason code was changed to do some kind of parsing from session, but VIN is added directly
	return s.ID
}

func (s *Session) SkipTeardown(skip bool) {
	s.skipteardown = skip
}
func PrintRequest(r *http.Request) string {
	// Create return string
	var request []string
	// Add the request string
	url := fmt.Sprintf("%v %v %v", r.Method, r.URL, r.Proto)
	request = append(request, url)
	// Add the host
	request = append(request, fmt.Sprintf("Host: %v", r.Host))
	// Loop through headers
	for name, headers := range r.Header {
		name = strings.ToLower(name)
		for _, h := range headers {
			request = append(request, fmt.Sprintf("%v: %v", name, h))
		}
	}

	// If this is a POST, add post data
	if r.Method == "POST" {
		r.ParseForm()
		request = append(request, "\n")
		request = append(request, r.Form.Encode())
	}
	// Return the request as a string
	return strings.Join(request, "\n")

}
