package file_service

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"path/filepath"
	"strings"

	"evening_detective_server/internal/modules/file_storage"
	"evening_detective_server/internal/modules/string_tools"
)

// Ошибки валидации загружаемого файла. Маппятся в gRPC-статусы в app-слое:
// ErrFileTooLarge → ResourceExhausted, остальные → InvalidArgument.
var (
	ErrEmptyName          = errors.New("пустое имя файла")
	ErrEmptyData          = errors.New("пустые данные файла")
	ErrInvalidExtension   = errors.New("недопустимое расширение файла (разрешены pdf, jpg, jpeg, png, gif, webp, mp3, ogg, wav)")
	ErrInvalidContentType = errors.New("содержимое файла не соответствует разрешённым типам (pdf, изображения или аудио)")
	ErrFileTooLarge       = errors.New("файл слишком большой")
)

// allowedExtensions — расширения, разрешённые для загрузки: PDF, изображения
// и аудио (аудио-введение сценария загружается тем же эндпоинтом).
var allowedExtensions = map[string]struct{}{
	".pdf":  {},
	".jpg":  {},
	".jpeg": {},
	".png":  {},
	".gif":  {},
	".webp": {},
	".mp3":  {},
	".ogg":  {},
	".wav":  {},
}

type FileService struct {
	fileStorage file_storage.IFileStorage
	stringTools string_tools.IStringTools
	maxFileSize int
}

func NewFileService(
	fileStorage file_storage.IFileStorage,
	stringTools string_tools.IStringTools,
	maxFileSize int,
) *FileService {
	return &FileService{
		fileStorage: fileStorage,
		stringTools: stringTools,
		maxFileSize: maxFileSize,
	}
}

// UploadFile сохраняет файл в хранилище под уникальным именем и возвращает
// сохранённое имя и категорию файла (image|pdf|audio).
//
// Порядок проверок: имя/данные → расширение → base → размер → содержимое.
// Проверка размера — defense-in-depth: фактический лимит запроса
// устанавливается на HTTP-слое (MaxBytesReader) и gRPC-лимитами.
func (s *FileService) UploadFile(
	ctx context.Context,
	filename string,
	data []byte,
) (string, string, error) {
	if filename == "" {
		return "", "", ErrEmptyName
	}
	if len(data) == 0 {
		return "", "", ErrEmptyData
	}

	ext := strings.ToLower(filepath.Ext(filename))
	if _, ok := allowedExtensions[ext]; !ok {
		return "", "", ErrInvalidExtension
	}

	base := strings.TrimSuffix(filename, filepath.Ext(filename))
	if base == "" {
		return "", "", ErrEmptyName
	}

	// Транслитерация может обнулить base (например, «!!!.pdf»): хранимое
	// имя без base недопустимо — проверяем до размера и содержимого.
	transliterated := s.stringTools.Transliterate(base)
	if transliterated == "" {
		return "", "", ErrEmptyName
	}

	if len(data) > s.maxFileSize {
		return "", "", fmt.Errorf("%w (лимит %d МБ)", ErrFileTooLarge, s.maxFileSize>>20)
	}

	// Категория по содержимому (сырой sniff): text/* и octet-stream не
	// проходят вообще, расширение должно соответствовать категории.
	sniffed := http.DetectContentType(data)
	fileType := contentTypeCategory(sniffed)
	if fileType == "" || fileType != file_storage.FileType(filename) {
		return "", "", ErrInvalidContentType
	}

	randomName, err := file_storage.NewName()
	if err != nil {
		return "", "", fmt.Errorf("не удалось сгенерировать имя файла: %w", err)
	}
	storedName := transliterated + "_" + randomName + ext

	if err := s.fileStorage.Put(ctx, &file_storage.File{
		Name: storedName,
		Data: data,
		Mime: sniffed,
	}); err != nil {
		return "", "", err
	}
	return storedName, fileType, nil
}

// contentTypeCategory возвращает категорию по sniffed-типу:
// application/pdf → pdf; image/* → image; audio/* и application/ogg → audio;
// всё остальное (text/*, application/octet-stream и пр.) → "".
func contentTypeCategory(sniffed string) string {
	switch {
	case sniffed == "application/pdf":
		return "pdf"
	case strings.HasPrefix(sniffed, "image/"):
		return "image"
	case strings.HasPrefix(sniffed, "audio/"):
		return "audio"
	case sniffed == "application/ogg":
		return "audio"
	default:
		return ""
	}
}

func (s *FileService) DownloadFile(
	ctx context.Context,
	filename string,
) (*file_storage.File, error) {
	file, err := s.fileStorage.Get(ctx, filename)
	if err != nil {
		return nil, err
	}
	return file, nil
}
