package tmobile

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/fiskerinc/cloud-services/pkg/grpc/sms"
	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/fiskerinc/cloud-services/pkg/utils/randomvalues"
	"github.com/pkg/errors"
	errorsO "errors"

	tmtg "github.com/fiskerinc/cloud-services/pkg/tmobtokengen"
)

const (
	failedToParseRequest   = "failed to encode request: %v"
	payloadMsg             = "payload: %s"
	contentType            = "Content-Type"
	failedGeneratePod      = "failed to generate pop token: %v"
	startingTMobileTimeout = time.Millisecond * 10
	maximumTMobileTimeout  = time.Minute

	Endpoint = "https://adn.t-mobile.com"
)

var (
	fakeIDGenerator       = randomvalues.NewNonCryptoGenerator("1234567890", 0)
	currentTMobileTimeout = startingTMobileTimeout
)

type httpClienter interface {
	Do(req *http.Request) (*http.Response, error)
}

type TMobClienter interface {
	AccessToken(ctx context.Context) (out *AccessTokenResponse, err error)
	SetAccessToken(accessToken string)
	SendSMS(ctx context.Context, in *SendSMSRequest) (out *SendSMSResponse, err error)
	Details(ctx context.Context, ID string) (out *SMSDetailsResponse, err error)
	ChangeRatePlan(context.Context, *ChangeRatePlanRequest) (*ChangeRatePlanResponse, error)
	CustomAttributes(context.Context, *CustomAtributesRequest) (*CustomAtributesResponse, error)
	GetProducts(context.Context, *sms.GetAvailableProductsRequest) (*sms.GetAvailableProductsResponse, error)
	DeviceDetails(context.Context, *DeviceDetailsRequest) (*DeviceDetailsResponse, error)
	ChangeDeviceStatus(ctx context.Context, cda ChangeDeviceActivation) (err error)

	SetFilter(filter []string)
}

type TMobClient struct {
	tg          tmtg.Generator
	client      httpClienter
	accessToken string
	baseURL     *url.URL
	lock        sync.RWMutex // Using a read-write mutex and anyone can send a sms at any time, but if the token is do for a renew, we should stop sending sms for a second
	toRefresh   <-chan time.Time
	iccidFilter ICCIDFilter
}


var _ TMobClienter = &TMobClient{}

const (
	Authorization     = "Authorization"
	XAuthorization    = "X-Authorization"
	XAuthOriginator   = "x-auth-originator"
	failedToDoRequest = "failed to do request"
	applicationJSON   = "application/json"
)

// baseURLstr: in the url to tmobile i.e. "https://core.saas.api.t-mobile.com"
// tg: should have been initiated with the correct keys. I believe this service should do it itself given the correct input
// timeout: The amount of time the http client will do a request before it gives up doing the request
func NewTMobileClient(baseURLstr string, tg tmtg.Generator, timeout time.Duration) (*TMobClient, error) {
	u, err := url.Parse(baseURLstr)
	if err != nil {
		return nil, errors.WithMessage(err, "failed to parse base URL")
	}

	tmb := &TMobClient{
		baseURL: u,
		tg:      tg,
		client: &http.Client{
			Timeout: timeout,
		},
		toRefresh: make(<-chan time.Time),
	}
	tmb.refresh(context.Background())

	tmb.iccidFilter = InitFilter()
	return tmb, nil
}

func (c *TMobClient) do(req *http.Request, out interface{}) error {

	resp, err := c.client.Do(req)
	if err != nil {
		return errors.WithMessagef(ErrDoRequest, "failed to do request: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		var body []byte
		body, err = io.ReadAll(resp.Body)
		if err != nil {
			return errors.WithMessagef(ErrReadResponseBody, "failed to read response body: %v", err)
		}

		toWrapErr := ErrBadStatusCode
		if resp.StatusCode == http.StatusUnauthorized {
			toWrapErr = ErrAccessTokenExpired
		} else if resp.StatusCode == http.StatusBadGateway {
			toWrapErr = ErrBadGatewayCode
		} else if resp.StatusCode == http.StatusGatewayTimeout {
			toWrapErr = ErrBadGatewayCode
		}

		return errors.WithMessagef(toWrapErr, "request failed with status code %d, body: %s", resp.StatusCode, string(body))
	}

	err = json.NewDecoder(resp.Body).Decode(out)
	if err != nil {
		return errors.WithMessagef(ErrJSONMarshal, "failed to decode response: %v", err)
	}

	return nil
}

// Must call setAccessToken after calling this function to set the access token
func (c *TMobClient) AccessToken(ctx context.Context) (out *AccessTokenResponse, err error) {
	path := "/oauth2/v1/tokens"

	c.lock.Lock()
	defer c.lock.Unlock()
	emap := make(tmtg.EHTSMap).SetAuthorization(c.tg.ClientSecretAsAuthVal()).
		SetURI(path).
		SetHTTPMethod(http.MethodPost)

	token, err := c.tg.Generate(emap)
	if err != nil {
		return nil, err
	}

	fullPath := c.baseURL.String() + path

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, nil)
	if err != nil {
		return nil, errors.WithMessagef(ErrCreateRequest, "failed to create request")
	}

	req.Header.Set(Authorization, c.tg.ClientSecretAsAuthVal())
	req.Header.Set(XAuthorization, token)

	err = c.do(req, &out)
	if err != nil {
		return nil, errors.WithMessage(err, failedToDoRequest)
	}
	return
}

