package admin

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"mime"
	"net/http"
	"net/url"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

type ImageUploader struct {
	client         *s3.Client
	bucket         string
	prefix         string
	publicBaseURL  string
	maxUploadBytes int64
}

type UploadedImage struct {
	Key         string `json:"key"`
	URL         string `json:"url"`
	Markdown    string `json:"markdown"`
	Filename    string `json:"filename"`
	Size        int64  `json:"size"`
	ContentType string `json:"contentType"`
}

func NewImageUploader(ctx context.Context, cfg R2Config) (*ImageUploader, error) {
	normalized, err := normalizeR2Config(cfg)
	if err != nil {
		return nil, err
	}

	awsConfig, err := config.LoadDefaultConfig(ctx,
		config.WithRegion(normalized.Region),
		config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
			normalized.AccessKeyID,
			normalized.SecretAccessKey,
			"",
		)),
	)
	if err != nil {
		return nil, err
	}

	client := s3.NewFromConfig(awsConfig, func(options *s3.Options) {
		options.BaseEndpoint = aws.String(normalized.Endpoint)
		options.UsePathStyle = true
	})

	return &ImageUploader{
		client:         client,
		bucket:         normalized.Bucket,
		prefix:         normalized.Prefix,
		publicBaseURL:  normalized.PublicBaseURL,
		maxUploadBytes: normalized.MaxUploadBytes,
	}, nil
}

func (u *ImageUploader) Upload(ctx context.Context, filename string, body io.Reader, size int64) (UploadedImage, error) {
	if u == nil {
		return UploadedImage{}, errors.New("R2 image uploader is not configured")
	}
	if size <= 0 {
		return UploadedImage{}, errors.New("file is empty")
	}
	if u.maxUploadBytes > 0 && size > u.maxUploadBytes {
		return UploadedImage{}, fmt.Errorf("file is too large: max %d bytes", u.maxUploadBytes)
	}

	limit := u.maxUploadBytes
	if limit <= 0 {
		limit = 20 * 1024 * 1024
	}
	data, err := io.ReadAll(io.LimitReader(body, limit+1))
	if err != nil {
		return UploadedImage{}, err
	}
	if int64(len(data)) > limit {
		return UploadedImage{}, fmt.Errorf("file is too large: max %d bytes", limit)
	}
	if len(data) == 0 {
		return UploadedImage{}, errors.New("file is empty")
	}

	head := data
	if len(head) > 512 {
		head = head[:512]
	}
	contentType := http.DetectContentType(head)
	if !strings.HasPrefix(contentType, "image/") {
		return UploadedImage{}, errors.New("only image files are supported")
	}

	key, cleanFilename, err := u.objectKey(filename, contentType)
	if err != nil {
		return UploadedImage{}, err
	}

	if _, err := u.client.PutObject(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(u.bucket),
		Key:         aws.String(key),
		Body:        bytes.NewReader(data),
		ContentType: aws.String(contentType),
	}); err != nil {
		return UploadedImage{}, err
	}

	publicURL := strings.TrimRight(u.publicBaseURL, "/") + "/" + escapePath(key)
	return UploadedImage{
		Key:         key,
		URL:         publicURL,
		Markdown:    fmt.Sprintf("![%s](%s)", cleanFilename, publicURL),
		Filename:    cleanFilename,
		Size:        size,
		ContentType: contentType,
	}, nil
}

func (u *ImageUploader) objectKey(filename string, contentType string) (string, string, error) {
	cleanFilename := sanitizeFilename(filename)
	ext := strings.ToLower(filepath.Ext(cleanFilename))
	if ext == "" {
		extensions, _ := mime.ExtensionsByType(contentType)
		if len(extensions) > 0 {
			ext = extensions[0]
			cleanFilename += ext
		}
	}
	token, err := randomHex(4)
	if err != nil {
		return "", "", err
	}
	stem := strings.TrimSuffix(cleanFilename, filepath.Ext(cleanFilename))
	keyName := fmt.Sprintf("%s-%s-%s%s", time.Now().Format("20060102-150405"), token, stem, ext)
	return joinObjectPath(u.prefix, keyName), cleanFilename, nil
}

func normalizeR2Config(cfg R2Config) (R2Config, error) {
	cfg.Endpoint = strings.TrimSpace(cfg.Endpoint)
	cfg.Bucket = strings.Trim(strings.TrimSpace(cfg.Bucket), "/")
	cfg.Prefix = strings.Trim(strings.TrimSpace(cfg.Prefix), "/")
	cfg.AccessKeyID = strings.TrimSpace(cfg.AccessKeyID)
	cfg.SecretAccessKey = strings.TrimSpace(cfg.SecretAccessKey)
	cfg.Region = firstNonEmpty(cfg.Region, "auto")
	cfg.PublicBaseURL = strings.TrimSpace(cfg.PublicBaseURL)
	if cfg.MaxUploadBytes <= 0 {
		cfg.MaxUploadBytes = 20 * 1024 * 1024
	}

	endpoint, err := url.Parse(cfg.Endpoint)
	if err != nil || endpoint.Scheme == "" || endpoint.Host == "" {
		return R2Config{}, errors.New("r2.endpoint must be a valid URL")
	}
	endpointPath := strings.Trim(endpoint.Path, "/")
	endpoint.Path = ""
	endpoint.RawPath = ""
	endpoint.RawQuery = ""
	endpoint.Fragment = ""
	cfg.Endpoint = endpoint.String()

	if cfg.Bucket == "" {
		return R2Config{}, errors.New("r2.bucket is required")
	}
	bucketParts := strings.SplitN(cfg.Bucket, "/", 2)
	cfg.Bucket = bucketParts[0]
	if len(bucketParts) == 2 {
		cfg.Prefix = joinObjectPath(bucketParts[1], cfg.Prefix)
	}
	if endpointPath != "" && endpointPath != cfg.Bucket {
		cfg.Prefix = joinObjectPath(endpointPath, cfg.Prefix)
	}

	if cfg.AccessKeyID == "" || cfg.SecretAccessKey == "" {
		return R2Config{}, errors.New("r2 access keys are required")
	}
	if cfg.PublicBaseURL == "" {
		return R2Config{}, errors.New("r2.publicBaseUrl is required")
	}
	if !strings.HasPrefix(cfg.PublicBaseURL, "http://") && !strings.HasPrefix(cfg.PublicBaseURL, "https://") {
		cfg.PublicBaseURL = "https://" + cfg.PublicBaseURL
	}
	return cfg, nil
}

func sanitizeFilename(filename string) string {
	filename = strings.TrimSpace(filepath.Base(filename))
	if filename == "" || filename == "." {
		return "image"
	}
	var out strings.Builder
	for _, r := range filename {
		switch {
		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
			out.WriteRune(r)
		case r == '.', r == '-', r == '_':
			out.WriteRune(r)
		default:
			out.WriteByte('-')
		}
	}
	cleaned := strings.Trim(out.String(), ".-") // keep keys readable and URL-safe
	if cleaned == "" {
		return "image"
	}
	return cleaned
}

func joinObjectPath(parts ...string) string {
	clean := []string{}
	for _, part := range parts {
		part = strings.Trim(part, "/")
		if part != "" {
			clean = append(clean, part)
		}
	}
	return path.Join(clean...)
}

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

func randomHex(bytes int) (string, error) {
	buffer := make([]byte, bytes)
	if _, err := rand.Read(buffer); err != nil {
		return "", err
	}
	return hex.EncodeToString(buffer), nil
}
