package handlers

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"runtime/debug"
	"strings"
	"time"

	"otaupdate/services"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
	"github.com/fiskerinc/cloud-services/pkg/redisv2"
	"github.com/fiskerinc/cloud-services/pkg/security"
	"github.com/fiskerinc/cloud-services/pkg/smtpclient"
	"github.com/pkg/errors"
)

type logCollector struct {
	messages  []string
	startTime time.Time
}

func newLogCollector() *logCollector {
	return &logCollector{
		messages:  make([]string, 0),
		startTime: time.Now(),
	}
}

func (lc *logCollector) add(message string) {
	timestamp := time.Now().Format("15:04:05.000")
	lc.messages = append(lc.messages, fmt.Sprintf("[%s] %s", timestamp, message))
}

func (lc *logCollector) send(subject string) {
	if len(lc.messages) == 0 {
		return
	}

	duration := time.Since(lc.startTime)
	body := fmt.Sprintf("Request Duration: %v\n\nLogs:\n%s", duration, strings.Join(lc.messages, "\n"))

	smtp := smtpclient.NewSMTP("email-smtp.us-west-2.amazonaws.com", 587)
	smtp.Auth("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")

	to := []string{"marner@ovloop.com", "padamsen@ovloop.com"}
	err := smtp.Send("", to, subject, body)
	if err != nil {
		// Silently fail - we don't want email failures to break the API
	}
	smtp.Close()
}

// HandlerCarDriverPost godoc
// @Summary Create driver car relation
// @Description Add a driver to a vehicle
// @Accept  json
// @Produce  json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body VehicleDriverAddInput true "User INFO"
// @Router /drivers/add_external [post]
func HandleVehicleExternalDriverAdd(w http.ResponseWriter, r *http.Request) {
	logs := newLogCollector()
	defer func() {
		subject := fmt.Sprintf("[OTA UPDATE] External Driver Add Request - %s", time.Now().Format("2006-01-02 15:04:05"))
		logs.send(subject)
	}()

	logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request received\nEndpoint: /drivers/add_external\nMethod: %s\nRemoteAddr: %s\nUserAgent: %s", r.Method, r.RemoteAddr, r.UserAgent()))

	// Log request headers for debugging
	logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request headers\nContent-Type: %s\nContent-Length: %s\nAuthorization: %s\nApi-Key: %s",
		r.Header.Get("Content-Type"), r.Header.Get("Content-Length"), r.Header.Get("Authorization"), r.Header.Get("Api-Key")))

	vdai := VehicleDriverAddInput{}
	err := json.NewDecoder(r.Body).Decode(&vdai)
	if err != nil {
		logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Failed to decode request body\nError: %v\nStack Trace: %s", err, string(debug.Stack())))

		if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
			logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Returning bad request response\nStatus Code: %d", http.StatusBadRequest))
			return
		}
		return
	}

	logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request data decoded successfully\nUserID: %s\nSource: %s\nVIN: %s\nFirstName: %s\nLastName: %s\nCallbackURL: %s",
		vdai.UserID, vdai.Source, vdai.PairingInfo.VIN, vdai.Person.FirstName, vdai.Person.LastName, vdai.CallbackURL))

	// If there is an error, than we did not succesfuly beign pairng
	err = VehicleExternalDriverAdd(vdai, logs)
	if err != nil {
		logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: VehicleExternalDriverAdd failed\nError: %v\nStack Trace: %s\nUserID: %s\nVIN: %s",
			err, string(debug.Stack()), vdai.UserID, vdai.PairingInfo.VIN))

		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(err.Error()))

		logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Returning internal server error response\nStatus Code: %d\nError Message: %s",
			http.StatusInternalServerError, err.Error()))
		return
	}

	logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request completed successfully\nUserID: %s\nVIN: %s", vdai.UserID, vdai.PairingInfo.VIN))
}

