package s3api

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	"strings"

	"github.com/dustin/go-humanize"
	"github.com/gorilla/mux"
	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/s3api/policy"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)

func (s3a *S3ApiServer) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {

	// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
	// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html

	bucket := mux.Vars(r)["bucket"]

	glog.V(3).Infof("PostPolicyBucketHandler %s", bucket)

	reader, err := r.MultipartReader()
	if err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest)
		return
	}
	form, err := reader.ReadForm(int64(5 * humanize.MiByte))
	if err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest)
		return
	}
	defer form.RemoveAll()

	fileBody, fileName, fileContentType, fileSize, formValues, err := extractPostPolicyFormValues(form)
	if err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest)
		return
	}
	if fileBody == nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrPOSTFileRequired)
		return
	}
	defer fileBody.Close()

	formValues.Set("Bucket", bucket)

	if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
		formValues.Set("Key", strings.Replace(formValues.Get("Key"), "${filename}", fileName, -1))
	}
	rawObject := formValues.Get("Key")
	if rawObject == "" || !s3_constants.IsValidObjectKey(rawObject) {
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}
	object := s3_constants.NormalizeObjectKey(rawObject)
	if err := s3a.validateTableBucketObjectPath(bucket, object); err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
		return
	}

	successRedirect := formValues.Get("success_action_redirect")
	successStatus := formValues.Get("success_action_status")
	var redirectURL *url.URL
	if successRedirect != "" {
		redirectURL, err = url.Parse(successRedirect)
		if err != nil {
			s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest)
			return
		}
	}

	// Verify policy signature.
	identity, errCode := s3a.iam.doesPolicySignatureMatch(formValues)
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}
	if identity != nil {
		r = r.WithContext(s3_constants.SetIdentityNameInContext(r.Context(), identity.Name))
	}

	policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
	if err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest)
		return
	}

	// Handle policy if it is set.
	if len(policyBytes) > 0 {

		postPolicyForm, err := policy.ParsePostPolicyForm(string(policyBytes))
		if err != nil {
			s3err.WriteErrorResponse(w, r, s3err.ErrPostPolicyConditionInvalidFormat)
			return
		}

		// Make sure formValues adhere to policy restrictions.
		if err = policy.CheckPostPolicy(formValues, postPolicyForm); err != nil {
			glog.V(3).Infof("PostPolicy check failed for bucket %s: %v", bucket, err)
			s3err.WriteErrorResponseWithMessage(w, r, s3err.ErrAccessDenied, err.Error())
			return
		}

		// Ensure that the object size is within expected range, also the file size
		// should not exceed the maximum single Put size (5 GiB)
		lengthRange := postPolicyForm.Conditions.ContentLengthRange
		if lengthRange.Valid {
			if fileSize < lengthRange.Min {
				s3err.WriteErrorResponse(w, r, s3err.ErrEntityTooSmall)
				return
			}

			if fileSize > lengthRange.Max {
				s3err.WriteErrorResponse(w, r, s3err.ErrEntityTooLarge)
				return
			}
		}
	}

	filePath := fmt.Sprintf("%s/%s", s3a.bucketDir(bucket), object)

	// Get ContentType from post formData
	// Otherwise from formFile ContentType
	contentType := formValues.Get("Content-Type")
	if contentType == "" {
		contentType = fileContentType
	}
	r.Header.Set("Content-Type", contentType)

	// Forward validated POST form fields to the underlying PUT as headers.
	applyPostPolicyFormHeaders(r, formValues)

	// Authorize the object like the PUT path; the coarse Write check above does not consult the bucket policy.
	if errCode := s3a.iam.AuthorizeObjectWrite(r, identity, bucket, object); errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	// Use fileSize, not r.ContentLength: the multipart body wrapping form
	// fields and boundaries inflates ContentLength relative to the
	// object body, which would mis-evaluate any size-filtered rule.
	ttlSec := s3a.lifecycleTTLForObjectWrite(bucket, object, fileSize)
	etag, errCode, sseMetadata := s3a.putToFiler(r, filePath, fileBody, bucket, object, 1, ttlSec, nil, false, "")

	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	if successRedirect != "" {
		// Replace raw query params..
		redirectURL.RawQuery = getRedirectPostRawQuery(bucket, object, etag)
		w.Header().Set("Location", redirectURL.String())
		s3err.WriteEmptyResponse(w, r, http.StatusSeeOther)
		return
	}

	setEtag(w, etag)
	// Include SSE response headers (important for bucket-default encryption)
	s3a.setSSEResponseHeaders(w, r, sseMetadata)

	// Decide what http response to send depending on success_action_status parameter
	switch successStatus {
	case "201":
		resp := PostResponse{
			Bucket:   bucket,
			Key:      object,
			ETag:     `"` + etag + `"`,
			Location: w.Header().Get("Location"),
		}
		s3err.WriteXMLResponse(w, r, http.StatusCreated, resp)
		s3err.PostLog(r, http.StatusCreated, s3err.ErrNone)
	case "200":
		s3err.WriteEmptyResponse(w, r, http.StatusOK)
	case "204":
		s3err.WriteEmptyResponse(w, r, http.StatusNoContent)
	default:
		s3err.WriteEmptyResponse(w, r, http.StatusNoContent)
	}

}

