package app

import (
	"compress/gzip"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"

	cos "github.com/tencentyun/cos-go-sdk-v5"
	"hyapp/services/wallet-service/internal/config"
)

var errWalletOutboxArchiveObjectExists = errors.New("wallet outbox archive object already exists")

// walletOutboxArchiveCOSStore 只使用私有 bucket API，不生成公网 URL，也不在对象上设置 public-read ACL。
type walletOutboxArchiveCOSStore struct {
	client *cos.Client
}

func newWalletOutboxArchiveCOSStore(cfg config.OutboxArchiveCOSConfig, transport http.RoundTripper) (*walletOutboxArchiveCOSStore, error) {
	bucketURL, err := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", strings.TrimSpace(cfg.Bucket), strings.TrimSpace(cfg.Region)))
	if err != nil {
		return nil, fmt.Errorf("build wallet outbox archive COS url: %w", err)
	}
	if transport == nil {
		transport = http.DefaultTransport
	}
	var authorizationTransport http.RoundTripper
	if cfg.CredentialSource == "cvm_role" {
		authorizationTransport = &cvmRoleAuthorizationTransport{provider: newCVMCAMCredentialProvider(cfg.CAMRoleName), transport: transport}
	} else {
		authorizationTransport = &cos.AuthorizationTransport{
			SecretID:     cfg.SecretID,
			SecretKey:    cfg.SecretKey,
			SessionToken: cfg.SessionToken,
			Transport:    transport,
		}
	}
	return &walletOutboxArchiveCOSStore{client: cos.NewClient(&cos.BaseURL{BucketURL: bucketURL}, &http.Client{
		Timeout:   cfg.HTTPTimeout,
		Transport: authorizationTransport,
	})}, nil
}

func (s *walletOutboxArchiveCOSStore) putGzipAndVerify(ctx context.Context, key string, filePath string, sizeBytes int64, gzipSHA256 string, ndjsonSHA256 string) (string, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return "", err
	}
	defer file.Close()
	// 对象本体已经是 gzip 文件，不设置 Content-Encoding。Go HTTP Transport 会自动解压带
	// Content-Encoding:gzip 的响应，导致后续无法同时核对“压缩对象 hash”和“解压 NDJSON hash”。
	putCRC64, err := s.put(ctx, key, file, sizeBytes, "application/gzip", "", gzipSHA256)
	if err != nil && !errors.Is(err, errWalletOutboxArchiveObjectExists) {
		return "", err
	}
	crc64, err := s.verifyHead(ctx, key, sizeBytes, gzipSHA256, putCRC64)
	if err != nil {
		return "", err
	}
	if err := s.verifyGzipReadback(ctx, key, gzipSHA256, ndjsonSHA256); err != nil {
		return "", err
	}
	return crc64, nil
}

func (s *walletOutboxArchiveCOSStore) putBytesAndVerify(ctx context.Context, key string, body []byte, contentType string, expectedSHA256 string) (string, error) {
	putCRC64, err := s.put(ctx, key, strings.NewReader(string(body)), int64(len(body)), contentType, "", expectedSHA256)
	if err != nil && !errors.Is(err, errWalletOutboxArchiveObjectExists) {
		return "", err
	}
	crc64, err := s.verifyHead(ctx, key, int64(len(body)), expectedSHA256, putCRC64)
	if err != nil {
		return "", err
	}
	response, err := s.client.Object.Get(ctx, key, nil)
	if err != nil {
		return "", err
	}
	defer response.Body.Close()
	hash := sha256.New()
	if _, err := io.Copy(hash, response.Body); err != nil {
		return "", err
	}
	if actual := hex.EncodeToString(hash.Sum(nil)); actual != expectedSHA256 {
		return "", fmt.Errorf("wallet outbox archive COS readback hash mismatch for %s", key)
	}
	return crc64, nil
}

