package s3err

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"os"
	"strings"
	"sync/atomic"
	"time"

	"github.com/fluent/fluent-logger-golang/fluent"
	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	"github.com/seaweedfs/seaweedfs/weed/util/request_id"
)

type AccessLogExtend struct {
	AccessLog
	AccessLogHTTP
}

type AccessLog struct {
	Bucket           string `msg:"bucket" json:"bucket"`                         // awsexamplebucket1
	Time             int64  `msg:"time" json:"time"`                             // [06/Feb/2019:00:00:38 +0000]
	RemoteIP         string `msg:"remote_ip" json:"remote_ip,omitempty"`         // 192.0.2.3
	Requester        string `msg:"requester" json:"requester,omitempty"`         // IAM user id
	RequesterArn     string `msg:"requester_arn" json:"requester_arn,omitempty"` // arn:aws:sts::0:assumed-role/Role/session
	RequestID        string `msg:"request_id" json:"request_id,omitempty"`       // 3E57427F33A59F07
	Operation        string `msg:"operation" json:"operation,omitempty"`         // REST.HTTP_method.resource_type REST.PUT.OBJECT
	Key              string `msg:"key" json:"key,omitempty"`                     // /photos/2019/08/puppy.jpg
	ErrorCode        string `msg:"error_code" json:"error_code,omitempty"`
	HostId           string `msg:"host_id" json:"host_id,omitempty"`
	HostHeader       string `msg:"host_header" json:"host_header,omitempty"` // s3.us-west-2.amazonaws.com
	UserAgent        string `msg:"user_agent" json:"user_agent,omitempty"`
	HTTPStatus       int    `msg:"status" json:"status,omitempty"`
	SignatureVersion string `msg:"signature_version" json:"signature_version,omitempty"`
}

type AccessLogHTTP struct {
	RequestURI         string `json:"request_uri,omitempty"` // "GET /awsexamplebucket1/photos/2019/08/puppy.jpg?x-foo=bar HTTP/1.1"
	BytesSent          string `json:"bytes_sent,omitempty"`
	ObjectSize         string `json:"object_size,omitempty"`
	TotalTime          int    `json:"total_time,omitempty"`
	TurnAroundTime     int    `json:"turn_around_time,omitempty"`
	Referer            string `json:"Referer,omitempty"`
	VersionId          string `json:"version_id,omitempty"`
	CipherSuite        string `json:"cipher_suite,omitempty"`
	AuthenticationType string `json:"auth_type,omitempty"`
	TLSVersion         string `json:"TLS_version,omitempty"`
}

const tag = "s3.access"

var (
	Logger      *fluent.Fluent
	hostname    = os.Getenv("HOSTNAME")
	environment = os.Getenv("ENVIRONMENT")
)

func InitAuditLog(config string) {
	configContent, readErr := os.ReadFile(config)
	if readErr != nil {
		glog.Errorf("fail to read fluent config %s : %v", config, readErr)
		return
	}
	fluentConfig := &fluent.Config{}
	if err := json.Unmarshal(configContent, fluentConfig); err != nil {
		glog.Errorf("fail to parse fluent config %s : %v", string(configContent), err)
		return
	}
	if len(fluentConfig.TagPrefix) == 0 && len(environment) > 0 {
		fluentConfig.TagPrefix = environment
	}
	fluentConfig.Async = true
	fluentConfig.AsyncResultCallback = func(data []byte, err error) {
		if err != nil {
			glog.Warning("Error while posting log: ", err)
		}
	}
	var err error
	Logger, err = fluent.New(*fluentConfig)
	if err != nil {
		glog.Errorf("fail to load fluent config: %v", err)
	}
}

// getRemoteIP returns the client IP for the audit log, honoring forwarding
// headers set by reverse proxies. Preference order: X-Forwarded-For (first
// non-empty entry), X-Real-IP, then r.RemoteAddr. Headers are trusted as-is;
// operators who expose the S3 endpoint directly to untrusted networks should
// strip these headers at the proxy boundary so clients cannot spoof them.
func getRemoteIP(r *http.Request) string {
	if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
		for _, entry := range strings.Split(forwardedFor, ",") {
			if ip := strings.TrimSpace(entry); ip != "" {
				return ip
			}
		}
	}
	if realIP := strings.TrimSpace(r.Header.Get("X-Real-IP")); realIP != "" {
		return realIP
	}
	if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
		return host
	}
	return r.RemoteAddr
}

func getREST(httpMetod string, resourceType string) string {
	return fmt.Sprintf("REST.%s.%s", httpMetod, resourceType)
}

