package httpapi

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"git.azzamo.net/Michilis/Spanglish/photo-api/internal/auth"
	"git.azzamo.net/Michilis/Spanglish/photo-api/internal/checksum"
	"git.azzamo.net/Michilis/Spanglish/photo-api/internal/imaging"
	"git.azzamo.net/Michilis/Spanglish/photo-api/internal/store"
)

// uploadPhotos accepts multipart form data with one or more "files" parts.
// Each file is sniffed by magic bytes (client filename/Content-Type are
// untrusted, same policy as /api/media/upload), stored as the untouched
// original, and queued for variant processing.
//
// A file whose bytes are already in this gallery is not stored a second time:
// the incoming copy is discarded, the existing photo is echoed back with
// "duplicate": true, and the rest of the batch carries on.
func (s *Server) uploadPhotos(w http.ResponseWriter, r *http.Request, _ auth.User) {
	galleryID := r.PathValue("id")
	g, err := s.db.GetGallery(r.Context(), galleryID)
	if err != nil {
		writeStoreError(w, err, "Gallery not found")
		return
	}

	maxFile := int64(s.cfg.MaxUploadMB) << 20
	// Generous request ceiling; nginx enforces its own client_max_body_size.
	r.Body = http.MaxBytesReader(w, r.Body, 40*maxFile)
	mr, err := r.MultipartReader()
	if err != nil {
		writeError(w, http.StatusBadRequest, "Expected multipart/form-data")
		return
	}

	position, err := s.db.NextPosition(r.Context(), g.ID)
	if err != nil {
		writeStoreError(w, err, "")
		return
	}

	var created []photoJSON
	for {
		part, err := mr.NextPart()
		if err == io.EOF {
			break
		}
		if err != nil {
			writeError(w, http.StatusBadRequest, "Malformed multipart body")
			return
		}
		if part.FormName() != "files" && part.FormName() != "file" {
			part.Close()
			continue
		}
		photo, duplicate, uploadErr := s.saveUpload(r, g, part, position, maxFile)
		part.Close()
		if uploadErr != nil {
			// One bad file fails the request explicitly rather than silently
			// skipping it; the admin UI uploads files individually.
			writeError(w, uploadErr.status, uploadErr.msg)
			return
		}
		out := s.photoToJSON(photo, s.viewTokenFor(g), true)
		out.Duplicate = duplicate
		created = append(created, out)
		if duplicate {
			continue // nothing was stored, so the next file keeps this position
		}
		position++
	}

	if len(created) == 0 {
		writeError(w, http.StatusBadRequest, "No file provided")
		return
	}
	s.worker.Nudge()
	writeJSON(w, http.StatusCreated, map[string]any{"photos": created})
}

type uploadError struct {
	status int
	msg    string
}

// saveUpload stores one part. The bool it returns reports a duplicate: the
// gallery already holds these bytes, so nothing was written and the photo
// returned is the existing one.
func (s *Server) saveUpload(r *http.Request, g store.Gallery, part *multipart.Part, position int, maxFile int64) (store.Photo, bool, *uploadError) {
	head := make([]byte, 16)
	n, err := io.ReadFull(part, head)
	if err != nil && err != io.ErrUnexpectedEOF {
		return store.Photo{}, false, &uploadError{http.StatusBadRequest, "Could not read file"}
	}
	head = head[:n]
	contentType, ext, ok, reason := imaging.Sniff(head)
	if !ok {
		return store.Photo{}, false, &uploadError{http.StatusUnsupportedMediaType, reason}
	}
	if contentType == "image/heic" && imaging.DetectHeicConverter(s.cfg.HeicConverter) == nil {
		return store.Photo{}, false, &uploadError{http.StatusUnsupportedMediaType,
			"HEIC uploads need an image converter on the server (install libvips-tools); please upload JPEG instead"}
	}

	// Spool to a temp file to learn the size before handing to storage
	// (S3 wants a length; local rename wants a file anyway). The same pass
	// hashes the bytes for the duplicate check below.
	tmp, err := os.CreateTemp(s.cfg.StoragePath, ".incoming-*")
	if err != nil {
		log.Printf("Error: %v", err)
		return store.Photo{}, false, &uploadError{http.StatusInternalServerError, "Internal Server Error"}
	}
	defer os.Remove(tmp.Name())
	defer tmp.Close()

	hasher := checksum.New()
	size, err := io.Copy(io.MultiWriter(tmp, hasher),
		io.MultiReader(bytes.NewReader(head), io.LimitReader(part, maxFile+1)))
	if err != nil {
		log.Printf("Error: %v", err)
		return store.Photo{}, false, &uploadError{http.StatusInternalServerError, "Internal Server Error"}
	}
	if size > maxFile {
		return store.Photo{}, false, &uploadError{http.StatusRequestEntityTooLarge,
			fmt.Sprintf("File exceeds the %d MB limit", s.cfg.MaxUploadMB)}
	}
	if _, err := tmp.Seek(0, io.SeekStart); err != nil {
		log.Printf("Error: %v", err)
		return store.Photo{}, false, &uploadError{http.StatusInternalServerError, "Internal Server Error"}
	}
	sum := checksum.Format(hasher)

	// Duplicate check before anything is written: the temp copy is dropped by
	// the deferred Remove, no object is stored and no row is inserted.
	if existing, err := s.db.FindPhotoByChecksum(r.Context(), g.ID, sum); err == nil {
		return existing, true, nil
	} else if err != store.ErrNotFound {
		log.Printf("Error: %v", err)
		return store.Photo{}, false, &uploadError{http.StatusInternalServerError, "Internal Server Error"}
	}

	photoID := newID()
	key := fmt.Sprintf("galleries/%s/orig/%s%s", g.ID, photoID, ext)
	if err := s.storage.Put(r.Context(), key, tmp, size, contentType); err != nil {
		log.Printf("Error: %v", err)
		return store.Photo{}, false, &uploadError{http.StatusInternalServerError, "Internal Server Error"}
	}

	now := time.Now()
	photo := store.Photo{
		ID:               photoID,
		GalleryID:        g.ID,
		Position:         position,
		OriginalKey:      key,
		OriginalFilename: sanitizeFilename(part.FileName()),
		ContentType:      contentType,
		SizeBytes:        size,
		Status:           "queued",
		NextAttemptAt:    now,
		CreatedAt:        now,
		UpdatedAt:        now,
		Checksum:         sum,
	}
	if err := s.db.InsertPhoto(r.Context(), photo); err != nil {
		s.storage.Delete(r.Context(), key)
		// A concurrent upload of the same bytes won the race between the
		// check above and this insert; the unique index caught it, so report
		// the winner as the duplicate instead of failing.
		if store.IsUniqueViolation(err) {
			if existing, findErr := s.db.FindPhotoByChecksum(r.Context(), g.ID, sum); findErr == nil {
				return existing, true, nil
			}
		}
		log.Printf("Error: %v", err)
		return store.Photo{}, false, &uploadError{http.StatusInternalServerError, "Internal Server Error"}
	}
	return photo, false, nil
}