func (c *TMobClient) SetAccessToken(accessToken string) {
	c.accessToken = "Bearer " + accessToken
}

func (c *TMobClient) SendSMS(ctx context.Context, in *SendSMSRequest) (out *SendSMSResponse, err error) {
	if !c.iccidFilter.ShouldSend(in.ICCID) {
		out = &SendSMSResponse{
			SmsMessageID: fakeSMSID(),
		}
		return
	}
	path := "/eitcsr-iotcp-notifications-v2/prd02/iotcp/v2/notifications/sms/messages"

	payload := new(bytes.Buffer)
	if err = json.NewEncoder(payload).Encode(in); err != nil {
		return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
	}

	payloadStr := payload.String()

	emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
		SetURI(path).
		SetHTTPMethod(http.MethodPost).
		SetContentType(applicationJSON).
		SetBody(payloadStr)

	logger.Debug().Msgf(payloadMsg, payloadStr)

	token, err := c.tg.Generate(emap)
	if err != nil {
		return nil, errors.WithMessagef(ErrTokenGen, "failed to generate token: %v", err)
	}

	fullPath := c.baseURL.String() + path

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, bytes.NewBufferString(payloadStr))
	if err != nil {
		return nil, err
	}

	c.lock.RLock()
	defer c.lock.RUnlock()
	req.Header.Set(Authorization, c.accessToken)
	req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
	req.Header.Set(XAuthorization, token)
	req.Header.Set(contentType, applicationJSON)

	err = c.do(req, &out)
	if err != nil {
		return nil, errors.WithMessage(err, failedToDoRequest)
	}

	return
}

func (c *TMobClient) Details(ctx context.Context, ID string) (out *SMSDetailsResponse, err error) {
	path := fmt.Sprintf("/eitcsr-iotcp-notifications-v2/prd02/iotcp/v2/notifications/sms/messages/%s", ID)

	emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
		SetURI(path).
		SetHTTPMethod(http.MethodGet)

	token, err := c.tg.Generate(emap)
	if err != nil {
		return nil, errors.WithMessagef(ErrTokenGen, "failed to generate token: %v", err)
	}

	fullPath := c.baseURL.String() + path

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullPath, nil)
	if err != nil {
		return nil, errors.WithMessagef(ErrCreateRequest, "failed to create request: %v", err)
	}

	c.lock.RLock()
	defer c.lock.RUnlock()
	req.Header.Set(Authorization, c.accessToken)
	req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
	req.Header.Set(XAuthorization, token)
	req.Header.Set(contentType, applicationJSON)

	err = c.do(req, &out)
	if err != nil {
		return nil, errors.WithMessage(err, failedToDoRequest)
	}

	return
}

func InitTokenGen(pkVal, clientId, secret string) (*tmtg.PopTokenGenerator, error) {
	// No reason to take a variable and write it to a text file and then read it from the text file

	tg, err := tmtg.NewTokenGenerator(
		clientId,
		secret,
		time.Minute*2,
		pkVal, // path to public key file
	)
	if err != nil {
		return nil, errors.WithMessage(err, "failed to create token generator")
	}

	return tg, nil
}