func getResourceType(object string, query_key string, method string) (string, bool) {
	if object == "/" {
		switch query_key {
		case "delete":
			return "BATCH.DELETE.OBJECT", true
		case "tagging":
			return getREST(method, "OBJECTTAGGING"), true
		case "lifecycle":
			return getREST(method, "LIFECYCLECONFIGURATION"), true
		case "acl":
			return getREST(method, "ACCESSCONTROLPOLICY"), true
		case "policy":
			return getREST(method, "BUCKETPOLICY"), true
		default:
			return getREST(method, "BUCKET"), false
		}
	} else {
		switch query_key {
		case "tagging":
			return getREST(method, "OBJECTTAGGING"), true
		default:
			return getREST(method, "OBJECT"), false
		}
	}
}

func getOperation(object string, r *http.Request) string {
	queries := r.URL.Query()
	var operation string
	var queryFound bool
	for key, _ := range queries {
		operation, queryFound = getResourceType(object, key, r.Method)
		if queryFound {
			return operation
		}
	}
	if len(queries) == 0 {
		operation, _ = getResourceType(object, "", r.Method)
	}
	return operation
}

func GetAccessLog(r *http.Request, HTTPStatusCode int, s3errCode ErrorCode) *AccessLog {
	bucket, key := s3_constants.GetBucketAndObject(r)
	var errorCode string
	if s3errCode != ErrNone {
		errorCode = GetAPIError(s3errCode).Code
	}
	remoteIP := getRemoteIP(r)
	hostHeader := r.Header.Get("X-Forwarded-Host")
	if len(hostHeader) == 0 {
		hostHeader = r.Host
	}
	return &AccessLog{
		HostHeader:       hostHeader,
		RequestID:        request_id.GetFromRequest(r),
		RemoteIP:         remoteIP,
		Requester:        s3_constants.GetIdentityNameFromContext(r), // Get from context, not header (secure)
		RequesterArn:     s3_constants.GetPrincipalArnFromContext(r),
		SignatureVersion: r.Header.Get(s3_constants.AmzAuthType),
		UserAgent:        r.Header.Get("user-agent"),
		HostId:           hostname,
		Bucket:           bucket,
		HTTPStatus:       HTTPStatusCode,
		Time:             time.Now().Unix(),
		Key:              key,
		Operation:        getOperation(key, r),
		ErrorCode:        errorCode,
	}
}

func PostLog(r *http.Request, HTTPStatusCode int, errorCode ErrorCode) {
	if r == nil {
		return
	}
	// Mark before the Logger nil-check so the middleware fallback in track()
	// still sees that audit was handled by the caller in deployments that
	// haven't configured fluent — keeps the flag behavior consistent and
	// makes wiring testable without standing up a fluent server.
	markAuditLogged(r)
	if Logger == nil {
		return
	}
	if err := Logger.Post(tag, *GetAccessLog(r, HTTPStatusCode, errorCode)); err != nil {
		glog.Warning("Error while posting log: ", err)
	}
}

func PostAccessLog(log AccessLog) {
	if Logger == nil || len(log.Key) == 0 {
		return
	}
	if err := Logger.Post(tag, log); err != nil {
		glog.Warning("Error while posting log: ", err)
	}
}

// auditLogCtxKey identifies the per-request flag used to detect whether an
// audit entry has already been emitted, so middleware can supply a fallback
// log for handlers that don't call PostLog themselves without double-logging
// the ones that do.
type auditLogCtxKey struct{}

// EnsureAuditTracking attaches an audit-tracking flag to the request context
// if one is not already present. Safe to call when no fluent logger is
// configured; the flag is harmless in that case.
func EnsureAuditTracking(r *http.Request) *http.Request {
	if r == nil {
		return nil
	}
	if v, ok := r.Context().Value(auditLogCtxKey{}).(*atomic.Bool); ok && v != nil {
		return r
	}
	flag := new(atomic.Bool)
	return r.WithContext(context.WithValue(r.Context(), auditLogCtxKey{}, flag))
}

// MarkAuditLogged signals that an audit entry has already been emitted for r.
// Callers that emit logs via paths other than PostLog (e.g. PostAccessLog in
// batch delete) should invoke this so the middleware fallback skips r.
func MarkAuditLogged(r *http.Request) {
	markAuditLogged(r)
}

func markAuditLogged(r *http.Request) {
	if r == nil {
		return
	}
	if v, ok := r.Context().Value(auditLogCtxKey{}).(*atomic.Bool); ok && v != nil {
		v.Store(true)
	}
}

// AuditAlreadyLogged reports whether PostLog (or MarkAuditLogged) has run for r.
func AuditAlreadyLogged(r *http.Request) bool {
	if r == nil {
		return false
	}
	if v, ok := r.Context().Value(auditLogCtxKey{}).(*atomic.Bool); ok && v != nil {
		return v.Load()
	}
	return false
}
