package recaptcha

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"time"
)

const reCAPTCHALink = "https://www.recaptcha.net/recaptcha/api/siteverify"

// VERSION the recaptcha api version
type VERSION int8

const (
	// V2 recaptcha api v2
	V2 VERSION = iota
	// V3 recaptcha api v3, more details can be found here : https://developers.google.com/recaptcha/docs/v3
	V3
	// DefaultTreshold Default minimin score when using V3 api
	DefaultTreshold float32 = 0.5
)

type reCHAPTCHARequest struct {
	Secret   string `json:"secret"`
	Response string `json:"response"`
	RemoteIP string `json:"remoteip,omitempty"`
}

type reCHAPTCHAResponse struct {
	Success        bool      `json:"success"`
	ChallengeTS    time.Time `json:"challenge_ts"`
	Hostname       string    `json:"hostname,omitempty"`
	ApkPackageName string    `json:"apk_package_name,omitempty"`
	Action         string    `json:"action,omitempty"`
	Score          float32   `json:"score,omitempty"`
	ErrorCodes     []string  `json:"error-codes,omitempty"`
}

// custom client so we can mock in tests
type netClient interface {
	PostForm(url string, formValues url.Values) (resp *http.Response, err error)
}

// custom clock so we can mock in tests
type clock interface {
	Since(t time.Time) time.Duration
}

type realClock struct {
}

func (realClock) Since(t time.Time) time.Duration {
	return time.Since(t)
}

// ReCAPTCHA recpatcha holder struct, make adding mocking code simpler
type ReCAPTCHA struct {
	client        netClient
	Secret        string
	ReCAPTCHALink string
	Version       VERSION
	Timeout       time.Duration
	horloge       clock
}

// NewReCAPTCHA new ReCAPTCHA instance if version is set to V2 uses recatpcha v2 API, get your secret from https://www.google.com/recaptcha/admin
//  if version is set to V2 uses recatpcha v2 API, get your secret from https://g.co/recaptcha/v3
func NewReCAPTCHA(ReCAPTCHASecret string, version VERSION, timeout time.Duration) (ReCAPTCHA, error) {
	if ReCAPTCHASecret == "" {
		return ReCAPTCHA{}, fmt.Errorf("recaptcha secret cannot be blank")
	}
	return ReCAPTCHA{
		client: &http.Client{
			Timeout: timeout,
		},
		horloge:       &realClock{},
		Secret:        ReCAPTCHASecret,
		ReCAPTCHALink: reCAPTCHALink,
		Timeout:       timeout,
		Version:       version,
	}, nil
}

// Verify returns `nil` if no error and the client solved the challenge correctly
func (r *ReCAPTCHA) Verify(challengeResponse string) error {
	body := reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse}
	return r.confirm(body, VerifyOption{})
}

// VerifyOption verification options expected for the challenge
type VerifyOption struct {
	Threshold      float32 // ignored in v2 recaptcha
	Action         string  // ignored in v2 recaptcha
	Hostname       string
	ApkPackageName string
	ResponseTime   time.Duration
	RemoteIP       string
}

// VerifyWithOptions returns `nil` if no error and the client solved the challenge correctly and all options are natching
// `Threshold` and `Action` are ignored when using V2 version
func (r *ReCAPTCHA) VerifyWithOptions(challengeResponse string, options VerifyOption) error {
	var body reCHAPTCHARequest
	if options.RemoteIP == "" {
		body = reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse}
	} else {
		body = reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse, RemoteIP: options.RemoteIP}
	}
	return r.confirm(body, options)
}

func (r *ReCAPTCHA) confirm(recaptcha reCHAPTCHARequest, options VerifyOption) (Err error) {
	Err = nil
	var formValues url.Values
	if recaptcha.RemoteIP != "" {
		formValues = url.Values{"secret": {recaptcha.Secret}, "remoteip": {recaptcha.RemoteIP}, "response": {recaptcha.Response}}
	} else {
		formValues = url.Values{"secret": {recaptcha.Secret}, "response": {recaptcha.Response}}
	}
	response, err := r.client.PostForm(r.ReCAPTCHALink, formValues)
	if err != nil {
		Err = fmt.Errorf("error posting to recaptcha endpoint: '%s'", err)
		return
	}
	defer response.Body.Close()
	resultBody, err := ioutil.ReadAll(response.Body)
	if err != nil {
		Err = fmt.Errorf("couldn't read response body: '%s'", err)
		return
	}
	var result reCHAPTCHAResponse
	err = json.Unmarshal(resultBody, &result)
	if err != nil {
		Err = fmt.Errorf("invalid response body json: '%s'", err)
		return
	}

	if options.Hostname != "" && options.Hostname != result.Hostname {
		Err = fmt.Errorf("invalid response hostname '%s', while expecting '%s'", result.Hostname, options.Hostname)
		return
	}

	if options.ApkPackageName != "" && options.ApkPackageName != result.ApkPackageName {
		Err = fmt.Errorf("invalid response ApkPackageName '%s', while expecting '%s'", result.ApkPackageName, options.ApkPackageName)
		return
	}

	if options.ResponseTime != 0 {
		duration := r.horloge.Since(result.ChallengeTS)
		if options.ResponseTime < duration {
			Err = fmt.Errorf("time spent in resolving challenge '%fs', while expecting maximum '%fs'", duration.Seconds(), options.ResponseTime.Seconds())
			return
		}
	}
	if r.Version == V3 {
		if options.Action != "" && options.Action != result.Action {
			Err = fmt.Errorf("invalid response action '%s', while expecting '%s'", result.Action, options.Action)
			return
		}
		if options.Threshold != 0 && options.Threshold >= result.Score {
			Err = fmt.Errorf("received score '%f', while expecting minimum '%f'", result.Score, options.Threshold)
			return
		}
		if options.Threshold == 0 && DefaultTreshold >= result.Score {
			Err = fmt.Errorf("received score '%f', while expecting minimum '%f'", result.Score, DefaultTreshold)
			return
		}
	}
	if result.ErrorCodes != nil {
		Err = fmt.Errorf("remote error codes: %v", result.ErrorCodes)
		return
	}
	if !result.Success && recaptcha.RemoteIP != "" {
		Err = fmt.Errorf("invalid challenge solution or remote IP")
	} else if !result.Success {
		Err = fmt.Errorf("invalid challenge solution")
	}
	return
}