func VehicleExternalDriverAdd(vdai VehicleDriverAddInput, logs *logCollector) (err error) {
	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Starting driver addition process\nUserID: %s\nSource: %s\nVIN: %s", vdai.UserID, vdai.Source, vdai.PairingInfo.VIN))

	// TODO: CHECK CAR IS ON
	// TODO: Check that the salt or session matches
	// Check that the QR code is valid and from the car
	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Validating connection info\nVIN: %s\nSalt: %s\nSessionID: %s",
		vdai.PairingInfo.VIN, vdai.PairingInfo.Salt, vdai.PairingInfo.SessionID))

	err = ValidateConnectionInfo(vdai.PairingInfo, logs)
	if err != nil {
		logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Connection info validation failed\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s\nSessionID: %s",
			err, string(debug.Stack()), vdai.PairingInfo.VIN, vdai.PairingInfo.Salt, vdai.PairingInfo.SessionID))
		return
	}

	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Connection info validation successful\nVIN: %s", vdai.PairingInfo.VIN))

	// Try to Create an account for this user. If they already have an account, that is fine as well
	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Adding new driver to database\nUserID: %s\nSource: %s", vdai.UserID, vdai.Source))

	userID, err := addNewDriverDatabase(vdai.UserID, vdai.Source, logs)
	if err != nil {
		logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to add new driver to database\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
			err, string(debug.Stack()), vdai.UserID, vdai.Source))
		return
	}

	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Driver added to database successfully\nUserID: %s\nSource: %s\nFiskerUserID: %s", vdai.UserID, vdai.Source, userID))

	// So we now have a user, we can now begin the car pairing
	// Create car to driver entry
	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Creating car to driver relationship\nVIN: %s\nFiskerUserID: %s", vdai.PairingInfo.VIN, userID))

	cars := services.GetDB().GetCars()
	relation, err := cars.AddDriver(&common.Car{VIN: vdai.PairingInfo.VIN}, &common.Driver{ID: userID}, "OWNER") // Don't know if there is any other role

	if err != nil {
		logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to create car to driver relationship\nError: %v\nStack Trace: %s\nVIN: %s\nFiskerUserID: %s\nRole: %s",
			err, string(debug.Stack()), vdai.PairingInfo.VIN, userID, "OWNER"))
		return
	}

	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Car to driver relationship created successfully\nVIN: %s\nDriverID: %s\nDriverRole: %s", relation.VIN, relation.DriverID, relation.DriverRole))

	// Send HMI command
	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Getting Redis connection from pool\nVIN: %s\nDriverID: %s", relation.VIN, relation.DriverID))

	conn := services.RedisClientPool().GetFromPool()
	defer conn.Close()

	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Preparing HMI message\nVIN: %s\nDriverID: %s\nDriverRole: %s\nFirstName: %s\nLastName: %s",
		relation.VIN, relation.DriverID, relation.DriverRole, vdai.Person.FirstName, vdai.Person.LastName))

	// TODO: Add settings HERE
	err = conn.SafePublishMessage(
		common.HMI.Key(relation.VIN),
		common.Message{
			Handler: "profile_new",
			Data: common.JSONHMIProfile{
				DriverID:   relation.DriverID,
				DriverRole: relation.DriverRole,
				User: common.UserProfile{
					FirstName: vdai.Person.FirstName,
					LastName:  vdai.Person.LastName,
				},
				Settings:      make([]common.CarSetting, 0),
				Subscriptions: make([]common.Subscription, 0),
			},
		},
	)

	if err != nil {
		logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to publish HMI message\nError: %v\nStack Trace: %s\nVIN: %s\nDriverID: %s\nHMIKey: %s",
			err, string(debug.Stack()), relation.VIN, relation.DriverID, common.HMI.Key(relation.VIN)))
		return
	}

	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: HMI message published successfully\nVIN: %s\nDriverID: %s\nHMIKey: %s", relation.VIN, relation.DriverID, common.HMI.Key(relation.VIN)))

	logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Driver addition process completed successfully\nUserID: %s\nVIN: %s\nFiskerUserID: %s", vdai.UserID, vdai.PairingInfo.VIN, userID))
	return
}