func (s *walletOutboxArchiveCOSStore) put(ctx context.Context, key string, reader io.Reader, sizeBytes int64, contentType string, contentEncoding string, contentSHA256 string) (string, error) {
	if s == nil || s.client == nil || strings.TrimSpace(key) == "" || reader == nil || sizeBytes <= 0 {
		return "", fmt.Errorf("wallet outbox archive COS put input is invalid")
	}
	metadata := make(http.Header)
	metadata.Set("x-cos-meta-sha256", contentSHA256)
	optionHeaders := make(http.Header)
	// 归档键由内容指纹决定；禁止覆盖可避免 bucket versioning 在并发/重试时生成重复版本。
	optionHeaders.Set("x-cos-forbid-overwrite", "true")
	options := &cos.ObjectPutOptions{ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{
		ContentLength:            sizeBytes,
		ContentType:              contentType,
		ContentEncoding:          contentEncoding,
		XCosMetaXXX:              &metadata,
		XOptionHeader:            &optionHeaders,
		XCosStorageClass:         "STANDARD",
		XCosServerSideEncryption: "AES256",
	}}
	response, err := s.client.Object.Put(ctx, strings.TrimLeft(key, "/"), reader, options)
	crc64 := ""
	if response != nil {
		crc64 = strings.TrimSpace(response.Header.Get("x-cos-hash-crc64ecma"))
	}
	if response != nil && response.Body != nil {
		_ = response.Body.Close()
	}
	if err != nil {
		var cosErr *cos.ErrorResponse
		if errors.As(err, &cosErr) && cosErr.Response != nil && (cosErr.Response.StatusCode == http.StatusConflict || cosErr.Response.StatusCode == http.StatusPreconditionFailed) {
			return "", errWalletOutboxArchiveObjectExists
		}
		return "", err
	}
	if crc64 == "" {
		return "", fmt.Errorf("wallet outbox archive COS PUT response has no CRC64 for %s", key)
	}
	return crc64, nil
}

func (s *walletOutboxArchiveCOSStore) verifyHead(ctx context.Context, key string, expectedSize int64, expectedSHA256 string, expectedCRC64 string) (string, error) {
	response, err := s.client.Object.Head(ctx, key, nil)
	if err != nil {
		return "", err
	}
	if response.Body != nil {
		defer response.Body.Close()
	}
	if response.ContentLength != expectedSize || response.Header.Get("x-cos-meta-sha256") != expectedSHA256 {
		return "", fmt.Errorf("wallet outbox archive COS HEAD mismatch for %s", key)
	}
	crc64 := strings.TrimSpace(response.Header.Get("x-cos-hash-crc64ecma"))
	if crc64 == "" || (expectedCRC64 != "" && crc64 != expectedCRC64) {
		return "", fmt.Errorf("wallet outbox archive COS HEAD CRC64 mismatch for %s", key)
	}
	// SSE-COS 是归档对象的强制约束；响应缺失加密标记时不能写 verified receipt。
	if !strings.EqualFold(response.Header.Get("x-cos-server-side-encryption"), "AES256") {
		return "", fmt.Errorf("wallet outbox archive COS object is not SSE-COS encrypted: %s", key)
	}
	return crc64, nil
}

func (s *walletOutboxArchiveCOSStore) verifyGzipReadback(ctx context.Context, key string, expectedGzipSHA256 string, expectedNDJSONSHA256 string) error {
	response, err := s.client.Object.Get(ctx, key, nil)
	if err != nil {
		return err
	}
	defer response.Body.Close()
	gzipHash := sha256.New()
	compressedReader := io.TeeReader(response.Body, gzipHash)
	gzipReader, err := gzip.NewReader(compressedReader)
	if err != nil {
		return err
	}
	ndjsonHash := sha256.New()
	_, copyErr := io.Copy(ndjsonHash, gzipReader)
	closeErr := gzipReader.Close()
	// gzip.Reader 可能在内部缓冲区之后留下响应尾部；继续读完才能验证整个压缩对象的 hash。
	_, drainErr := io.Copy(io.Discard, compressedReader)
	if copyErr != nil {
		return copyErr
	}
	if closeErr != nil {
		return closeErr
	}
	if drainErr != nil {
		return drainErr
	}
	if actual := hex.EncodeToString(gzipHash.Sum(nil)); actual != expectedGzipSHA256 {
		return fmt.Errorf("wallet outbox archive COS gzip hash mismatch for %s", key)
	}
	if actual := hex.EncodeToString(ndjsonHash.Sum(nil)); actual != expectedNDJSONSHA256 {
		return fmt.Errorf("wallet outbox archive COS ndjson hash mismatch for %s", key)
	}
	return nil
}
