package clients

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"time"
)

const (
	volcesAssetDefaultEndpoint = "https://ark.cn-beijing.volcengineapi.com"
	volcesAssetRegion          = "cn-beijing"
	volcesAssetService         = "ark"
	volcesAssetVersion         = "2024-01-01"
)

type VolcesAssetClient struct {
	HTTPClient *http.Client
	Now        func() time.Time
}

type VolcesAssetCredentials struct {
	AccessKey string
	SecretKey string
	Endpoint  string
}

type VolcesAssetResult struct {
	ID          string         `json:"Id"`
	Name        string         `json:"Name,omitempty"`
	URL         string         `json:"URL,omitempty"`
	AssetType   string         `json:"AssetType,omitempty"`
	GroupID     string         `json:"GroupId,omitempty"`
	Status      string         `json:"Status,omitempty"`
	Error       map[string]any `json:"Error,omitempty"`
	ProjectName string         `json:"ProjectName,omitempty"`
	CreateTime  string         `json:"CreateTime,omitempty"`
	UpdateTime  string         `json:"UpdateTime,omitempty"`
}

func (c VolcesAssetClient) CreateAsset(ctx context.Context, credentials VolcesAssetCredentials, body map[string]any) (VolcesAssetResult, string, error) {
	var result struct {
		ID string `json:"Id"`
	}
	requestID, err := c.call(ctx, credentials, "CreateAsset", body, &result)
	return VolcesAssetResult{ID: result.ID}, requestID, err
}

func (c VolcesAssetClient) GetAsset(ctx context.Context, credentials VolcesAssetCredentials, body map[string]any) (VolcesAssetResult, string, error) {
	var result VolcesAssetResult
	requestID, err := c.call(ctx, credentials, "GetAsset", body, &result)
	return result, requestID, err
}

func (c VolcesAssetClient) call(ctx context.Context, credentials VolcesAssetCredentials, action string, body map[string]any, target any) (string, error) {
	accessKey := strings.TrimSpace(credentials.AccessKey)
	secretKey := strings.TrimSpace(credentials.SecretKey)
	if accessKey == "" || secretKey == "" {
		return "", &ClientError{Code: "missing_credentials", Message: "volces portrait asset accessKey and secretKey are required", Retryable: false}
	}
	endpoint := strings.TrimRight(strings.TrimSpace(credentials.Endpoint), "/")
	if endpoint == "" {
		endpoint = volcesAssetDefaultEndpoint
	}
	baseURL, err := url.Parse(endpoint)
	if err != nil || baseURL.Scheme == "" || baseURL.Host == "" {
		return "", &ClientError{Code: "invalid_configuration", Message: "invalid volces portrait asset endpoint", Retryable: false}
	}
	bodyJSON, err := json.Marshal(body)
	if err != nil {
		return "", fmt.Errorf("marshal volces asset request: %w", err)
	}
	now := time.Now().UTC()
	if c.Now != nil {
		now = c.Now().UTC()
	}
	xDate := now.Format("20060102T150405Z")
	contentSHA := sha256HexBytes(bodyJSON)
	requestURL := *baseURL
	requestURL.Path = "/"
	requestURL.RawPath = ""
	requestURL.RawQuery = canonicalVolcesAssetQuery(map[string]string{"Action": action, "Version": volcesAssetVersion})
	headers := map[string]string{
		"content-type":     "application/json",
		"host":             baseURL.Host,
		"x-content-sha256": contentSHA,
		"x-date":           xDate,
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(bodyJSON))
	if err != nil {
		return "", err
	}
	req.Host = baseURL.Host
	req.Header.Set("Content-Type", headers["content-type"])
	req.Header.Set("X-Content-Sha256", headers["x-content-sha256"])
	req.Header.Set("X-Date", headers["x-date"])
	req.Header.Set("Authorization", volcesAssetAuthorization(accessKey, secretKey, http.MethodPost, "/", requestURL.RawQuery, headers, contentSHA, xDate))

	response, err := httpClient(nil, c.HTTPClient).Do(req)
	if err != nil {
		return "", transportClientError(err)
	}
	defer response.Body.Close()
	var envelope struct {
		ResponseMetadata struct {
			RequestID string `json:"RequestId"`
			Error     struct {
				Code    string `json:"Code"`
				Message string `json:"Message"`
			} `json:"Error"`
		} `json:"ResponseMetadata"`
		Result json.RawMessage `json:"Result"`
	}
	if err := json.NewDecoder(response.Body).Decode(&envelope); err != nil {
		return requestIDFromHTTPResponse(response), &ClientError{Code: "invalid_response", Message: "decode volces portrait asset response: " + err.Error(), Retryable: HTTPRetryable(response.StatusCode), StatusCode: response.StatusCode}
	}
	requestID := firstNonEmpty(requestIDFromHTTPResponse(response), envelope.ResponseMetadata.RequestID)
	if envelope.ResponseMetadata.Error.Code != "" || response.StatusCode >= http.StatusBadRequest {
		message := strings.TrimSpace(envelope.ResponseMetadata.Error.Message)
		if message == "" {
			message = strings.TrimSpace(envelope.ResponseMetadata.Error.Code)
		}
		if message == "" {
			message = fmt.Sprintf("volces %s failed with status %d", action, response.StatusCode)
		}
		return requestID, &ClientError{Code: firstNonEmpty(envelope.ResponseMetadata.Error.Code, "volces_asset_error"), Message: message, RequestID: requestID, StatusCode: response.StatusCode, Retryable: HTTPRetryable(response.StatusCode)}
	}
	if len(envelope.Result) == 0 || string(envelope.Result) == "null" {
		return requestID, &ClientError{Code: "invalid_response", Message: "volces " + action + " returned empty result", RequestID: requestID, Retryable: false}
	}
	if err := json.Unmarshal(envelope.Result, target); err != nil {
		return requestID, &ClientError{Code: "invalid_response", Message: "decode volces " + action + " result: " + err.Error(), RequestID: requestID, Retryable: false}
	}
	return requestID, nil
}

