package sms_provider

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

	"github.com/supabase/auth/internal/conf"
	"github.com/supabase/auth/internal/utilities"
)

const (
	verifyServiceApiBase = "https://verify.twilio.com/v2/Services/"
)

type TwilioVerifyProvider struct {
	Config  *conf.TwilioVerifyProviderConfiguration
	APIPath string
}

type VerificationResponse struct {
	To              string `json:"to"`
	Status          string `json:"status"`
	Channel         string `json:"channel"`
	Valid           bool   `json:"valid"`
	VerificationSID string `json:"sid"`
	ErrorCode       string `json:"error_code"`
	ErrorMessage    string `json:"error_message"`
}

// See: https://www.twilio.com/docs/verify/api/verification-check
type VerificationCheckResponse struct {
	To           string `json:"to"`
	Status       string `json:"status"`
	Channel      string `json:"channel"`
	Valid        bool   `json:"valid"`
	ErrorCode    string `json:"error_code"`
	ErrorMessage string `json:"error_message"`
}

// Creates a SmsProvider with the Twilio Config
func NewTwilioVerifyProvider(config conf.TwilioVerifyProviderConfiguration) (SmsProvider, error) {
	if err := config.Validate(); err != nil {
		return nil, err
	}
	apiPath := verifyServiceApiBase + config.MessageServiceSid + "/Verifications"

	return &TwilioVerifyProvider{
		Config:  &config,
		APIPath: apiPath,
	}, nil
}

func (t *TwilioVerifyProvider) SendMessage(phone, message, channel, otp string) (string, error) {
	switch channel {
	case SMSProvider, WhatsappProvider:
		return t.SendSms(phone, message, channel)
	default:
		return "", fmt.Errorf("channel type %q is not supported for Twilio", channel)
	}
}

// Send an SMS containing the OTP with Twilio's API
func (t *TwilioVerifyProvider) SendSms(phone, message, channel string) (string, error) {
	// Unlike Programmable Messaging, Verify does not require a prefix for channel
	receiver := "+" + phone
	body := url.Values{
		"To":      {receiver},
		"Channel": {channel},
	}
	client := &http.Client{Timeout: defaultTimeout}
	r, err := http.NewRequest("POST", t.APIPath, strings.NewReader(body.Encode()))
	if err != nil {
		return "", err
	}
	r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	r.SetBasicAuth(t.Config.AccountSid, t.Config.AuthToken)
	res, err := client.Do(r)
	if err != nil {
		return "", err
	}
	defer utilities.SafeClose(res.Body)
	if !(res.StatusCode == http.StatusOK || res.StatusCode == http.StatusCreated) {
		resp := &twilioErrResponse{}
		if err := json.NewDecoder(res.Body).Decode(resp); err != nil {
			return "", err
		}
		return "", resp
	}

	resp := &VerificationResponse{}
	derr := json.NewDecoder(res.Body).Decode(resp)
	if derr != nil {
		return "", derr
	}
	return resp.VerificationSID, nil
}

func (t *TwilioVerifyProvider) VerifyOTP(phone, code string) error {
	verifyPath := verifyServiceApiBase + t.Config.MessageServiceSid + "/VerificationCheck"
	receiver := "+" + phone

	body := url.Values{
		"To":   {receiver}, // twilio api requires "+" extension to be included
		"Code": {code},
	}
	client := &http.Client{Timeout: defaultTimeout}
	r, err := http.NewRequest("POST", verifyPath, strings.NewReader(body.Encode()))
	if err != nil {
		return err
	}
	r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	r.SetBasicAuth(t.Config.AccountSid, t.Config.AuthToken)
	res, err := client.Do(r)
	if err != nil {
		return err
	}
	defer utilities.SafeClose(res.Body)
	if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated {
		resp := &twilioErrResponse{}
		if err := json.NewDecoder(res.Body).Decode(resp); err != nil {
			return err
		}
		return resp
	}
	resp := &VerificationCheckResponse{}
	derr := json.NewDecoder(res.Body).Decode(resp)
	if derr != nil {
		return derr
	}

	if resp.Status != "approved" || !resp.Valid {
		return fmt.Errorf("twilio verification error: %v %v", resp.ErrorMessage, resp.Status)
	}

	return nil
}