type reorderBody struct {
	PhotoIDs []string `json:"photoIds"`
}

func (s *Server) reorderPhotos(w http.ResponseWriter, r *http.Request, _ auth.User) {
	galleryID := r.PathValue("id")
	var body reorderBody
	if err := decodeJSON(r, &body); err != nil {
		writeError(w, http.StatusBadRequest, "Invalid JSON body")
		return
	}
	existing, err := s.db.ListPhotos(r.Context(), galleryID, false)
	if err != nil {
		writeStoreError(w, err, "")
		return
	}
	if len(existing) == 0 {
		writeStoreError(w, store.ErrNotFound, "Gallery not found or empty")
		return
	}
	current := map[string]bool{}
	for _, p := range existing {
		current[p.ID] = true
	}
	if len(body.PhotoIDs) != len(existing) {
		writeError(w, http.StatusBadRequest, "photoIds must contain every photo of the gallery exactly once")
		return
	}
	seen := map[string]bool{}
	for _, id := range body.PhotoIDs {
		if !current[id] || seen[id] {
			writeError(w, http.StatusBadRequest, "photoIds must contain every photo of the gallery exactly once")
			return
		}
		seen[id] = true
	}
	if err := s.db.ReorderPhotos(r.Context(), galleryID, body.PhotoIDs); err != nil {
		writeStoreError(w, err, "")
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"message": "Order updated"})
}

func (s *Server) deletePhoto(w http.ResponseWriter, r *http.Request, _ auth.User) {
	p, err := s.db.GetPhoto(r.Context(), r.PathValue("photoId"))
	if err != nil {
		writeStoreError(w, err, "Photo not found")
		return
	}
	if err := s.db.DeletePhoto(r.Context(), p.ID); err != nil {
		writeStoreError(w, err, "Photo not found")
		return
	}
	for _, key := range []string{p.OriginalKey, p.ThumbKey, p.PreviewKey} {
		if key == "" {
			continue
		}
		if err := s.storage.Delete(r.Context(), key); err != nil {
			log.Printf("delete object %s: %v", key, err)
		}
	}
	writeJSON(w, http.StatusOK, map[string]string{"message": "Photo deleted"})
}

func (s *Server) retryPhoto(w http.ResponseWriter, r *http.Request, _ auth.User) {
	id := r.PathValue("photoId")
	if err := s.db.RequeuePhoto(r.Context(), id); err != nil {
		writeStoreError(w, err, "Photo not found or not failed")
		return
	}
	s.worker.Nudge()
	p, err := s.db.GetPhoto(r.Context(), id)
	if err != nil {
		writeStoreError(w, err, "Photo not found")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"photo": s.photoToJSON(p, "", true)})
}

func sanitizeFilename(name string) string {
	name = filepath.Base(name)
	if name == "." || name == "/" {
		return ""
	}
	if len(name) > 200 {
		name = name[len(name)-200:]
	}
	return name
}
