package biz

import (
	"bufio"
	"context"
	"crypto/md5"
	"encoding/hex"
	"errors"
	"io"
	"net/http"
	"path/filepath"
	"strings"
	"time"

	"github.com/google/uuid"
)

type MediaRepo interface {
	MediaMetadataRepo
	UploadRepo
}

type MediaUsecase struct {
	MediaRepo
	files    FileStorage
	settings RuntimeSettings
}

var ErrMediaTooLarge = errors.New("文件超过大小上限")

func NewMediaUsecase(repo MediaRepo, files FileStorage, settings RuntimeSettings) *MediaUsecase {
	return &MediaUsecase{MediaRepo: repo, files: files, settings: settings}
}

func (uc *MediaUsecase) maxMediaFileSize() int64 {
	if uc.settings == nil {
		return MediaSettings{}.EffectiveMaxFileSize()
	}
	return uc.settings.MediaSettings().EffectiveMaxFileSize()
}

var allowedMediaExtensions = map[string]bool{
	".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, ".bmp": true, ".ico": true, ".avif": true,
	".mp3": true, ".wav": true, ".ogg": true, ".m4a": true, ".flac": true, ".aac": true,
	".mp4": true, ".webm": true, ".mov": true, ".avi": true, ".mkv": true,
	".txt": true, ".md": true, ".csv": true, ".json": true, ".log": true, ".pdf": true,
	".doc": true, ".docx": true, ".xls": true, ".xlsx": true, ".ppt": true, ".pptx": true,
	".zip": true, ".rar": true, ".7z": true, ".tar": true, ".gz": true, ".tgz": true, ".bin": true,
}

func validateMediaName(name string) error {
	if name == "" || strings.TrimSpace(name) != name || strings.ContainsAny(name, `/\`) {
		return errors.New("file extension is not allowed")
	}
	if !allowedMediaExtensions[strings.ToLower(filepath.Ext(name))] {
		return errors.New("file extension is not allowed")
	}
	return nil
}
func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, suppliedMIME string, categoryID int, reader io.Reader, save bool) (*MediaFile, error) {
	if err := validateMediaName(name); err != nil {
		return nil, err
	}
	ext := strings.ToLower(filepath.Ext(name))
	buffered := bufio.NewReader(reader)
	// Keep the multipart Content-Type when the client supplied one.
	// Only infer it when the header is empty: first by extension, then by
	// sniffing the first 512 bytes.  Overwriting a valid client MIME with
	// http.DetectContentType changes the media record for common uploads
	// (for example SVG/JSON), and diverges from the compatible DetectMIME helper.
	if strings.TrimSpace(suppliedMIME) == "" {
		if detected := mediaMIMEByExtension(ext); detected != "" {
			suppliedMIME = detected
		} else if header, _ := buffered.Peek(512); len(header) > 0 {
			suppliedMIME = http.DetectContentType(header)
		} else {
			suppliedMIME = "application/octet-stream"
		}
	}
	key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext
	hash := md5.New()
	maxSize := uc.maxMediaFileSize()
	limited := &io.LimitedReader{R: buffered, N: maxSize + 1}
	stored, err := uc.files.Put(ctx, key, io.TeeReader(limited, hash))
	if err != nil {
		return nil, err
	}
	readSize := maxSize + 1 - limited.N
	if stored == nil {
		_ = uc.files.Delete(ctx, key)
		return nil, errors.New("文件存储未返回结果")
	}
	if readSize > maxSize || stored.Size > maxSize {
		_ = uc.files.Delete(ctx, key)
		return nil, ErrMediaTooLarge
	}
	if stored.Size != readSize {
		_ = uc.files.Delete(ctx, key)
		return nil, errors.New("文件存储大小不一致")
	}
	media := &MediaFile{Name: name, CategoryID: categoryID, URL: stored.URL, Tag: strings.TrimPrefix(filepath.Ext(name), "."), Key: key, Size: stored.Size, Mime: suppliedMIME, MD5: hex.EncodeToString(hash.Sum(nil)), UserID: userID}
	if save {
		count, countErr := uc.MediaKeyReferences(ctx, key)
		if countErr != nil {
			return nil, countErr
		}
		if count == 0 {
			if err = uc.CreateMedia(ctx, media); err != nil {
				return nil, err
			}
		}
	}
	return media, nil
}

// mediaMIMEByExtension mirrors the small extension fallback table. It is
// intentionally narrower than mime.TypeByExtension: all other types are sniffed.
// types from the first 512 bytes.
func mediaMIMEByExtension(ext string) string {
	switch ext {
	case ".json":
		return "application/json"
	case ".xml":
		return "application/xml"
	case ".pdf":
		return "application/pdf"
	case ".zip":
		return "application/zip"
	case ".mp4":
		return "video/mp4"
	case ".mp3":
		return "audio/mpeg"
	case ".csv":
		return "text/csv"
	default:
		return ""
	}
}
func (uc *MediaUsecase) Delete(ctx context.Context, id uint) error {
	media, err := uc.FindMedia(ctx, id)
	if err != nil {
		return err
	}
	count, err := uc.MediaKeyReferences(ctx, media.Key)
	if err != nil {
		return err
	}
	if count <= 1 {
		if err = uc.files.Delete(ctx, media.Key); err != nil {
			return err
		}
	}
	return uc.DeleteMedia(ctx, id)
}
func (uc *MediaUsecase) ListStorage(ctx context.Context, prefix, cursor string, limit int) ([]*StoredFile, string, bool, error) {
	if limit <= 0 {
		limit = 100
	}
	// Multipart chunks are an internal staging detail. The compatible local
	// implementation keeps them outside the configured object-storage root, so
	// the public listing must not expose them even when the selected backend is
	// also used for staging.
	chunkRoot := "uploads/chunks"
	if uc.settings != nil {
		if configured := strings.Trim(uc.settings.MediaSettings().ChunkDir, "/\\ "); configured != "" {
			chunkRoot = configured
		}
	}
	chunkRoot = strings.Trim(chunkRoot, "/") + "/"
	result := make([]*StoredFile, 0, limit)
	nextCursor := cursor
	for {
		// Ask only for the remaining visible capacity. Otherwise a page that is
		// partially filtered could fill the response halfway through the next
		// storage page while returning that page's end cursor, skipping objects.
		remaining := limit - len(result)
		items, next, more, err := uc.files.List(ctx, prefix, nextCursor, remaining)
		if err != nil {
			return nil, "", false, err
		}
		for _, item := range items {
			if item == nil || strings.HasPrefix(strings.TrimPrefix(item.Path, "/"), chunkRoot) {
				continue
			}
			result = append(result, item)
			if len(result) == limit {
				return result, next, more, nil
			}
		}
		if !more || next == "" || next == nextCursor {
			return result, next, more, nil
		}
		nextCursor = next
	}
}
