package chat

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

	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	"github.com/yan1h/agent-coding-workflow/internal/infra/storage"
	"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
)

// Uploader handles multipart file uploads for chat attachments.
// It validates the file size and MIME type, stores the file via Storage,
// and records the attachment in the repository. If storage succeeds but DB
// insert fails, the stored blob is deleted to prevent orphaned objects.
type Uploader struct {
	repo      Repository
	storage   storage.Storage
	mimeWhite map[string]bool
	maxFile   int64
}

// NewUploader constructs an Uploader.
// mimeList is a whitelist of allowed MIME types; "text/*" is a wildcard prefix.
// maxFile is the maximum allowed file size in bytes.
func NewUploader(repo Repository, st storage.Storage, mimeList []string, maxFile int64) *Uploader {
	mw := make(map[string]bool, len(mimeList))
	for _, m := range mimeList {
		mw[m] = true
	}
	return &Uploader{repo: repo, storage: st, mimeWhite: mw, maxFile: maxFile}
}

// Handle implements POST /api/v1/uploads.
func (u *Uploader) Handle(w http.ResponseWriter, r *http.Request) {
	uid, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		writeErr(w, r, errs.New(errs.CodeUnauthorized, "unauthorized"))
		return
	}

	// Parse the multipart form; allow a 1 MiB overhead beyond maxFile for
	// form boundary / metadata.
	if err := r.ParseMultipartForm(u.maxFile + 1<<20); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "parse multipart"))
		return
	}

	file, header, err := r.FormFile("file")
	if err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "no file field"))
		return
	}
	defer file.Close()

	if header.Size > u.maxFile {
		writeErr(w, r, errs.New(errs.CodeChatAttachmentTooLarge,
			fmt.Sprintf("size %d exceeds limit %d", header.Size, u.maxFile)))
		return
	}

	mt := header.Header.Get("Content-Type")
	if !u.mimeAllowed(mt) {
		writeErr(w, r, errs.New(errs.CodeChatAttachmentUnsupportedMime, mt))
		return
	}

	now := time.Now().UTC()
	id, err := uuid.NewV7()
	if err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInternal, "generate uuid"))
		return
	}
	ext := filepath.Ext(header.Filename)
	key := fmt.Sprintf("%04d/%02d/%s%s", now.Year(), int(now.Month()), id.String(), ext)

	res, err := u.storage.Put(r.Context(), key, file, mt)
	if err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeStoragePutFailed, "storage put"))
		return
	}

	att, err := u.repo.InsertAttachment(r.Context(), InsertAttachmentParams{
		ID:          id,
		UserID:      uid,
		Filename:    header.Filename,
		MimeType:    mt,
		SizeBytes:   res.Size,
		SHA256:      res.SHA256,
		StoragePath: res.StoragePath,
	})
	if err != nil {
		// Best-effort blob cleanup to prevent orphaned storage objects.
		_ = u.storage.Delete(context.Background(), res.StoragePath)
		writeErr(w, r, err)
		return
	}

	writeJSON(w, http.StatusOK, AttachmentDTO{
		ID:        att.ID,
		Filename:  att.Filename,
		MimeType:  att.MimeType,
		SizeBytes: att.SizeBytes,
		SHA256:    att.SHA256,
		CreatedAt: att.CreatedAt,
	})
}

// mimeAllowed reports whether the given MIME type is in the whitelist.
// "text/*" in the whitelist acts as a wildcard for any text/ subtype.
func (u *Uploader) mimeAllowed(mt string) bool {
	if u.mimeWhite[mt] {
		return true
	}
	if u.mimeWhite["text/*"] && strings.HasPrefix(mt, "text/") {
		return true
	}
	return false
}
