package api

import (
	"context"
	"net"
	"net/http"
	"net/url"
	"regexp"
	"strings"
	"sync"

	"github.com/gorilla/mux"
)

type (
	// KeyVal -- appended to ReqInfo.Tags.
	KeyVal struct {
		Key string
		Val string
	}

	// ReqInfo stores the request info.
	ReqInfo struct {
		sync.RWMutex
		RemoteHost   string   // Client Host/IP
		Host         string   // Node Host/IP
		UserAgent    string   // User Agent
		DeploymentID string   // random generated s3-deployment-id
		RequestID    string   // x-amz-request-id
		API          string   // API name -- GetObject PutObject NewMultipartUpload etc.
		BucketName   string   // Bucket name
		ObjectName   string   // Object name
		URL          *url.URL // Request url
		tags         []KeyVal // Any additional info not accommodated by above fields
	}

	// ObjectRequest represents object request data.
	ObjectRequest struct {
		Bucket string
		Object string
		Method string
	}
)

// Key used for Get/SetReqInfo.
type contextKeyType string

const ctxRequestInfo = contextKeyType("NeoFS-S3-GW")

var (
	// De-facto standard header keys.
	xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
	xRealIP       = http.CanonicalHeaderKey("X-Real-IP")
)

var (
	// RFC7239 defines a new "Forwarded: " header designed to replace the
	// existing use of X-Forwarded-* headers.
	// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43.
	forwarded = http.CanonicalHeaderKey("Forwarded")
	// Allows for a sub-match of the first value after 'for=' to the next
	// comma, semi-colon or space. The match is case-insensitive.
	forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|, )]+)(.*)`)
)

// GetSourceIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239
// Forwarded headers (in that order), falls back to r.RemoteAddr when everything
// else fails.
func GetSourceIP(r *http.Request) string {
	var addr string

	if fwd := r.Header.Get(xForwardedFor); fwd != "" {
		// Only grabs the first (client) address. Note that '192.168.0.1,
		// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
		// the first one may represent forwarding proxies earlier in the chain.
		s := strings.Index(fwd, ", ")
		if s == -1 {
			s = len(fwd)
		}
		addr = fwd[:s]
	} else if fwd := r.Header.Get(xRealIP); fwd != "" {
		// X-Real-IP should only contain one IP address (the client making the
		// request).
		addr = fwd
	} else if fwd := r.Header.Get(forwarded); fwd != "" {
		// match should contain at least two elements if the protocol was
		// specified in the Forwarded header. The first element will always be
		// the 'for=' capture, which we ignore. In the case of multiple IP
		// addresses (for=8.8.8.8, 8.8.4.4, 172.16.1.20 is valid) we only
		// extract the first, which should be the client IP.
		if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 {
			// IPv6 addresses in Forwarded headers are quoted-strings. We strip
			// these quotes.
			addr = strings.Trim(match[1], `"`)
		}
	}

	if addr != "" {
		return addr
	}

	// Default to remote address if headers not set.
	addr, _, _ = net.SplitHostPort(r.RemoteAddr)
	return addr
}

func prepareContext(w http.ResponseWriter, r *http.Request) context.Context {
	vars := mux.Vars(r)
	bucket := vars["bucket"]
	object, err := url.PathUnescape(vars["object"])
	if err != nil {
		object = vars["object"]
	}
	prefix, err := url.QueryUnescape(vars["prefix"])
	if err != nil {
		prefix = vars["prefix"]
	}
	if prefix != "" {
		object = prefix
	}
	return SetReqInfo(r.Context(),
		// prepare request info
		NewReqInfo(w, r, ObjectRequest{
			Bucket: bucket,
			Object: object,
			Method: mux.CurrentRoute(r).GetName(),
		}))
}

// NewReqInfo returns new ReqInfo based on parameters.
func NewReqInfo(w http.ResponseWriter, r *http.Request, req ObjectRequest) *ReqInfo {
	return &ReqInfo{
		API:          req.Method,
		BucketName:   req.Bucket,
		ObjectName:   req.Object,
		UserAgent:    r.UserAgent(),
		RemoteHost:   GetSourceIP(r),
		RequestID:    GetRequestID(w),
		DeploymentID: deploymentID.String(),
		URL:          r.URL,
	}
}

// AppendTags -- appends key/val to ReqInfo.tags.
func (r *ReqInfo) AppendTags(key string, val string) *ReqInfo {
	if r == nil {
		return nil
	}
	r.Lock()
	defer r.Unlock()
	r.tags = append(r.tags, KeyVal{key, val})
	return r
}

// SetTags -- sets key/val to ReqInfo.tags.
func (r *ReqInfo) SetTags(key string, val string) *ReqInfo {
	if r == nil {
		return nil
	}
	r.Lock()
	defer r.Unlock()
	// Search for a tag key already existing in tags
	var updated bool
	for _, tag := range r.tags {
		if tag.Key == key {
			tag.Val = val
			updated = true
			break
		}
	}
	if !updated {
		// Append to the end of tags list
		r.tags = append(r.tags, KeyVal{key, val})
	}
	return r
}

// GetTags -- returns the user defined tags.
func (r *ReqInfo) GetTags() []KeyVal {
	if r == nil {
		return nil
	}
	r.RLock()
	defer r.RUnlock()
	return append([]KeyVal(nil), r.tags...)
}

// SetReqInfo sets ReqInfo in the context.
func SetReqInfo(ctx context.Context, req *ReqInfo) context.Context {
	if ctx == nil {
		return nil
	}
	return context.WithValue(ctx, ctxRequestInfo, req)
}

// GetReqInfo returns ReqInfo if set.
func GetReqInfo(ctx context.Context) *ReqInfo {
	if ctx == nil {
		return &ReqInfo{}
	} else if r, ok := ctx.Value(ctxRequestInfo).(*ReqInfo); ok {
		return r
	}
	return &ReqInfo{}
}