package notificationservice

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"bitbucket.org/nemt/nemt-portal-api/application/entitymapping"
	"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
	"bitbucket.org/nemt/nemt-portal-api/domain"
	"bitbucket.org/nemt/nemt-portal-api/domain/contract"
	"bitbucket.org/nemt/nemt-portal-api/domain/service"
	"bitbucket.org/nemt/nemt-portal-api/infra/config"
)

const (
	twilioMessageURL      = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json"
	twilioNumbersURL      = "https://api.twilio.com/2010-04-01/Accounts/%s/AvailablePhoneNumbers/US/Local.json"
	twiliActualNumbersURL = "https://api.twilio.com/2010-04-01/Accounts/%s/IncomingPhoneNumbers.json"
)

// twilioService holds methods to twillio application service
type twilioService struct {
	svc             *service.Service
	mapEntity       *entitymapping.Mapper
	cfg             *config.Config
	urlMessage      string
	urlNumber       string
	urlActualNumber string
	cache           contract.CacheManager
}

// newTwilioService returns a twilioService instance
func newTwilioService(svc *service.Service, mapper *entitymapping.Mapper, cfg *config.Config, cache contract.CacheManager) *twilioService {
	return &twilioService{
		svc:             svc,
		mapEntity:       mapper,
		cfg:             cfg,
		urlMessage:      fmt.Sprintf(twilioMessageURL, cfg.Twilio.Account),
		urlNumber:       fmt.Sprintf(twilioNumbersURL, cfg.Twilio.Account),
		urlActualNumber: fmt.Sprintf(twiliActualNumbersURL, cfg.Twilio.Account),
		cache:           cache,
	}
}

func (s twilioService) GetProxyNumber(to string) (viewmodel.ProxyNumber, error) {
	key := "proxy" + to
	var number viewmodel.ProxyNumber
	err := s.cache.GetStruct(key, &number)
	if err != nil || err == domain.ErrCacheMiss {
		return number, err
	} else {
		return number, nil
	}
}

func (s twilioService) GetAvaliableNumbers(to string) (viewmodel.ListNumbers, error) {
	if to != "" {
		if strings.Contains(to, "+1") {
			to = strings.Replace(to, "+1", "", -1)
		}

		area := to[0:3]

		msgData := url.Values{}
		msgData.Set("PhoneNumber", area)

		client := &http.Client{}
		req, _ := http.NewRequest("GET", s.urlActualNumber+"?"+msgData.Encode(), bytes.NewBuffer([]byte{}))

		req.SetBasicAuth(s.cfg.Twilio.Account, s.cfg.Twilio.Token)
		resp, err := client.Do(req)
		if err != nil {
			fmt.Println("Error Twilio: ", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			var data viewmodel.ListNumbers
			decoder := json.NewDecoder(resp.Body)
			err := decoder.Decode(&data)
			if err != nil {
				return viewmodel.ListNumbers{}, err
			}

			return data, nil
		} else {
			return viewmodel.ListNumbers{}, errors.New(resp.Status)
		}
	}
	return viewmodel.ListNumbers{}, nil
}

func (s twilioService) GetAvaliableNewNumbers(to string) (viewmodel.ListNumbers, error) {
	if to != "" {
		if strings.Contains(to, "+1") {
			to = strings.Replace(to, "+1", "", -1)
		}

		area := to[0:3]

		msgData := url.Values{}
		msgData.Set("AreaCode", area)

		client := &http.Client{}
		req, _ := http.NewRequest("GET", s.urlNumber+"?"+msgData.Encode(), bytes.NewBuffer([]byte{}))

		req.SetBasicAuth(s.cfg.Twilio.Account, s.cfg.Twilio.Token)
		resp, err := client.Do(req)
		if err != nil {
			fmt.Println("Error Twilio: ", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			var data viewmodel.ListNumbers
			decoder := json.NewDecoder(resp.Body)
			err := decoder.Decode(&data)
			if err != nil {
				return viewmodel.ListNumbers{}, err
			}

			return data, nil
		} else {
			return viewmodel.ListNumbers{}, errors.New(resp.Status)
		}
	}
	return viewmodel.ListNumbers{}, nil
}

func (s twilioService) SaveNewNumber(number viewmodel.Number) (viewmodel.Number, error) {
	phoneNumber := number.PhoneNumber
	if strings.Contains(phoneNumber, "+1") {
		phoneNumber = strings.Replace(phoneNumber, "+1", "", -1)
	}
	area := phoneNumber[0:3]

	msgData := url.Values{}
	msgData.Set("PhoneNumber", number.PhoneNumber)
	msgData.Set("AreaCode", area)
	msgData.Set("FriendlyName", number.FriendlyName)
	msgData.Set("SmsApplicationSid", s.cfg.Twilio.TwiMLSID)
	msgDataReader := *strings.NewReader(msgData.Encode())

	client := &http.Client{}
	req, _ := http.NewRequest("POST", s.urlActualNumber, &msgDataReader)
	req.Header.Add("Accept", "application/json")
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(s.cfg.Twilio.Account, s.cfg.Twilio.Token)
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Error Twilio: ", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		decoder := json.NewDecoder(resp.Body)
		err := decoder.Decode(&number)
		if err != nil {
			return viewmodel.Number{}, err
		}

		return number, nil
	} else {
		return viewmodel.Number{}, errors.New(resp.Status)
	}
}

func (s twilioService) GetValidNumber(from string, to string) (viewmodel.Number, error) {
	list, err := s.GetAvaliableNumbers(to)
	if err != nil {
		return viewmodel.Number{}, err
	}

	if len(list.IncomingPhones) > 0 {
		return list.IncomingPhones[0], nil
	}

	list, err = s.GetAvaliableNewNumbers(to)
	if err != nil {
		return viewmodel.Number{}, err
	}

	if len(list.AvailableNumbers) > 0 {
		number, err := s.SaveNewNumber(list.AvailableNumbers[0])
		if err != nil {
			fmt.Println("Error on GetValidNumber: ", err.Error())
			return viewmodel.Number{}, err
		}

		return number, nil
	} else {
		return viewmodel.Number{}, errors.New("No new number available")
	}
}

func (s twilioService) GetNumber(from string, to string) (string, error) {
	number, err := s.GetValidNumber(from, to)
	if err != nil {
		return from, err
	}

	return number.PhoneNumber, nil
}

func (s twilioService) SendSMS(from string, to string, message string) error {
	if from == "" {
		return errors.New("TwilioService.SendSMS: from parameter is mandatory")
	}
	if to == "" {
		return errors.New("TwilioService.SendSMS: to parameter is mandatory")
	}
	if message == "" {
		return errors.New("TwilioService.SendSMS: message parameter is mandatory")
	}

	senderNumber, err := s.GetNumber(from, to)
	if err != nil {
		return err
	}

	msgData := url.Values{}
	msgData.Set("To", to)
	msgData.Set("From", senderNumber)
	msgData.Set("Body", message)

	msgDataReader := *strings.NewReader(msgData.Encode())

	client := &http.Client{}
	req, _ := http.NewRequest("POST", s.urlMessage, &msgDataReader)

	req.SetBasicAuth(s.cfg.Twilio.Account, s.cfg.Twilio.Token)
	req.Header.Add("Accept", "application/json")
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Error Twilio: ", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		var data map[string]interface{}
		decoder := json.NewDecoder(resp.Body)
		err := decoder.Decode(&data)
		if err != nil {
			return err
		}
	} else {
		fmt.Println("Error on Twilio Response: ", resp.Status)
		return errors.New(resp.Status)
	}

	return nil
}
