package tncservice

import (
	"bytes"
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"

	"bitbucket.org/nemt/nemt-portal-api/application/entitymapping"
	"bitbucket.org/nemt/nemt-portal-api/application/notificationservice"
	"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
	"bitbucket.org/nemt/nemt-portal-api/domain/service"
	"bitbucket.org/nemt/nemt-portal-api/infra/config"
	"github.com/lyft/lyft-go-sdk/lyft"
	"github.com/pquerna/ffjson/ffjson"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/clientcredentials"
)

const (
	lyftURL = "https://api.lyft.com/v1/"
)

// lyftService holds methods to lyft application service
type lyftService struct {
	svc          *service.Service
	mapEntity    *entitymapping.Mapper
	client       *lyft.APIClient
	httpClient   *http.Client
	cfg          *config.Config
	notification *notificationservice.Service
	lyftConfig   clientcredentials.Config
	refreshToken string
}

// newLyftProdService returns a lyftService instance with production info
func newLyftProdService(svc *service.Service, mapper *entitymapping.Mapper, cfg *config.Config, notificationService *notificationservice.Service) *lyftService {
	config := clientcredentials.Config{
		ClientID:     cfg.LyftProd.Lyft.Client,
		ClientSecret: cfg.LyftProd.Lyft.Secret,
		TokenURL:     "https://api.lyft.com/oauth/token",
		Scopes:       []string{"public", "privileged.rides.dispatch", "rides.read", "rides.request", "profile", "offline"},
	}
	httpClient := config.Client(context.Background())

	return &lyftService{
		svc:          svc,
		mapEntity:    mapper,
		client:       lyft.NewAPIClient(httpClient, "nemt"),
		httpClient:   httpClient,
		cfg:          cfg,
		notification: notificationService,
		lyftConfig:   config,
		refreshToken: cfg.LyftProd.Lyft.RefreshToken,
	}
}

// newLyftService returns a lyftService instance
func newLyftService(svc *service.Service, mapper *entitymapping.Mapper, cfg *config.Config, notificationService *notificationservice.Service) *lyftService {
	config := clientcredentials.Config{
		ClientID:     cfg.Lyft.Client,
		ClientSecret: cfg.Lyft.Secret,
		TokenURL:     "https://api.lyft.com/oauth/token",
		Scopes:       []string{"public", "privileged.rides.dispatch", "rides.read", "rides.request", "profile", "offline"},
	}
	httpClient := config.Client(context.Background())

	return &lyftService{
		svc:          svc,
		mapEntity:    mapper,
		client:       lyft.NewAPIClient(httpClient, "nemt"),
		httpClient:   httpClient,
		cfg:          cfg,
		notification: notificationService,
		lyftConfig:   config,
		refreshToken: cfg.Lyft.RefreshToken,
	}
}

func (s *lyftService) getToken() string {
	token, err := s.lyftConfig.Token(context.Background())
	if err != nil {
		fmt.Println("Error to get token: ", err)
		return ""
	}

	return "Bearer " + token.AccessToken
}

func (s *lyftService) setTokenHeader(req *http.Request) string {
	type Token struct {
		GrantType    string `json:"grant_type"`
		RefreshToken string `json:"refresh_token"`
	}

	token := Token{
		GrantType:    "refresh_token",
		RefreshToken: s.refreshToken,
	}

	bObject, err := ffjson.Marshal(token)
	if err != nil {
		return ""
	}

	tokenReq, err := http.NewRequest("POST", s.lyftConfig.TokenURL, bytes.NewBuffer(bObject))
	if err != nil {
		return ""
	}
	tokenReq.Header.Set("Content-Type", "application/json")
	tokenReq.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", s.lyftConfig.ClientID, s.lyftConfig.ClientSecret))))
	tokenReq.Header.Add("cache-control", "no-cache")

	resp, err := http.DefaultClient.Do(tokenReq)
	if err != nil {
		fmt.Println("Error to refresh token: ", err.Error())
		return ""
	}
	defer resp.Body.Close()

	body, _ := ioutil.ReadAll(resp.Body)

	var returnToken oauth2.Token
	err = ffjson.Unmarshal(body, &returnToken)
	if err != nil {
		fmt.Println("Error to parse token: ", err.Error())
		return ""
	}

	returnToken.SetAuthHeader(req)

	return "Bearer " + returnToken.AccessToken
}

