/*
 * Copyright 2021 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package descriptor

import (
	"context"
	"errors"

	"github.com/cloudwego/kitex/internal/generic/proto"
)

// HTTPMapping http mapping annotation
type HTTPMapping interface {
	// get value from request
	Request(ctx context.Context, req *HTTPRequest, field *FieldDescriptor) (interface{}, bool, error)
	// set value to response
	Response(ctx context.Context, resp *HTTPResponse, field *FieldDescriptor, val interface{}) error
}

// NewHTTPMapping HTTPMapping creator
// api.query = value
type NewHTTPMapping func(value string) HTTPMapping

// DefaultNewMapping the default mapping creator
var DefaultNewMapping = NewAPIBody

var (
	// APIQueryAnnotation api.path annotation
	APIQueryAnnotation = NewBAMAnnotation("api.query", NewAPIQuery)
	// APIPathAnnotation api.path annotation
	APIPathAnnotation = NewBAMAnnotation("api.path", NewAPIPath)
	// APIHeaderAnnotation api.header annotation
	APIHeaderAnnotation = NewBAMAnnotation("api.header", NewAPIHeader)
	// APICookieAnnotation api.cookie annotation
	APICookieAnnotation = NewBAMAnnotation("api.cookie", NewAPICookie)
	// APIBodyAnnotation api.body annotation
	APIBodyAnnotation = NewBAMAnnotation("api.body", NewAPIBody)
	// APIHttpCodeAnnotation api.http_code annotation
	APIHttpCodeAnnotation = NewBAMAnnotation("api.http_code", NewAPIHTTPCode)
	// APINoneAnnotation api.none annotation
	APINoneAnnotation = NewBAMAnnotation("api.none", NewAPINone)
	// APIRawBodyAnnotation api.raw_body annotation
	APIRawBodyAnnotation = NewBAMAnnotation("api.raw_body", NewAPIRawBody)
)

// api.query = xx
type apiQuery struct {
	value string // == xx
}

// NewAPIQuery ...
var NewAPIQuery NewHTTPMapping = func(value string) HTTPMapping {
	return &apiQuery{value}
}

func (m *apiQuery) Request(ctx context.Context, req *HTTPRequest, field *FieldDescriptor) (interface{}, bool, error) {
	val := req.GetQuery(m.value)
	return val, val != "", nil
}

func (*apiQuery) Response(ctx context.Context, resp *HTTPResponse, field *FieldDescriptor, val interface{}) error {
	return nil
}

// api.path = xx
type apiPath struct {
	value string
}

// NewAPIPath ...
var NewAPIPath NewHTTPMapping = func(value string) HTTPMapping {
	return &apiPath{value}
}

func (m *apiPath) Request(ctx context.Context, req *HTTPRequest, field *FieldDescriptor) (interface{}, bool, error) {
	if req.Params == nil {
		return nil, false, nil
	}
	val := req.Params.ByName(m.value)
	return val, val != "", nil
}

func (*apiPath) Response(ctx context.Context, resp *HTTPResponse, field *FieldDescriptor, val interface{}) error {
	return nil
}

// api.header = xx
type apiHeader struct {
	value string
}

// NewAPIHeader ...
var NewAPIHeader NewHTTPMapping = func(value string) HTTPMapping {
	return &apiHeader{value}
}

func (m *apiHeader) Request(ctx context.Context, req *HTTPRequest, field *FieldDescriptor) (interface{}, bool, error) {
	val := req.GetHeader(m.value)
	return val, val != "", nil
}

func (m *apiHeader) Response(ctx context.Context, resp *HTTPResponse, field *FieldDescriptor, val interface{}) error {
	resp.Header.Set(m.value, convertToString(val))
	return nil
}

// api.cookie = xx
type apiCookie struct {
	value string
}

// NewAPICookie ...
var NewAPICookie NewHTTPMapping = func(value string) HTTPMapping {
	return &apiCookie{value}
}

func (m *apiCookie) Request(ctx context.Context, req *HTTPRequest, field *FieldDescriptor) (interface{}, bool, error) {
	val := req.GetCookie(m.value)
	if val == "" {
		return val, false, nil
	}
	return val, true, nil
}

func (m *apiCookie) Response(ctx context.Context, resp *HTTPResponse, field *FieldDescriptor, val interface{}) error {
	return nil
}

// api.body = xx
type apiBody struct {
	value string
}

// NewAPIBody ...
var NewAPIBody NewHTTPMapping = func(value string) HTTPMapping {
	return &apiBody{value}
}

func (m *apiBody) Request(ctx context.Context, req *HTTPRequest, field *FieldDescriptor) (interface{}, bool, error) {
	switch req.ContentType {
	case "", MIMEApplicationJson:
		val, ok := req.Body[m.value]
		return val, ok, nil
	case MIMEApplicationProtobuf:
		msg := req.GeneralBody.(proto.Message)
		val, err := msg.TryGetFieldByNumber(int(field.ID))
		return val, err == nil, nil
	default:
		return nil, false, errors.New("unsupported request content type")
	}
}

func (m *apiBody) Response(ctx context.Context, resp *HTTPResponse, field *FieldDescriptor, val interface{}) error {
	switch resp.ContentType {
	case "", MIMEApplicationJson:
		resp.Body[m.value] = val
		return nil
	case MIMEApplicationProtobuf:
		msg := resp.GeneralBody.(proto.Message)
		return msg.TrySetFieldByNumber(int(field.ID), val)
	default:
		return errors.New("unsupported response content type")
	}
}

type apiHTTPCode struct{}

// NewAPIHTTPCode ...
var NewAPIHTTPCode NewHTTPMapping = func(value string) HTTPMapping {
	return &apiHTTPCode{}
}

func (m *apiHTTPCode) Request(ctx context.Context, req *HTTPRequest, field *FieldDescriptor) (interface{}, bool, error) {
	return nil, false, nil
}

func (m *apiHTTPCode) Response(ctx context.Context, resp *HTTPResponse, field *FieldDescriptor, val interface{}) error {
	resp.StatusCode = convertToInt32(val)
	return nil
}

type apiNone struct{}

// NewAPINone ...
var NewAPINone NewHTTPMapping = func(value string) HTTPMapping {
	return &apiNone{}
}

func (m *apiNone) Request(ctx context.Context, req *HTTPRequest, field *FieldDescriptor) (interface{}, bool, error) {
	return nil, false, nil
}

func (m *apiNone) Response(ctx context.Context, resp *HTTPResponse, field *FieldDescriptor, val interface{}) error {
	return nil
}

type apiRawBody struct{}

// NewAPIRawBody ...
var NewAPIRawBody NewHTTPMapping = func(value string) HTTPMapping {
	return &apiRawBody{}
}

func (m *apiRawBody) Request(ctx context.Context, req *HTTPRequest, field *FieldDescriptor) (interface{}, bool, error) {
	return req.RawBody, true, nil
}

func (m *apiRawBody) Response(ctx context.Context, resp *HTTPResponse, field *FieldDescriptor, val interface{}) error {
	return nil
}
