package alicloud

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"

	awssdk "github.com/aws/aws-sdk-go-v2/aws"
	v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
	"github.com/stephnangue/warden/credential"
	"github.com/stephnangue/warden/logger"
	"github.com/stephnangue/warden/logical"
	"github.com/stephnangue/warden/provider/sdk/sigv4"
)

// alicloudCredentialInfo holds extracted Alicloud credentials from the request.
type alicloudCredentialInfo struct {
	accessKeyID     string
	accessKeySecret string
	securityToken   string
}

// headersToStrip are client/proxy headers removed before re-signing.
var headersToStrip = []string{
	// Client-supplied ACS auth artifacts (will be regenerated by SignACS3)
	HeaderAuthorization,
	HeaderACSDate,
	HeaderACSContentSHA256,
	HeaderACSSignatureNonce,
	HeaderACSSecurityToken,
	// Warden-specific
	"X-Warden-Token",
	"X-Warden-Role",
	"X-Warden-Provider",
	"X-Warden-On-Behalf-Of",
	// Hop-by-hop
	"Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization",
	"Te", "Trailer", "Trailers", "Transfer-Encoding", "Upgrade",
	// Proxy
	"X-Forwarded-For", "X-Forwarded-Host", "X-Forwarded-Proto",
	"X-Forwarded-Port", "X-Real-Ip", "Forwarded",
}

// handleGateway dispatches the request to the correct handler based on the
// client's Authorization header:
//   - ACS3-HMAC-SHA256: Alicloud OpenAPI (REST) — verify + re-sign with ACS3
//   - AWS4-HMAC-SHA256: OSS S3-compatible — verify + re-sign with SigV4
func (b *alicloudBackend) handleGateway(ctx context.Context, req *logical.Request) {
	// Set URL scheme and host (lost during HTTP parsing)
	if req.HTTPRequest.URL.Scheme == "" {
		if req.HTTPRequest.TLS != nil {
			req.HTTPRequest.URL.Scheme = "https"
		} else {
			req.HTTPRequest.URL.Scheme = "http"
		}
	}
	if req.HTTPRequest.URL.Host == "" {
		req.HTTPRequest.URL.Host = req.HTTPRequest.Host
	}

	switch {
	case IsACS3Request(req.HTTPRequest):
		b.handleAPIRequest(ctx, req)
	case sigv4.IsSigV4Request(req.HTTPRequest):
		b.handleOSSRequest(ctx, req)
	default:
		http.Error(req.ResponseWriter,
			"Unsupported request: expected ACS3-HMAC-SHA256 or AWS4-HMAC-SHA256 Authorization header",
			http.StatusBadRequest)
	}
}