// postPolicyReservedFormFields are multipart form fields that are part of the
// POST Object auth/policy mechanism (or handled explicitly elsewhere in the
// handler) and must not be forwarded to the upload as HTTP headers. Keys are
// already run through http.CanonicalHeaderKey before lookup.
var postPolicyReservedFormFields = map[string]struct{}{
	// POST policy signature (V2)
	"Policy":         {},
	"Signature":      {},
	"Awsaccesskeyid": {},
	// POST policy signature (V4)
	"X-Amz-Signature":      {},
	"X-Amz-Credential":     {},
	"X-Amz-Algorithm":      {},
	"X-Amz-Date":           {},
	"X-Amz-Security-Token": {},
	// Target descriptors
	"Key":    {},
	"File":   {},
	"Bucket": {},
	// Success actions (handled elsewhere in the handler)
	"Success_action_redirect": {},
	"Success_action_status":   {},
	"Redirect":                {},
	// Content-Type is resolved separately above (from form or file part)
	"Content-Type": {},
}

// applyPostPolicyFormHeaders forwards validated POST Object form fields to the
// request headers so they are applied to the resulting PUT. Reserved fields
// that are part of the POST policy mechanism itself (signature, key, etc.) are
// skipped. The acl form field is translated to the X-Amz-Acl header to match
// how AWS promotes the form value to the underlying PUT.
func applyPostPolicyFormHeaders(r *http.Request, formValues http.Header) {
	for k := range formValues {
		if _, reserved := postPolicyReservedFormFields[k]; reserved {
			continue
		}
		switch {
		case k == "Acl":
			r.Header.Set(s3_constants.AmzCannedAcl, formValues.Get(k))
		case k == "Cache-Control",
			k == "Expires",
			k == "Content-Disposition",
			k == "Content-Encoding",
			k == "Content-Language":
			r.Header.Set(k, formValues.Get(k))
		case strings.HasPrefix(k, "X-Amz-"):
			r.Header.Set(k, formValues.Get(k))
		}
	}
}

// Extract form fields and file data from a HTTP POST Policy
func extractPostPolicyFormValues(form *multipart.Form) (filePart io.ReadCloser, fileName, fileContentType string, fileSize int64, formValues http.Header, err error) {
	// / HTML Form values
	fileName = ""
	fileContentType = ""

	// Canonicalize the form values into http.Header.
	formValues = make(http.Header)
	for k, v := range form.Value {
		formValues[http.CanonicalHeaderKey(k)] = v
	}

	// Validate form values.
	if err = validateFormFieldSize(formValues); err != nil {
		return nil, "", "", 0, nil, err
	}

	// this means that filename="" was not specified for file key and Go has
	// an ugly way of handling this situation. Refer here
	// https://golang.org/src/mime/multipart/formdata.go#L61
	if len(form.File) == 0 {
		var b = &bytes.Buffer{}
		for _, v := range formValues["File"] {
			b.WriteString(v)
		}
		fileSize = int64(b.Len())
		filePart = io.NopCloser(b)
		return filePart, fileName, fileContentType, fileSize, formValues, nil
	}

	// Iterator until we find a valid File field and break
	for k, v := range form.File {
		canonicalFormName := http.CanonicalHeaderKey(k)
		if canonicalFormName == "File" {
			if len(v) == 0 {
				return nil, "", "", 0, nil, errors.New("Invalid arguments specified")
			}
			// Fetch fileHeader which has the uploaded file information
			fileHeader := v[0]
			// Set filename
			fileName = fileHeader.Filename
			// Set contentType
			fileContentType = fileHeader.Header.Get("Content-Type")
			// Open the uploaded part
			filePart, err = fileHeader.Open()
			if err != nil {
				return nil, "", "", 0, nil, err
			}
			// Compute file size
			fileSize, err = filePart.(io.Seeker).Seek(0, 2)
			if err != nil {
				return nil, "", "", 0, nil, err
			}
			// Reset Seek to the beginning
			_, err = filePart.(io.Seeker).Seek(0, 0)
			if err != nil {
				return nil, "", "", 0, nil, err
			}
			// File found and ready for reading
			break
		}
	}
	return filePart, fileName, fileContentType, fileSize, formValues, nil
}

// Validate form field size for s3 specification requirement.
func validateFormFieldSize(formValues http.Header) error {
	// Iterate over form values
	for k := range formValues {
		// Check if value's field exceeds S3 limit
		if int64(len(formValues.Get(k))) > int64(1*humanize.MiByte) {
			return errors.New("Data size larger than expected")
		}
	}

	// Success.
	return nil
}

func getRedirectPostRawQuery(bucket, key, etag string) string {
	redirectValues := make(url.Values)
	redirectValues.Set("bucket", bucket)
	redirectValues.Set("key", key)
	redirectValues.Set("etag", "\""+etag+"\"")
	return redirectValues.Encode()
}

// Check to see if Policy is signed correctly, returning the signing identity.
func (iam *IdentityAccessManagement) doesPolicySignatureMatch(formValues http.Header) (*Identity, s3err.ErrorCode) {
	// For SignV2 - Signature field will be valid
	if _, ok := formValues["Signature"]; ok {
		return iam.doesPolicySignatureV2Match(formValues)
	}
	return iam.doesPolicySignatureV4Match(formValues)
}