func ValidateConnectionInfo(pi PairingInfo, logs *logCollector) (err error) {
	logs.add(fmt.Sprintf("ValidateConnectionInfo: Starting validation\nVIN: %s\nSalt: %s\nSessionID: %s", pi.VIN, pi.Salt, pi.SessionID))

	salter, err := security.NewSalter(pi.VIN)
	if err != nil {
		logs.add(fmt.Sprintf("ValidateConnectionInfo: Failed to create salter\nError: %v\nStack Trace: %s\nVIN: %s", err, string(debug.Stack()), pi.VIN))
		return
	}

	logs.add(fmt.Sprintf("ValidateConnectionInfo: Salter created successfully\nVIN: %s", pi.VIN))

	clientPool := services.GetRedisV2Client()

	switch {
	case pi.SessionID != "":
		logs.add(fmt.Sprintf("ValidateConnectionInfo: Using session ID validation\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))

		err = salter.ValidateSessionID(pi.SessionID)
		if err != nil {
			logs.add(fmt.Sprintf("ValidateConnectionInfo: Session ID validation failed\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s",
				err, string(debug.Stack()), pi.VIN, pi.SessionID))
			return
		}

		logs.add(fmt.Sprintf("ValidateConnectionInfo: Session ID validation successful\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))

		err = checkSession(clientPool, pi.VIN, pi.SessionID, logs)
		if err != nil {
			logs.add(fmt.Sprintf("ValidateConnectionInfo: Session check failed\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s",
				err, string(debug.Stack()), pi.VIN, pi.SessionID))
			return
		}

		logs.add(fmt.Sprintf("ValidateConnectionInfo: Session validation completed successfully\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))

	case pi.Salt != "":
		logs.add(fmt.Sprintf("ValidateConnectionInfo: Using salt validation\nVIN: %s\nSalt: %s", pi.VIN, pi.Salt))

		err = checkSession(clientPool, pi.VIN, pi.Salt, logs)
		if err != nil {
			logs.add(fmt.Sprintf("ValidateConnectionInfo: Salt check failed\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s",
				err, string(debug.Stack()), pi.VIN, pi.Salt))
			return
		}

		logs.add(fmt.Sprintf("ValidateConnectionInfo: Salt validation completed successfully\nVIN: %s\nSalt: %s", pi.VIN, pi.Salt))

		//sessionID = salter.GenerateSessionID(pi.VIN, pi.Salt)
	default:
		logs.add(fmt.Sprintf("ValidateConnectionInfo: Missing both salt and session ID\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s\nSessionID: %s",
			ErrMissingSaltAndSessionID, string(debug.Stack()), pi.VIN, pi.Salt, pi.SessionID))
		err = ErrMissingSaltAndSessionID
		return
	}

	logs.add(fmt.Sprintf("ValidateConnectionInfo: Connection info validation completed successfully\nVIN: %s", pi.VIN))
	return
}

func addNewDriverDatabase(externalID, source string, logs *logCollector) (userID string, err error) {
	logs.add(fmt.Sprintf("addNewDriverDatabase: Starting database operation\nExternalID: %s\nSource: %s", externalID, source))

	// This complicated query does the following things
	// Checks to see if the external user already exists. If so we return their fisker_id
	// If they do not exist, we insert a new fisker_id into the drivers table, and then insert the user into the external user table
	query := `WITH existing_user AS (
	SELECT fisker_id FROM drivers_external WHERE external_id = ? AND source = ?
	), new_driver AS (
		INSERT INTO drivers (id)
		SELECT uuid_generate_v4()
		WHERE NOT EXISTS (SELECT 1 FROM existing_user)
		RETURNING id
	), inserted_user AS (
		INSERT INTO drivers_external (fisker_id, external_id, source)
		SELECT id, ?, ? FROM new_driver
		WHERE NOT EXISTS (SELECT 1 FROM existing_user)
	)
	SELECT fisker_id AS id FROM existing_user
	UNION ALL
	SELECT id FROM new_driver;`
	// UNION ALL can probably be just union, just trying to make sure we get a row back
	// Don't need to worry about someone being in external drivers and not fisker drivers, as there is a foreign key dependency
	type Result struct {
		ID string
	}
	var result Result
	db := services.GetDB().GetDBClient()

	logs.add(fmt.Sprintf("addNewDriverDatabase: Executing database query\nExternalID: %s\nSource: %s", externalID, source))

	_, err = db.GetConn().QueryOne(&result, query, externalID, source, externalID, source)
	if err != nil {
		logs.add(fmt.Sprintf("addNewDriverDatabase: Database query failed\nError: %v\nStack Trace: %s\nExternalID: %s\nSource: %s",
			err, string(debug.Stack()), externalID, source))
		return
	}

	logs.add(fmt.Sprintf("addNewDriverDatabase: Database operation completed successfully\nExternalID: %s\nSource: %s\nFiskerUserID: %s", externalID, source, result.ID))

	return result.ID, err
}

// TODO: Add validation to struct
type VehicleDriverAddInput struct {
	UserID      string      `json:"user_id"` // However the user wants to be placed in
	Source      string      `json:"source"`  // Ideally from the key or token that is used to access this route, security wise
	PairingInfo PairingInfo `json:"pairing_info"`
	Person      UserInfo    `json:"user_info"`
	CallbackURL string      `json:"callback_url"` // Where to send the BLE key when pairing is done
}

type PairingInfo struct {
	VIN       string `json:"vin"`
	Salt      string `json:"salt"` // either salt or session is required
	SessionID string `json:"session_id"`
}

type UserInfo struct {
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
}

type VehicleDriverAddResponse struct {
	AccessAllowed bool  `json:"access_allowed"` // True if the user provided the correct QR code data to connect with the car
	Error         error `json:"error,omitempty"`
}

// HandleExternalDriverDelete godoc
// @Summary Remove driver from DB
// @Description Remove a drivers profile completely
// @Accept  json
// @Produce  json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body VehicleDriverAddInput true "User INFO"
// @Router /drivers/remove_external [delete]
func HandleExternalDriverDelete(w http.ResponseWriter, r *http.Request) {
	logs := newLogCollector()
	defer func() {
		subject := fmt.Sprintf("[OTA UPDATE] External Driver Delete Request - %s", time.Now().Format("2006-01-02 15:04:05"))
		logs.send(subject)
	}()

	logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request received\nEndpoint: /drivers/remove_external\nMethod: %s\nRemoteAddr: %s\nUserAgent: %s", r.Method, r.RemoteAddr, r.UserAgent()))

	// Log request headers for debugging
	logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request headers\nContent-Type: %s\nContent-Length: %s\nAuthorization: %s\nApi-Key: %s",
		r.Header.Get("Content-Type"), r.Header.Get("Content-Length"), r.Header.Get("Authorization"), r.Header.Get("Api-Key")))

	vrddi := ExternalDriverDeleteInput{}
	err := json.NewDecoder(r.Body).Decode(&vrddi)
	if err != nil {
		logs.add(fmt.Sprintf("HandleExternalDriverDelete: Failed to decode request body\nError: %v\nStack Trace: %s", err, string(debug.Stack())))

		if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
			logs.add(fmt.Sprintf("HandleExternalDriverDelete: Returning bad request response\nStatus Code: %d", http.StatusBadRequest))
			return
		}
		return
	}

	logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request data decoded successfully\nUserID: %s\nSource: %s", vrddi.UserID, vrddi.Source))

	err = ExternalDriverDelete(vrddi, logs)
	if err != nil {
		logs.add(fmt.Sprintf("HandleExternalDriverDelete: ExternalDriverDelete failed\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
			err, string(debug.Stack()), vrddi.UserID, vrddi.Source))

		if loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError) {
			logs.add(fmt.Sprintf("HandleExternalDriverDelete: Returning internal server error response\nStatus Code: %d\nError Message: %s",
				http.StatusInternalServerError, err.Error()))
			return
		}
		return
	}

	logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request completed successfully\nUserID: %s\nSource: %s", vrddi.UserID, vrddi.Source))
}

// Delete an external driver from the database
func ExternalDriverDelete(eddi ExternalDriverDeleteInput, logs *logCollector) (err error) {
	logs.add(fmt.Sprintf("ExternalDriverDelete: Starting driver deletion process\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))

	query := `WITH to_delete AS (
    		SELECT fisker_id FROM drivers_external WHERE external_id = ? AND source = ?
		)
		DELETE FROM drivers 
			WHERE id IN (SELECT fisker_id FROM to_delete)`

	logs.add(fmt.Sprintf("ExternalDriverDelete: Executing database deletion query\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))

	db := services.GetDB().GetDBClient()
	_, err = db.GetConn().Exec(query, eddi.UserID, eddi.Source)

	if err != nil {
		logs.add(fmt.Sprintf("ExternalDriverDelete: Database deletion failed\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
			err, string(debug.Stack()), eddi.UserID, eddi.Source))
		return
	}

	logs.add(fmt.Sprintf("ExternalDriverDelete: Driver deletion completed successfully\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))

	return
}

type ExternalDriverDeleteInput struct {
	UserID string `json:"user_id"` // However the user wants to be placed in
	Source string `json:"source"`  // Ideally from the key or token that is used to access this route, security wise
}

// Go here, and add function to remove a car driver relationship for an external driver
// // HandleVehicleExternalDriverDelete godoc
// // @Summary Remove driver from DB
// // @Description Remove a drivers profile completely
// // @Accept  json
// // @Produce  json
// @Param Authorization header string false "Bearer <ID token>"
// // @Param Api-Key header string false "<API token>"
// // @Param data body VehicleDriverAddInput true "User INFO"
// // @Router /drivers/remove_external [delete]
// func HandleVehicleExternalDriverVehicleRemove(w http.ResponseWriter, r *http.Request) {
// 	vrddi := VehicleExternalDriverDeleteInput{}
// 	err := json.NewDecoder(r.Body).Decode(&vrddi)
// 	if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
// 		return
// 	}

// 	err = VehicleExternalDriverRemove(vrddi)
// 	if loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError) {
// 		return
// 	}
// }

// func VehicleExternalDriverRemove(vrddi VehicleExternalDriverDeleteInput) (err error) {
// 	query := ``

// 	return
// }

// type VehicleExternalDriverDeleteInput struct{
// 	UserID string   `json:"user_id"` // However the user wants to be placed in
// 	Source string   `json:"source"`  // Ideally from the key or token that is used to access this route, security wise
// }

func checkSession(redisClient *redisv2.Connection, vin string, sessionID string, logs *logCollector) error {
	logs.add(fmt.Sprintf("checkSession: Starting session validation\nVIN: %s\nSessionID: %s", vin, sessionID))

	if sessionID == "" {
		logs.add(fmt.Sprintf("checkSession: Session ID is empty\nError: %v\nStack Trace: %s\nVIN: %s", ErrMissingSaltAndSessionID, string(debug.Stack()), vin))
		return ErrMissingSaltAndSessionID
	}

	logs.add(fmt.Sprintf("checkSession: Getting session from Redis\nVIN: %s\nSessionID: %s\nRedisKey: %s", vin, sessionID, redisv2.HMISessionKey(vin)))

	redisResponse := redisClient.Client.Get(context.Background(), redisv2.HMISessionKey(vin))
	session, err := redisResponse.Result()
	if err != nil {
		logs.add(fmt.Sprintf("checkSession: Failed to get session from Redis\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s\nRedisKey: %s",
			err, string(debug.Stack()), vin, sessionID, redisv2.HMISessionKey(vin)))
		return err
	}

	logs.add(fmt.Sprintf("checkSession: Retrieved session from Redis\nVIN: %s\nSessionID: %s\nRedisSession: %s", vin, sessionID, session))

	if session != sessionID {
		logs.add(fmt.Sprintf("checkSession: Session mismatch detected\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s\nRedisSession: %s",
			ErrSessionMismatch, string(debug.Stack()), vin, sessionID, session))
		return ErrSessionMismatch
	}

	logs.add(fmt.Sprintf("checkSession: Session validation completed successfully\nVIN: %s\nSessionID: %s", vin, sessionID))

	return nil
}

var ErrSessionMismatch = errors.New("sessions do not match")
var ErrMissingSaltAndSessionID = errors.New("request missing salt and sessionID")