func (s *lyftService) GetRideDetails(rideRequest viewmodel.RideRequest) (viewmodel.RideRequest, error) {
	url := lyftURL + "dispatches/" + rideRequest.RideID

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return viewmodel.RideRequest{}, err
	}
	s.setTokenHeader(req)
	req.Header.Add("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return viewmodel.RideRequest{}, err
	}
	defer resp.Body.Close()

	body, _ := ioutil.ReadAll(resp.Body)
	var raw viewmodel.RideRequest
	err = ffjson.Unmarshal(body, &raw)
	if err != nil {
		return viewmodel.RideRequest{}, err
	}

	return raw, nil
}

func (s *lyftService) GetRideStatus(rideRequest viewmodel.RideRequest, param string) (viewmodel.RideRequest, error) {
	params := make(map[string]string)
	params["status"] = param

	bObject, err := ffjson.Marshal(params)
	if err != nil {
		return viewmodel.RideRequest{}, err
	}

	fmt.Println("STATUS CHANGE REQUEST ON LYFT")
	fmt.Println("Ride ID: ", rideRequest.RideID)
	fmt.Println("Status: ", param)
	url := lyftURL + "sandbox/dispatches/" + rideRequest.RideID
	req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(bObject))
	if err != nil {
		return viewmodel.RideRequest{}, err
	}
	req.Header.Add("Content-Type", "application/json")

	resp, err := s.httpClient.Do(req)
	if err != nil {
		return viewmodel.RideRequest{}, err
	}
	defer resp.Body.Close()

	body, _ := ioutil.ReadAll(resp.Body)
	fmt.Println("STATUS CHANGE RESPONSE ON LYFT")
	fmt.Println(string(body))

	var raw viewmodel.RideRequest
	err = ffjson.Unmarshal(body, &raw)
	if err != nil {
		return viewmodel.RideRequest{}, err
	}

	rideRequest.Status = raw.Status
	return s.GetRideDetails(rideRequest)
}

func (s *lyftService) CancelRide(rideRequest viewmodel.RideRequest) error {
	finalURL := lyftURL + "dispatches/" + rideRequest.RideID + "/cancel"
	req, err := http.NewRequest("POST", finalURL, bytes.NewBuffer([]byte{}))
	if err != nil {
		return err
	}
	s.setTokenHeader(req)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)

	if resp.StatusCode == 204 || resp.StatusCode == 404 {
		if err != nil {
			fmt.Println("Error: ", err)
		}

		return nil
	} else {
		fmt.Println("Response Body: ", string(body))

		raw := map[string]interface{}{}
		err = ffjson.Unmarshal(body, &raw)
		if err != nil {
			return err
		} else {
			return errors.New(raw["error_description"].(string))
		}
	}
}

func (s *lyftService) RequestRide(rideRequest viewmodel.RideRequest) (viewmodel.RideRequest, error) {
	bObject, err := ffjson.Marshal(rideRequest)
	if err != nil {
		return viewmodel.RideRequest{}, err
	}

	req, err := http.NewRequest("POST", lyftURL+"dispatches", bytes.NewBuffer(bObject))
	if err != nil {
		return viewmodel.RideRequest{}, err
	}
	s.setTokenHeader(req)
	req.Header.Add("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return viewmodel.RideRequest{}, err
	}
	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)

	var raw viewmodel.RideRequest
	err = ffjson.Unmarshal(body, &raw)
	if err != nil {
		return viewmodel.RideRequest{}, err
	}

	if raw.Error != "" {
		fmt.Println("Error to call Lyft: ", raw.Error+" - "+raw.ErrorDescription)
		fmt.Println("Full body error: ", string(body))
		return viewmodel.RideRequest{}, errors.New("Lyft Error: " + raw.Error + " - " + raw.ErrorDescription)
	}

	return s.GetRideDetails(raw)
}

func (s *lyftService) GetETA(lag float64, log float64, params map[string]interface{}) (interface{}, error) {
	resp, _, err := s.client.PublicApi.GetETA(lag, log, params)
	if err != nil {
		return nil, err
	}

	return resp, nil
}

func (s *lyftService) GetCost(lag float64, log float64, params map[string]interface{}) (interface{}, error) {
	resp, _, err := s.client.PublicApi.GetCost(lag, log, params)
	if err != nil {
		return nil, err
	}

	return resp, nil
}

func (s *lyftService) GetDrivers(lag float64, log float64) (interface{}, error) {
	resp, _, err := s.client.PublicApi.GetDrivers(lag, log)
	if err != nil {
		return nil, err
	}

	return resp, nil
}

func (s *lyftService) GetTypes(lag float64, log float64, params map[string]interface{}) (interface{}, error) {
	resp, _, err := s.client.PublicApi.GetRideTypes(lag, log, params)
	if err != nil {
		return nil, err
	}

	return resp, nil
}
