package file

import (
	"bytes"
	"context"
	"errors"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	"strings"
)

var (
	ErrDependencyUnavailable = errors.New("dependency unavailable")
	ErrInvalidFile           = errors.New("invalid file")
)

const maxUploadSize = 10 * 1024 * 1024

var allowedContentTypes = map[string]bool{
	"image/jpeg":      true,
	"image/png":       true,
	"image/webp":      true,
	"application/pdf": true,
}

type Service struct {
	storage *Storage
}

func NewService(storage *Storage) *Service {
	return &Service{storage: storage}
}

func (s *Service) Upload(req uploadRequest) (*UploadDTO, error) {
	if s.storage == nil {
		return nil, ErrDependencyUnavailable
	}
	if req.Header == nil || req.Reader == nil || req.Header.Size <= 0 {
		return nil, ErrInvalidFile
	}
	data, err := readUploadData(req.Reader)
	if err != nil || len(data) == 0 || len(data) > maxUploadSize {
		return nil, ErrInvalidFile
	}
	contentType := normalizeContentType(req.ContentType, data)
	scene := normalizeScene(req.Scene)
	if !allowedContentTypes[contentType] || (scene == "chat" && !strings.HasPrefix(contentType, "image/")) {
		return nil, ErrInvalidFile
	}
	key, err := newObjectKey(scene, req.Header.Filename)
	if err != nil {
		return nil, err
	}
	if err := s.storage.PutObject(req.Context, key, bytes.NewReader(data), int64(len(data)), contentType, map[string]string{
		"original-filename": req.Header.Filename,
	}); err != nil {
		return nil, err
	}
	var thumbnailURL string
	var mediumURL string
	for _, variant := range generateImageVariants(key, data, contentType) {
		err := s.storage.PutObject(req.Context, variant.Key, bytes.NewReader(variant.Content), int64(len(variant.Content)), variant.ContentType, map[string]string{
			"source-object": key,
		})
		if err != nil {
			continue
		}
		if strings.Contains(variant.Key, "."+ImageVariantThumb+".") {
			thumbnailURL = fileURLForScene(scene, variant.Key)
		}
		if strings.Contains(variant.Key, "."+ImageVariantMedium+".") {
			mediumURL = fileURLForScene(scene, variant.Key)
		}
	}
	fileURL := fileURLForScene(scene, key)
	return &UploadDTO{
		ObjectKey:    key,
		URL:          fileURL,
		ThumbnailURL: thumbnailURL,
		MediumURL:    mediumURL,
		Filename:     req.Header.Filename,
		ContentType:  contentType,
		Size:         int64(len(data)),
	}, nil
}

func readUploadData(reader multipart.File) ([]byte, error) {
	limited := io.LimitReader(reader, maxUploadSize+1)
	return io.ReadAll(limited)
}

func normalizeContentType(contentType string, data []byte) string {
	contentType = strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
	if allowedContentTypes[contentType] {
		return contentType
	}
	detected := strings.ToLower(strings.TrimSpace(strings.Split(http.DetectContentType(data), ";")[0]))
	if allowedContentTypes[detected] {
		return detected
	}
	return contentType
}

func fileURLForScene(scene string, key string) string {
	fileURL := "/api/files/object?key=" + url.QueryEscape(key)
	if scene == "home-banner" || scene == "avatar" {
		fileURL = "/api/public/files/object?key=" + url.QueryEscape(key)
	}
	return fileURL
}

func normalizeScene(scene string) string {
	scene = strings.TrimSpace(strings.ToLower(scene))
	switch scene {
	case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat":
		return scene
	default:
		return "misc"
	}
}

type uploadRequest struct {
	Context     context.Context
	Scene       string
	Header      *multipart.FileHeader
	Reader      multipart.File
	ContentType string
}
