// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 httpserver

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"net"
	"net/http"
	"strconv"
	"sync"
	"time"

	"github.com/gin-gonic/gin"

	mhttp "github.com/milvus-io/milvus/internal/http"
	"github.com/milvus-io/milvus/internal/json"
	"github.com/milvus-io/milvus/pkg/v3/mlog"
	"github.com/milvus-io/milvus/pkg/v3/util/merr"
	"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
)

// BufferPool represents a pool of buffers.
type BufferPool struct {
	pool sync.Pool
}

// Get returns a buffer from the buffer pool.
// If the pool is empty, a new buffer is created and returned.
func (p *BufferPool) Get() *bytes.Buffer {
	buf := p.pool.Get()
	if buf == nil {
		return &bytes.Buffer{}
	}
	return buf.(*bytes.Buffer)
}

// Put adds a buffer back to the pool.
func (p *BufferPool) Put(buf *bytes.Buffer) {
	p.pool.Put(buf)
}

// Timeout struct
type Timeout struct {
	handler gin.HandlerFunc
}

const timeoutRecorderNoWritten = -1

type timeoutResponseRecorder struct {
	body        *bytes.Buffer
	headers     http.Header
	mu          sync.Mutex
	closed      bool
	status      int
	size        int
	closeNotify chan bool
}

func newTimeoutResponseRecorder(buf *bytes.Buffer) *timeoutResponseRecorder {
	return &timeoutResponseRecorder{
		body:        buf,
		headers:     make(http.Header),
		status:      http.StatusOK,
		size:        timeoutRecorderNoWritten,
		closeNotify: make(chan bool),
	}
}

func (w *timeoutResponseRecorder) Header() http.Header {
	return w.headers
}

func (w *timeoutResponseRecorder) Write(data []byte) (int, error) {
	w.mu.Lock()
	defer w.mu.Unlock()
	if w.closed || w.body == nil {
		return 0, merr.WrapErrServiceInternalMsg("response writer closed")
	}
	if !w.written() {
		w.size = 0
	}
	n, err := w.body.Write(data)
	w.size += n
	return n, err
}

func (w *timeoutResponseRecorder) WriteString(s string) (int, error) {
	w.mu.Lock()
	defer w.mu.Unlock()
	if w.closed || w.body == nil {
		return 0, merr.WrapErrServiceInternalMsg("response writer closed")
	}
	if !w.written() {
		w.size = 0
	}
	n, err := w.body.WriteString(s)
	w.size += n
	return n, err
}

func (w *timeoutResponseRecorder) WriteHeader(code int) {
	if code == -1 {
		return
	}
	checkWriteHeaderCode(code)

	w.mu.Lock()
	defer w.mu.Unlock()
	if w.closed || w.written() {
		return
	}
	w.status = code
}

func (w *timeoutResponseRecorder) WriteHeaderNow() {
	w.mu.Lock()
	defer w.mu.Unlock()
	if w.closed || w.written() {
		return
	}
	w.size = 0
}

func (w *timeoutResponseRecorder) Status() int {
	w.mu.Lock()
	defer w.mu.Unlock()
	return w.status
}

func (w *timeoutResponseRecorder) Size() int {
	w.mu.Lock()
	defer w.mu.Unlock()
	return w.size
}

func (w *timeoutResponseRecorder) Written() bool {
	w.mu.Lock()
	defer w.mu.Unlock()
	return w.written()
}

func (w *timeoutResponseRecorder) Flush() {}

func (w *timeoutResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	return nil, nil, merr.WrapErrServiceInternalMsg("response writer does not support hijack")
}

func (w *timeoutResponseRecorder) CloseNotify() <-chan bool {
	return w.closeNotify
}

func (w *timeoutResponseRecorder) Pusher() http.Pusher {
	return nil
}

func (w *timeoutResponseRecorder) CommitTo(realWriter gin.ResponseWriter) error {
	w.mu.Lock()
	defer w.mu.Unlock()
	if w.closed || w.body == nil {
		return merr.WrapErrServiceInternalMsg("response writer closed")
	}

	dst := realWriter.Header()
	for k, vv := range w.headers {
		dst[k] = append([]string(nil), vv...)
	}
	realWriter.WriteHeader(w.status)
	if w.body.Len() == 0 {
		if w.written() || w.status != http.StatusOK {
			realWriter.WriteHeaderNow()
		}
		return nil
	}
	_, err := realWriter.Write(w.body.Bytes())
	return err
}