func (c *TMobClient) ChangeRatePlan(ctx context.Context, in *ChangeRatePlanRequest) (*ChangeRatePlanResponse, error) {
	path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/change-rate-plan"
	fullPath := c.baseURL.String() + path

	// The in body is so small, no use in using the encode method
	payload := new(bytes.Buffer)
	if err := json.NewEncoder(payload).Encode(in); err != nil {
		return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
	}
	payloadStr := payload.String()

	emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
		SetURI(path).
		SetHTTPMethod(http.MethodPost).
		SetContentType(applicationJSON).
		SetBody(payloadStr)

	logger.Debug().Msgf(payloadMsg, payloadStr)
	popToken, err := c.tg.Generate(emap)
	if err != nil {
		return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
	if err != nil {
		return nil, err
	}

	c.lock.RLock()
	defer c.lock.RUnlock()
	req.Header.Set(Authorization, c.accessToken)
	req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
	req.Header.Set(XAuthorization, popToken)
	req.Header.Set(contentType, applicationJSON)
	out := &ChangeRatePlanResponse{}

	err = c.do(req, out)
	if err != nil {
		return nil, errors.WithMessage(err, failedToDoRequest)
	}

	return out, nil
}

func (c *TMobClient) CustomAttributes(ctx context.Context, in *CustomAtributesRequest) (*CustomAtributesResponse, error) {
	path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/device-attributes"
	fullPath := c.baseURL.String() + path

	payload := new(bytes.Buffer)
	if err := json.NewEncoder(payload).Encode(in); err != nil {
		return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
	}
	payloadStr := payload.String()

	emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
		SetURI(path).
		SetHTTPMethod(http.MethodPost).
		SetContentType(applicationJSON).
		SetBody(payloadStr)

	logger.Debug().Msgf(payloadMsg, payloadStr)
	popToken, err := c.tg.Generate(emap)
	if err != nil {
		return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
	if err != nil {
		return nil, err
	}

	c.lock.RLock()
	defer c.lock.RUnlock()
	req.Header.Set(Authorization, c.accessToken)
	req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
	req.Header.Set(XAuthorization, popToken)
	req.Header.Set(contentType, applicationJSON)
	out := &CustomAtributesResponse{}

	err = c.do(req, out)
	if err != nil {
		return nil, errors.WithMessage(err, failedToDoRequest)
	}

	return out, nil
}

func (c *TMobClient) GetProducts(ctx context.Context, in *sms.GetAvailableProductsRequest) (*sms.GetAvailableProductsResponse, error) {
	path := "/eitcsr-iot-product-service-v1/prd02/iotcp/v1/iot-product-service/products/query"
	fullPath := c.baseURL.String() + path

	payload := new(bytes.Buffer)
	if err := json.NewEncoder(payload).Encode(in); err != nil {
		return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
	}
	payloadStr := payload.String()

	emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
		SetURI(path).
		SetHTTPMethod(http.MethodPost).
		SetContentType(applicationJSON).
		SetBody(payloadStr)

	logger.Debug().Msgf(payloadMsg, payloadStr)
	popToken, err := c.tg.Generate(emap)
	if err != nil {
		return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
	if err != nil {
		return nil, err
	}

	c.lock.RLock()
	defer c.lock.RUnlock()
	req.Header.Set(Authorization, c.accessToken)
	req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
	req.Header.Set(XAuthorization, popToken)
	req.Header.Set(contentType, applicationJSON)
	out := &sms.GetAvailableProductsResponse{}

	err = c.do(req, &out.AvailableProducts)
	if err != nil {
		return nil, errors.WithMessage(err, failedToDoRequest)
	}

	return out, nil
}

func (c *TMobClient) DeviceDetails(ctx context.Context, in *DeviceDetailsRequest) (*DeviceDetailsResponse, error) {
	path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/details"
	fullPath := c.baseURL.String() + path

	payload := new(bytes.Buffer)
	if err := json.NewEncoder(payload).Encode(in); err != nil {
		return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
	}
	payloadStr := payload.String()

	emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
		SetURI(path).
		SetHTTPMethod(http.MethodPost).
		SetContentType(applicationJSON).
		SetBody(payloadStr)

	logger.Debug().Msgf(payloadMsg, payloadStr)
	popToken, err := c.tg.Generate(emap)
	if err != nil {
		return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
	if err != nil {
		return nil, err
	}

	c.lock.RLock()
	defer c.lock.RUnlock()
	req.Header.Set(Authorization, c.accessToken)
	req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
	req.Header.Set(XAuthorization, popToken)
	req.Header.Set(contentType, applicationJSON)
	out := &DeviceDetailsResponse{}

	err = c.do(req, &out)
	if err != nil {
		return nil, errors.WithMessage(err, failedToDoRequest)
	}

	return out, nil
}

func (c *TMobClient) refresh(ctx context.Context) {
	var expiresIn time.Duration

	at, err := c.AccessToken(ctx)
	if err != nil {
		logger.Error().Msgf("failed to get access token: %v", err)
		expiresIn = currentTMobileTimeout
		currentTMobileTimeout = currentTMobileTimeout * currentTMobileTimeout
		if currentTMobileTimeout > maximumTMobileTimeout {
			currentTMobileTimeout = maximumTMobileTimeout
		}

	} else {
		expiresIn = time.Duration(at.ExpiresIn) * time.Second
		expiresIn -= time.Minute * 1 // Expire 1 minute earlier than needed. Previous code was refreshing in half the time
		c.SetAccessToken(at.AccessToken)
		logger.Info().Msgf("Refreshed access token, expires in %s", expiresIn)
		// Reset the increasing timeout
		currentTMobileTimeout = startingTMobileTimeout
	}

	time.AfterFunc(expiresIn, func() { c.refresh(context.Background()) })
}

// SetFilter implements TMobClienter.
func (c *TMobClient) SetFilter(filter []string) {
	c.iccidFilter = ICCIDFilter{
		filter: filter,
	}
}

// SmsMessageID is a string consisting of numbers, will return a marker showing its not real
func fakeSMSID() (fakeID string) {
	return fmt.Sprintf("FAKE_SMS_ID:%s", fakeIDGenerator.GetString(10))
}

func IsRealSMSID(smsID string) (isReal bool) {
	return !strings.HasPrefix(smsID, "FAKE_SMS_ID:")
}


// HandleChangeDeviceStatus implements TMobClienter.
func (c *TMobClient) ChangeDeviceStatus(ctx context.Context, cda ChangeDeviceActivation) (err error) {
	var fn func(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error)
	if cda.Enabled {
		fn = c.handleServiceRestore
	}else {
		fn = c.handleServiceCancel
	}
	for _, iccid := range cda.ICCIDs {
		iccid = strings.TrimSuffix(strings.TrimSuffix(iccid, "F"), "f")
		temp := ICCIDBody{
			ICCID: iccid,
		}
		_, tErr := fn(ctx, &temp)
		if tErr != nil {
			err = errorsO.Join(err, tErr)
		}
	}
	return err
}

// sets status as ACTIVATED
func (c *TMobClient) handleServiceRestore(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error){
	path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/service-restore"
	fullPath := c.baseURL.String() + path

	payload := new(bytes.Buffer)
	err = json.NewEncoder(payload).Encode(in)
	if err != nil {
		return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
	}
	payloadStr := payload.String()

	emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
		SetURI(path).
		SetHTTPMethod(http.MethodPost).
		SetContentType(applicationJSON).
		SetBody(payloadStr)
	popToken, err := c.tg.Generate(emap)
	if err != nil {
		return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
	if err != nil {
		return nil, err
	}

	c.lock.RLock()
	defer c.lock.RUnlock()
	req.Header.Set(Authorization, c.accessToken)
	req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
	req.Header.Set(XAuthorization, popToken)
	req.Header.Set(contentType, applicationJSON)
	out = &ICCIDBody{}

	err = c.do(req, &out)

	if err != nil {
		return nil, errors.WithMessage(err, failedToDoRequest)
	}
	return
}

// sets status as DEACTIVATED
func (c *TMobClient) handleServiceCancel(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error){
	path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/service-suspend"
	fullPath := c.baseURL.String() + path

	payload := new(bytes.Buffer)
	err = json.NewEncoder(payload).Encode(in)
	if err != nil {
		return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
	}
	payloadStr := payload.String()

	emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
		SetURI(path).
		SetHTTPMethod(http.MethodPost).
		SetContentType(applicationJSON).
		SetBody(payloadStr)
	popToken, err := c.tg.Generate(emap)
	if err != nil {
		return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
	if err != nil {
		return nil, err
	}

	c.lock.RLock()
	defer c.lock.RUnlock()
	req.Header.Set(Authorization, c.accessToken)
	req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
	req.Header.Set(XAuthorization, popToken)
	req.Header.Set(contentType, applicationJSON)
	out = &ICCIDBody{}
	
	err = c.do(req, &out)

	if err != nil {
		return nil, errors.WithMessage(err, failedToDoRequest)
	}
	return
}