func volcesAssetAuthorization(accessKey string, secretKey string, method string, path string, canonicalQuery string, headers map[string]string, bodySHA string, xDate string) string {
	signedHeaders := []string{"content-type", "host", "x-content-sha256", "x-date"}
	canonicalHeaderLines := make([]string, 0, len(signedHeaders))
	for _, key := range signedHeaders {
		canonicalHeaderLines = append(canonicalHeaderLines, key+":"+strings.TrimSpace(headers[key]))
	}
	canonicalRequest := strings.Join([]string{
		strings.ToUpper(method), path, canonicalQuery,
		strings.Join(canonicalHeaderLines, "\n") + "\n",
		strings.Join(signedHeaders, ";"), bodySHA,
	}, "\n")
	date := xDate
	if len(date) >= 8 {
		date = date[:8]
	}
	scope := strings.Join([]string{date, volcesAssetRegion, volcesAssetService, "request"}, "/")
	stringToSign := strings.Join([]string{"HMAC-SHA256", xDate, scope, sha256HexString(canonicalRequest)}, "\n")
	kDate := hmacSHA256([]byte(secretKey), date)
	kRegion := hmacSHA256(kDate, volcesAssetRegion)
	kService := hmacSHA256(kRegion, volcesAssetService)
	kSigning := hmacSHA256(kService, "request")
	signature := hex.EncodeToString(hmacSHA256(kSigning, stringToSign))
	return "HMAC-SHA256 Credential=" + accessKey + "/" + scope + ", SignedHeaders=" + strings.Join(signedHeaders, ";") + ", Signature=" + signature
}

func canonicalVolcesAssetQuery(values map[string]string) string {
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys))
	for _, key := range keys {
		parts = append(parts, url.QueryEscape(key)+"="+url.QueryEscape(values[key]))
	}
	return strings.ReplaceAll(strings.Join(parts, "&"), "+", "%20")
}

func sha256HexBytes(value []byte) string {
	digest := sha256.Sum256(value)
	return hex.EncodeToString(digest[:])
}

func sha256HexString(value string) string { return sha256HexBytes([]byte(value)) }

func hmacSHA256(key []byte, value string) []byte {
	mac := hmac.New(sha256.New, key)
	_, _ = mac.Write([]byte(value))
	return mac.Sum(nil)
}