func (w *timeoutResponseRecorder) Close() {
	w.mu.Lock()
	defer w.mu.Unlock()
	if w.body != nil {
		w.body.Reset()
		w.body = nil
	}
	w.closed = true
}

func (w *timeoutResponseRecorder) CloseForTimeout() {
	w.Close()
}

func (w *timeoutResponseRecorder) written() bool {
	return w.size != timeoutRecorderNoWritten
}

func checkWriteHeaderCode(code int) {
	if code < 100 || code > 999 {
		panic(fmt.Sprintf("invalid http status code: %d", code))
	}
}

var timeoutContextKeysToPropagate = []string{
	HTTPReturnCode,
	HTTPReturnMessage,
	ContextRequest,
	ContextResponse,
	"traceID",
}

func propagateTimeoutContextKeys(dst *gin.Context, src *gin.Context) {
	for _, key := range timeoutContextKeysToPropagate {
		if value, ok := src.Get(key); ok {
			dst.Set(key, value)
		}
	}
}

func timeoutMiddleware(handler gin.HandlerFunc) gin.HandlerFunc {
	timeoutHandler := &Timeout{
		handler: handler,
	}
	bufPool := &BufferPool{}
	return func(gCtx *gin.Context) {
		timeout := paramtable.Get().HTTPCfg.RequestTimeoutMs.GetAsDuration(time.Millisecond)
		requestTimeout := gCtx.Request.Header.Get(mhttp.HTTPHeaderRequestTimeout)
		if requestTimeout != "" {
			timeoutSecond, err := strconv.ParseInt(requestTimeout, 10, 64)
			if err != nil {
				HTTPAbortReturn(gCtx, http.StatusOK, gin.H{
					mhttp.HTTPReturnCode: merr.Code(merr.ErrParameterInvalid),
					mhttp.HTTPReturnMessage: merr.WrapErrParameterInvalidMsg(
						"%s parse failed, err: %s",
						mhttp.HTTPHeaderRequestTimeout,
						err.Error(),
					).Error(),
				})
				return
			}
			timeout = time.Duration(timeoutSecond) * time.Second
		}
		topCtx, cancel := context.WithTimeout(gCtx.Request.Context(), timeout)
		defer cancel()
		req := gCtx.Request.WithContext(topCtx)
		gCtx.Request = req

		finish := make(chan struct{}, 1)
		panicChan := make(chan interface{}, 1)

		realWriter := gCtx.Writer
		buffer := bufPool.Get()
		buffer.Reset()
		recorder := newTimeoutResponseRecorder(buffer)
		handlerCtx := gCtx.Copy()
		handlerCtx.Request = req
		handlerCtx.Writer = recorder

		go func() {
			defer func() {
				if p := recover(); p != nil {
					panicChan <- p
				}
			}()
			timeoutHandler.handler(handlerCtx)
			finish <- struct{}{}
		}()

		timer := time.NewTimer(timeout)
		defer timer.Stop()

		select {
		case p := <-panicChan:
			recorder.Close()
			bufPool.Put(buffer)
			gCtx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{mhttp.HTTPReturnCode: http.StatusInternalServerError})
			panic(p)

		case <-finish:
			propagateTimeoutContextKeys(gCtx, handlerCtx)
			if handlerCtx.IsAborted() {
				gCtx.Abort()
			}
			gCtx.Next()
			if err := recorder.CommitTo(realWriter); err != nil {
				mlog.Warn(context.TODO(), "failed to write response body", mlog.Err(err))
				recorder.Close()
				bufPool.Put(buffer)
				return
			}
			recorder.Close()
			bufPool.Put(buffer)

		case <-timer.C:
			cancel()
			gCtx.Abort()
			gCtx.Set(HTTPReturnCode, merr.TimeoutCode)
			gCtx.Set(HTTPReturnMessage, "request timeout")
			recorder.CloseForTimeout()
			bufPool.Put(buffer)

			realWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
			if traceID, ok := getTraceID(gCtx); ok {
				setTraceIDHeaderTo(realWriter.Header(), traceID)
			}
			realWriter.WriteHeader(http.StatusRequestTimeout)
			body, _ := json.Marshal(gin.H{HTTPReturnCode: merr.TimeoutCode, HTTPReturnMessage: "request timeout"})
			realWriter.Write(body)
		}
	}
}
