package middleware

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

	"github.com/cloudreve/Cloudreve/v4/application/dependency"
	"github.com/cloudreve/Cloudreve/v4/pkg/logging"
	"github.com/cloudreve/Cloudreve/v4/pkg/recaptcha"
	request2 "github.com/cloudreve/Cloudreve/v4/pkg/request"
	"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
	"github.com/cloudreve/Cloudreve/v4/pkg/setting"
	"github.com/gin-gonic/gin"
	"github.com/mojocn/base64Captcha"
)

type req struct {
	Captcha string `json:"captcha"`
	Ticket  string `json:"ticket"`
	Randstr string `json:"randstr"`
}

const (
	captchaNotMatch = "CAPTCHA not match."
	captchaRefresh  = "Verification failed, please refresh the page and retry."

	tcCaptchaEndpoint = "captcha.tencentcloudapi.com"
	turnstileEndpoint = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
)

// CaptchaIDCtx defines keys for captcha ID
type (
	CaptchaIDCtx      struct{}
	turnstileResponse struct {
		Success bool `json:"success"`
	}
	capResponse struct {
		Success bool `json:"success"`
	}
)

// CaptchaRequired 验证请求签名
func CaptchaRequired(enabled func(c *gin.Context) bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		if enabled(c) {
			dep := dependency.FromContext(c)
			settings := dep.SettingProvider()
			l := logging.FromContext(c)

			var service req
			bodyCopy := new(bytes.Buffer)
			_, err := io.Copy(bodyCopy, c.Request.Body)
			if err != nil {
				c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, captchaNotMatch, err))
				c.Abort()
				return
			}

			bodyData := bodyCopy.Bytes()
			err = json.Unmarshal(bodyData, &service)
			if err != nil {
				c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, captchaNotMatch, err))
				c.Abort()
				return
			}

			c.Request.Body = io.NopCloser(bytes.NewReader(bodyData))
			switch settings.CaptchaType(c) {
			case setting.CaptchaNormal, setting.CaptchaTcaptcha:
				if service.Ticket == "" || !base64Captcha.VerifyCaptcha(service.Ticket, service.Captcha) {
					c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, captchaNotMatch, err))
					c.Abort()
					return
				}

				break
			case setting.CaptchaReCaptcha:
				captchaSetting := settings.ReCaptcha(c)
				reCAPTCHA, err := recaptcha.NewReCAPTCHA(captchaSetting.Secret, recaptcha.V2, 10*time.Second)
				if err != nil {
					l.Warning("reCAPTCHA verification failed, %s", err)
					c.Abort()
					break
				}

				err = reCAPTCHA.Verify(service.Captcha)
				if err != nil {
					l.Warning("reCAPTCHA verification failed, %s", err)
					c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, captchaRefresh, err))
					c.Abort()
					return
				}

				break
			case setting.CaptchaTurnstile:
				captchaSetting := settings.TurnstileCaptcha(c)
				r := dep.RequestClient(
					request2.WithContext(c),
					request2.WithLogger(logging.FromContext(c)),
					request2.WithHeader(http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}}),
				)
				formData := url.Values{}
				formData.Set("secret", captchaSetting.Secret)
				formData.Set("response", service.Ticket)
				res, err := r.Request("POST", turnstileEndpoint, strings.NewReader(formData.Encode())).
					CheckHTTPResponse(http.StatusOK).
					GetResponse()
				if err != nil {
					c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
					c.Abort()
					return
				}

				var trunstileRes turnstileResponse
				err = json.Unmarshal([]byte(res), &trunstileRes)
				if err != nil {
					l.Warning("Turnstile verification failed, %s", err)
					c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
					c.Abort()
					return
				}

				if !trunstileRes.Success {
					c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
					c.Abort()
					return
				}

				break
			case setting.CaptchaCap:
				captchaSetting := settings.CapCaptcha(c)
				if captchaSetting.InstanceURL == "" || captchaSetting.SiteKey == "" || captchaSetting.SecretKey == "" {
					l.Warning("Cap verification failed: missing configuration")
					c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha configuration error", nil))
					c.Abort()
					return
				}

				r := dep.RequestClient(
					request2.WithContext(c),
					request2.WithLogger(logging.FromContext(c)),
					request2.WithHeader(http.Header{"Content-Type": []string{"application/json"}}),
				)

				// Cap 2.0 API format: /{siteKey}/siteverify
				capEndpoint := strings.TrimSuffix(captchaSetting.InstanceURL, "/") + "/" + captchaSetting.SiteKey + "/siteverify"
				requestBody := map[string]string{
					"secret":   captchaSetting.SecretKey,
					"response": service.Ticket,
				}
				requestData, err := json.Marshal(requestBody)
				if err != nil {
					l.Warning("Cap verification failed: %s", err)
					c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
					c.Abort()
					return
				}

				res, err := r.Request("POST", capEndpoint, strings.NewReader(string(requestData))).
					CheckHTTPResponse(http.StatusOK).
					GetResponse()
				if err != nil {
					l.Warning("Cap verification failed: %s", err)
					c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
					c.Abort()
					return
				}

				var capRes capResponse
				err = json.Unmarshal([]byte(res), &capRes)
				if err != nil {
					l.Warning("Cap verification failed: %s", err)
					c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
					c.Abort()
					return
				}

				if !capRes.Success {
					l.Warning("Cap verification failed: validation returned false")
					c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", nil))
					c.Abort()
					return
				}

				break
			}
		}
		c.Next()
	}
}