// handleAPIRequest handles ACS3-signed Alicloud OpenAPI requests.
// It verifies the incoming ACS3 signature (using the client's implicit-auth
// credentials), re-signs with real Alicloud credentials, and forwards.
func (b *alicloudBackend) handleAPIRequest(ctx context.Context, req *logical.Request) {
	maxBody := b.MaxBodySize
	if maxBody <= 0 {
		maxBody = 10 << 20
	}

	// Step 1: Resolve the real Alicloud target from the incoming Host.
	// Fail fast before reading the body — this is also our SSRF guard.
	targetHost, err := resolveTargetHost(req.HTTPRequest.Host, b.getProxyDomains())
	if err != nil {
		b.Logger.Warn("host resolution failed", logger.Err(err))
		http.Error(req.ResponseWriter, err.Error(), http.StatusBadRequest)
		return
	}

	// Step 2: Read and buffer body
	bodyBytes, err := sigv4.ReadRequestBody(req.HTTPRequest, maxBody)
	if err != nil {
		http.Error(req.ResponseWriter, "Failed to read request body", http.StatusBadRequest)
		return
	}
	if bodyBytes == nil {
		bodyBytes = []byte{}
	}

	// Apply timeout for verification + forwarding
	if b.Timeout > 0 {
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, b.Timeout)
		defer cancel()
		req.HTTPRequest = req.HTTPRequest.WithContext(ctx)
	}

	// Step 3: Reconstruct client-side signing credentials and verify the
	// signature against the *original* incoming Host (what the client signed).
	incomingAccessKeyID := ExtractACS3AccessKeyID(req.HTTPRequest.Header.Get(HeaderAuthorization))
	incomingSecurityToken := req.HTTPRequest.Header.Get(HeaderACSSecurityToken)

	var verifySecret string
	if strings.HasPrefix(incomingSecurityToken, "eyJ") {
		verifySecret = incomingSecurityToken
	} else {
		verifySecret = incomingAccessKeyID
	}

	valid, err := VerifyACS3(req.HTTPRequest, verifySecret, bodyBytes)
	if err != nil {
		b.Logger.Warn("ACS3 signature verification failed", logger.Err(err))
		http.Error(req.ResponseWriter, "Signature verification failed", http.StatusForbidden)
		return
	}
	if !valid {
		b.Logger.Warn("ACS3 signature does not match")
		http.Error(req.ResponseWriter, "Signature does not match", http.StatusForbidden)
		return
	}

	// Step 4: Get real Alicloud credentials from req.Credential
	realCreds, err := b.getCredentials(req)
	if err != nil {
		b.Logger.Warn("Failed to extract Alicloud credentials", logger.Err(err))
		http.Error(req.ResponseWriter, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Step 5: Rewrite req.Host to the resolved Alicloud target so the
	// re-signature is computed against what Alicloud expects. URL.Host is
	// left untouched — forward() falls back to req.Host when it's unset in
	// production, and tests can override URL.Host to redirect to a test
	// server without affecting the signed host.
	req.HTTPRequest.Host = targetHost

	// Step 6: Strip client-supplied headers that will be regenerated
	for _, h := range headersToStrip {
		req.HTTPRequest.Header.Del(h)
	}

	// Step 7: Re-sign with real credentials against the target host
	if err := SignACS3(req.HTTPRequest, realCreds.accessKeyID, realCreds.accessKeySecret, realCreds.securityToken, bodyBytes); err != nil {
		b.Logger.Error("Failed to re-sign request", logger.Err(err))
		http.Error(req.ResponseWriter, "Internal server error", http.StatusInternalServerError)
		return
	}

	b.Logger.Trace("Proxying Alicloud request",
		logger.String("method", req.HTTPRequest.Method),
		logger.String("target_host", targetHost),
		logger.String("path", req.HTTPRequest.URL.Path),
		logger.String("request_id", req.RequestID),
	)

	// Step 8: Forward directly via transport (bypass ReverseProxy, which would
	// rewrite headers and break the signature).
	b.forward(req, bodyBytes)
}

// forward sends the signed request to the target Alicloud endpoint and
// streams the response back to the client.
func (b *alicloudBackend) forward(req *logical.Request, body []byte) {
	r := req.HTTPRequest
	if r.URL.Scheme == "" {
		r.URL.Scheme = "https"
	}
	if r.URL.Host == "" {
		r.URL.Host = r.Host
	}
	r.RequestURI = ""
	if len(body) > 0 {
		r.Body = io.NopCloser(bytes.NewReader(body))
		r.ContentLength = int64(len(body))
	} else {
		r.Body = nil
		r.ContentLength = 0
	}

	transport := b.Proxy.Transport
	if transport == nil {
		transport = http.DefaultTransport
	}

	resp, err := transport.RoundTrip(r)
	if err != nil {
		b.Logger.Error("forward failed", logger.Err(err))
		http.Error(req.ResponseWriter, "Bad Gateway", http.StatusBadGateway)
		return
	}
	defer resp.Body.Close()

	for k, vv := range resp.Header {
		for _, v := range vv {
			req.ResponseWriter.Header().Add(k, v)
		}
	}
	req.ResponseWriter.WriteHeader(resp.StatusCode)

	if flusher, ok := req.ResponseWriter.(http.Flusher); ok {
		buf := make([]byte, 32*1024)
		for {
			n, readErr := resp.Body.Read(buf)
			if n > 0 {
				req.ResponseWriter.Write(buf[:n])
				flusher.Flush()
			}
			if readErr != nil {
				break
			}
		}
	} else {
		io.Copy(req.ResponseWriter, resp.Body)
	}
}

// handleOSSRequest handles AWS SigV4-signed OSS (S3-compatible) requests.
// It verifies the incoming SigV4 signature (using the client's implicit-auth
// credentials), re-signs with real Alicloud OSS credentials, and forwards to
// the target OSS endpoint (oss-{region}.aliyuncs.com).
func (b *alicloudBackend) handleOSSRequest(ctx context.Context, req *logical.Request) {
	maxBody := b.MaxBodySize
	if maxBody <= 0 {
		maxBody = 10 << 20
	}

	// Step 0: Validate the incoming Host. For OSS the eventual target is
	// derived from the SigV4 region scope rather than the Host, but running
	// resolveTargetHost rejects obviously-forged hosts before we do any
	// signature work and enforces the same SSRF guard as the ACS3 path.
	if _, err := resolveTargetHost(req.HTTPRequest.Host, b.getProxyDomains()); err != nil {
		b.Logger.Warn("host resolution failed (OSS)", logger.Err(err))
		http.Error(req.ResponseWriter, err.Error(), http.StatusBadRequest)
		return
	}

	// Step 1: Read and buffer body
	bodyBytes, err := sigv4.ReadRequestBody(req.HTTPRequest, maxBody)
	if err != nil {
		http.Error(req.ResponseWriter, "Failed to read request body", http.StatusBadRequest)
		return
	}
	if bodyBytes == nil {
		bodyBytes = []byte{}
	}

	// Apply timeout for verification + forwarding
	if b.Timeout > 0 {
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, b.Timeout)
		defer cancel()
		req.HTTPRequest = req.HTTPRequest.WithContext(ctx)
	}

	// Step 2: Extract service, region, and access key from Authorization header.
	// For OSS, service is typically "s3" (S3-compatible mode) and region is an
	// OSS region like "oss-cn-hangzhou" or plain "cn-hangzhou".
	service, region, _, err := sigv4.ExtractFromAuthHeader(req.HTTPRequest.Header.Get(HeaderAuthorization))
	if err != nil {
		http.Error(req.ResponseWriter, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Step 3: Reconstruct client signing credentials.
	// JWT transparent: JWT in X-Amz-Security-Token, also used as SecretAccessKey.
	// Cert transparent: role name as both access_key_id and secret_access_key.
	accessKeyID := sigv4.ExtractAccessKeyID(req.HTTPRequest.Header.Get(HeaderAuthorization))
	securityToken := req.HTTPRequest.Header.Get("X-Amz-Security-Token")

	var verifyCreds awssdk.Credentials
	if strings.HasPrefix(securityToken, "eyJ") {
		verifyCreds = awssdk.Credentials{
			AccessKeyID:     accessKeyID,
			SecretAccessKey: securityToken,
			SessionToken:    securityToken,
		}
	} else {
		verifyCreds = awssdk.Credentials{
			AccessKeyID:     accessKeyID,
			SecretAccessKey: accessKeyID,
		}
	}

	// Step 4: Verify incoming signature.
	s3SignerOpts := []func(*v4.SignerOptions){
		func(o *v4.SignerOptions) { o.DisableURIPathEscaping = true },
	}
	valid, err := sigv4.VerifyIncomingSignature(b.Logger, s3SignerOpts, req.HTTPRequest, bodyBytes, verifyCreds, service, region)
	if err != nil {
		b.Logger.Warn("OSS signature verification failed", logger.Err(err))
		http.Error(req.ResponseWriter, "Signature verification failed", http.StatusForbidden)
		return
	}
	if !valid {
		b.Logger.Warn("OSS signature does not match")
		http.Error(req.ResponseWriter, "Signature does not match", http.StatusForbidden)
		return
	}

	// Step 5: Get real Alicloud credentials
	realCreds, err := b.getCredentials(req)
	if err != nil {
		b.Logger.Warn("Failed to extract Alicloud credentials", logger.Err(err))
		http.Error(req.ResponseWriter, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Step 6: Build target OSS endpoint. The region from the SigV4 credential
	// scope may be bare ("cn-hangzhou") or prefixed ("oss-cn-hangzhou"); normalize.
	targetRegion := strings.TrimPrefix(region, "oss-")
	targetHost := ossEndpoint(targetRegion, req.HTTPRequest.Host)
	req.HTTPRequest.URL.Scheme = "https"
	req.HTTPRequest.URL.Host = targetHost
	req.HTTPRequest.Host = targetHost
	req.HTTPRequest.RequestURI = ""

	// Step 7: Strip client security token and normalize for re-signing
	req.HTTPRequest.Header.Del("X-Amz-Security-Token")
	bodyBytes = sigv4.NormalizeRequest(b.Logger, req.HTTPRequest, bodyBytes)

	// Step 8: Re-sign with real Alicloud OSS credentials
	realAWSCreds := awssdk.Credentials{
		AccessKeyID:     realCreds.accessKeyID,
		SecretAccessKey: realCreds.accessKeySecret,
		SessionToken:    realCreds.securityToken,
	}
	if err := sigv4.ResignRequest(ctx, b.s3Signer, req.HTTPRequest, realAWSCreds, "s3", targetRegion, bodyBytes); err != nil {
		b.Logger.Error("Failed to re-sign OSS request", logger.Err(err))
		http.Error(req.ResponseWriter, "Internal server error", http.StatusInternalServerError)
		return
	}

	b.Logger.Trace("Proxying Alicloud OSS request",
		logger.String("method", req.HTTPRequest.Method),
		logger.String("host", targetHost),
		logger.String("path", req.HTTPRequest.URL.Path),
		logger.String("region", targetRegion),
		logger.String("request_id", req.RequestID),
	)

	// Step 9: Forward directly
	transport := b.Proxy.Transport
	if transport == nil {
		transport = http.DefaultTransport
	}
	sigv4.ForwardDirect(b.Logger, req.ResponseWriter, req.HTTPRequest, bodyBytes, transport)
}

// ossEndpoint returns the OSS endpoint host for a region. If incomingHost looks
// like a virtual-hosted-style bucket request ({bucket}.oss-{region}.aliyuncs.com),
// that form is preserved. Otherwise returns the path-style regional endpoint.
func ossEndpoint(region, incomingHost string) string {
	regional := fmt.Sprintf("oss-%s.aliyuncs.com", region)
	// Preserve virtual-hosted bucket style if the client used it
	if incomingHost != "" && strings.HasSuffix(incomingHost, ".aliyuncs.com") && strings.Contains(incomingHost, ".oss-") {
		idx := strings.Index(incomingHost, ".oss-")
		bucket := incomingHost[:idx]
		return bucket + "." + regional
	}
	return regional
}

// getCredentials extracts Alicloud credentials from the request's Warden credential.
func (b *alicloudBackend) getCredentials(req *logical.Request) (alicloudCredentialInfo, error) {
	if req.Credential == nil {
		return alicloudCredentialInfo{}, fmt.Errorf("no credential available")
	}
	if req.Credential.Type != credential.TypeAlicloudKeys {
		return alicloudCredentialInfo{}, fmt.Errorf("unsupported credential type: %s", req.Credential.Type)
	}

	ak := req.Credential.Data["access_key_id"]
	sk := req.Credential.Data["access_key_secret"]
	if ak == "" || sk == "" {
		return alicloudCredentialInfo{}, fmt.Errorf("credential missing access_key_id or access_key_secret")
	}

	return alicloudCredentialInfo{
		accessKeyID:     ak,
		accessKeySecret: sk,
		securityToken:   req.Credential.Data["security_token"],
	}, nil
}
