package runner

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"path"
	"strings"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

const directOSSUploadTimeout = 120 * time.Second

type directOSSUploader struct {
	endpoint        string
	bucket          string
	accessKeyID     string
	accessKeySecret string
	publicBaseURL   string
	objectPrefix    string
	client          *http.Client
	now             func() time.Time
}

func newDirectOSSUploader(cfg config.Config) *directOSSUploader {
	if !cfg.MediaOSSDirectEnabled {
		return nil
	}
	return &directOSSUploader{
		endpoint:        strings.TrimRight(cfg.MediaOSSEndpoint, "/"),
		bucket:          strings.TrimSpace(cfg.MediaOSSBucket),
		accessKeyID:     strings.TrimSpace(cfg.MediaOSSAccessKeyID),
		accessKeySecret: strings.TrimSpace(cfg.MediaOSSAccessKeySecret),
		publicBaseURL:   strings.TrimRight(cfg.MediaOSSPublicBaseURL, "/"),
		objectPrefix:    strings.Trim(cfg.MediaOSSObjectPrefix, "/"),
		client: &http.Client{
			Timeout: directOSSUploadTimeout,
			Transport: &http.Transport{
				MaxIdleConns:        256,
				MaxIdleConnsPerHost: 256,
				MaxConnsPerHost:     256,
				IdleConnTimeout:     90 * time.Second,
			},
		},
		now: time.Now,
	}
}

func directOSSScene(scene string) bool {
	switch strings.TrimSpace(scene) {
	case store.FileStorageSceneRequestAsset, store.FileStorageSceneImageResult:
		return true
	default:
		return false
	}
}

func (u *directOSSUploader) upload(ctx context.Context, payload FileUploadPayload) (map[string]any, error) {
	if u == nil {
		return nil, &clients.ClientError{Code: "upload_config_failed", Message: "direct OSS uploader is not configured", Retryable: false}
	}
	objectKey := u.objectKey(payload)
	escapedKey := escapeOSSObjectKey(objectKey)
	uploadURL := u.endpoint + "/" + escapedKey
	contentType := strings.TrimSpace(payload.ContentType)
	if contentType == "" {
		contentType = "application/octet-stream"
	}

	var lastErr error
	for attempt := 0; attempt < 3; attempt++ {
		if attempt > 0 {
			if err := sleepWithContext(ctx, time.Duration(attempt*attempt)*200*time.Millisecond); err != nil {
				return nil, err
			}
		}
		status, err := u.put(ctx, uploadURL, escapedKey, contentType, payload.Bytes)
		if err == nil && status >= 200 && status < 300 {
			publicURL := u.publicBaseURL + "/" + escapedKey
			return map[string]any{
				"url":      publicURL,
				"fileName": objectKey,
				"storageChannel": map[string]any{
					"channelKey": "environment-direct-oss",
					"name":       "Environment Direct OSS",
					"provider":   "aliyun_oss_direct",
				},
				"assetStorage": map[string]any{
					"scene":    payload.Scene,
					"source":   firstNonEmptyString(payload.Source, "ai-gateway"),
					"strategy": "direct_aliyun_oss",
				},
			}, nil
		}
		if err != nil {
			lastErr = err
			continue
		}
		lastErr = fmt.Errorf("HTTP %d", status)
		if status != http.StatusTooManyRequests && status < 500 {
			break
		}
	}
	message := "direct OSS upload failed"
	if lastErr != nil {
		message += ": " + lastErr.Error()
	}
	return nil, &clients.ClientError{Code: "upload_failed", Message: message, Retryable: true}
}

func (u *directOSSUploader) put(ctx context.Context, uploadURL string, escapedKey string, contentType string, payload []byte) (int, error) {
	date := u.now().UTC().Format(http.TimeFormat)
	canonicalResource := "/" + u.bucket + "/" + escapedKey
	stringToSign := "PUT\n\n" + contentType + "\n" + date + "\n" + canonicalResource
	mac := hmac.New(sha1.New, []byte(u.accessKeySecret))
	_, _ = mac.Write([]byte(stringToSign))
	signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))

	req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadURL, bytes.NewReader(payload))
	if err != nil {
		return 0, err
	}
	req.Header.Set("Authorization", "OSS "+u.accessKeyID+":"+signature)
	req.Header.Set("Content-Type", contentType)
	req.Header.Set("Date", date)
	resp, err := u.client.Do(req)
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()
	_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 64<<10))
	return resp.StatusCode, nil
}

func (u *directOSSUploader) objectKey(payload FileUploadPayload) string {
	now := u.now().UTC()
	extension := uploadFileExtension(payload.ContentType, path.Ext(payload.FileName))
	baseName := strings.TrimSuffix(path.Base(payload.FileName), path.Ext(payload.FileName))
	baseName = sanitizeGeneratedAssetNamePart(baseName)
	if baseName == "" {
		baseName = "gateway-media"
	}
	if len(baseName) > 48 {
		baseName = baseName[:48]
	}
	return fmt.Sprintf(
		"%s/%s/%04d/%02d/%02d/%s-%s%s",
		u.objectPrefix,
		strings.TrimSpace(payload.Scene),
		now.Year(),
		now.Month(),
		now.Day(),
		baseName,
		randomHexSuffix(8),
		extension,
	)
}

func escapeOSSObjectKey(objectKey string) string {
	parts := strings.Split(objectKey, "/")
	for index, part := range parts {
		parts[index] = url.PathEscape(part)
	}
	return strings.Join(parts, "/")
}
