package api

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"wedding-server/internal/config"
	"wedding-server/internal/mail"
	"wedding-server/internal/store"
	"wedding-server/internal/webhook"
)

type Server struct {
	cfg     *config.Config
	store   *store.Store
	mailer  *mail.Mailer
	webhook *webhook.Client
}

func New(cfg *config.Config, st *store.Store, mailer *mail.Mailer, wh *webhook.Client) *Server {
	return &Server{cfg: cfg, store: st, mailer: mailer, webhook: wh}
}

func (s *Server) Routes(mux *http.ServeMux) {
	mux.HandleFunc("GET /healthz", s.handleHealthz)
	mux.HandleFunc("GET /api/config", s.handleConfig)
	mux.HandleFunc("POST /api/rsvp", s.handleRSVP)
	mux.HandleFunc("GET /api/guests", s.handleGuestList)
	mux.HandleFunc("POST /api/photos/upload", s.handlePhotoUpload)
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, message string) {
	writeJSON(w, status, map[string]string{"error": message})
}

func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
}

func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, map[string]string{
		"siteUrl":      s.cfg.Site.URL,
		"weddingDate":  s.cfg.Wedding.Date.Format(time.RFC3339),
		"ceremonyTime": s.cfg.Wedding.CeremonyTime,
		"rsvpByDate":   s.cfg.Wedding.RSVPBy.Format(time.RFC3339),
		"venueName":    s.cfg.Wedding.VenueName,
		"venueAddress": s.cfg.Wedding.VenueAddress,
		"rsvpPhone":    s.cfg.Wedding.RSVPPhone,
	})
}

type guestRequest struct {
	Name string `json:"name"`
	Type string `json:"type"`
}

type rsvpRequest struct {
	Guests  []guestRequest `json:"guests"`
	Message string         `json:"message"`
}

func (s *Server) handleRSVP(w http.ResponseWriter, r *http.Request) {
	var req rsvpRequest
	if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "invalid request body")
		return
	}

	var guests []store.Guest
	for _, g := range req.Guests {
		name := strings.TrimSpace(g.Name)
		if name != "" {
			guests = append(guests, store.Guest{Name: name, IsChild: g.Type == "child"})
		}
	}
	if len(guests) == 0 {
		writeError(w, http.StatusBadRequest, "at least one name is required")
		return
	}

	rsvp := store.RSVP{
		Guests:  guests,
		Message: strings.TrimSpace(req.Message),
	}
	if err := s.store.InsertRSVP(rsvp); err != nil {
		log.Printf("rsvp: insert failed: %v", err)
		writeError(w, http.StatusInternalServerError, "could not save RSVP")
		return
	}

	if err := s.mailer.SendRSVP(rsvp); err != nil {
		log.Printf("rsvp: email failed: %v", err)
	}
	if err := s.webhook.SendRSVP(rsvp); err != nil {
		log.Printf("rsvp: webhook failed: %v", err)
	}

	writeJSON(w, http.StatusCreated, map[string]bool{"ok": true})
}

type guestResponse struct {
	Name    string `json:"name"`
	IsChild bool   `json:"isChild"`
}

func (s *Server) handleGuestList(w http.ResponseWriter, r *http.Request) {
	guests, err := s.store.ListGuests()
	if err != nil {
		log.Printf("guests: list failed: %v", err)
		writeError(w, http.StatusInternalServerError, "could not load guests")
		return
	}

	out := make([]guestResponse, 0, len(guests))
	adults, children := 0, 0
	for _, g := range guests {
		out = append(out, guestResponse{Name: g.Name, IsChild: g.IsChild})
		if g.IsChild {
			children++
		} else {
			adults++
		}
	}

	writeJSON(w, http.StatusOK, map[string]any{
		"guests":   out,
		"total":    len(out),
		"adults":   adults,
		"children": children,
	})
}

var allowedImageTypes = map[string]bool{
	"image/jpeg": true,
	"image/png":  true,
	"image/webp": true,
	"image/heic": true,
	"image/heif": true,
}

func (s *Server) handlePhotoUpload(w http.ResponseWriter, r *http.Request) {
	if time.Now().Before(s.cfg.Wedding.Date) {
		writeError(w, http.StatusForbidden, "photo uploads open on the wedding day")
		return
	}

	maxTotalBytes := int64(s.cfg.Upload.MaxSizeMB) * int64(s.cfg.Upload.MaxFiles) * 1024 * 1024
	r.Body = http.MaxBytesReader(w, r.Body, maxTotalBytes+1<<20)

	if err := r.ParseMultipartForm(10 << 20); err != nil {
		writeError(w, http.StatusBadRequest, "invalid upload (too large or malformed)")
		return
	}
	defer r.MultipartForm.RemoveAll()

	name := strings.TrimSpace(r.FormValue("name"))
	if name == "" {
		writeError(w, http.StatusBadRequest, "name is required")
		return
	}
	message := strings.TrimSpace(r.FormValue("message"))
	email := strings.TrimSpace(r.FormValue("email"))
	phone := strings.TrimSpace(r.FormValue("phone"))

	headers := r.MultipartForm.File["files"]
	if len(headers) == 0 {
		writeError(w, http.StatusBadRequest, "at least one photo is required")
		return
	}
	if len(headers) > s.cfg.Upload.MaxFiles {
		writeError(w, http.StatusBadRequest, fmt.Sprintf("at most %d photos per submission", s.cfg.Upload.MaxFiles))
		return
	}

	maxFileBytes := int64(s.cfg.Upload.MaxSizeMB) * 1024 * 1024
	for _, fh := range headers {
		if fh.Size > maxFileBytes {
			writeError(w, http.StatusBadRequest, fmt.Sprintf("%q is larger than %dMB", fh.Filename, s.cfg.Upload.MaxSizeMB))
			return
		}
	}

	// Sniff and validate every file's actual content before creating any
	// database row or directory, so a rejected upload leaves no trace.
	for _, fh := range headers {
		file, err := fh.Open()
		if err != nil {
			writeError(w, http.StatusBadRequest, "could not read uploaded file")
			return
		}
		sniff := make([]byte, 512)
		n, _ := io.ReadFull(file, sniff)
		file.Close()
		sniff = sniff[:n]

		contentType := http.DetectContentType(sniff)
		if contentType == "application/octet-stream" && isHEIC(sniff) {
			// net/http's sniffer table has no ISOBMFF/HEIC signature.
			contentType = "image/heic"
		}
		if !allowedImageTypes[contentType] {
			writeError(w, http.StatusBadRequest, fmt.Sprintf("%q is not a supported image type", fh.Filename))
			return
		}
	}

	uploadID, err := s.store.CreatePhotoUpload(store.PhotoUpload{
		Name: name, Message: message, Email: email, Phone: phone,
	})
	if err != nil {
		log.Printf("photos: create upload failed: %v", err)
		writeError(w, http.StatusInternalServerError, "could not save upload")
		return
	}

	uploadDir := filepath.Join(s.cfg.Storage.DataDir, "photos", strconv.FormatInt(uploadID, 10))
	if err := os.MkdirAll(uploadDir, 0o755); err != nil {
		log.Printf("photos: mkdir failed: %v", err)
		writeError(w, http.StatusInternalServerError, "could not save upload")
		return
	}

	var saved []store.PhotoFile
	for _, fh := range headers {
		file, err := fh.Open()
		if err != nil {
			writeError(w, http.StatusBadRequest, "could not read uploaded file")
			return
		}

		storedName := randomHex(8) + "_" + filepath.Base(fh.Filename)
		destPath := filepath.Join(uploadDir, storedName)
		dest, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
		if err != nil {
			file.Close()
			log.Printf("photos: create file failed: %v", err)
			writeError(w, http.StatusInternalServerError, "could not save upload")
			return
		}

		_, err = io.Copy(dest, file)
		dest.Close()
		file.Close()
		if err != nil {
			log.Printf("photos: write file failed: %v", err)
			writeError(w, http.StatusInternalServerError, "could not save upload")
			return
		}

		saved = append(saved, store.PhotoFile{
			Filename: fh.Filename,
			Path:     destPath,
			URL:      fmt.Sprintf("/uploads/%d/%s", uploadID, storedName),
		})
	}

	if err := s.store.AddPhotoFiles(uploadID, saved); err != nil {
		log.Printf("photos: add files failed: %v", err)
		writeError(w, http.StatusInternalServerError, "could not save upload")
		return
	}

	upload := store.PhotoUpload{Name: name, Message: message, Email: email, Phone: phone}
	if err := s.mailer.SendPhotoUpload(upload, saved); err != nil {
		log.Printf("photos: email failed: %v", err)
	}
	if err := s.webhook.SendPhotoUpload(upload, saved); err != nil {
		log.Printf("photos: webhook failed: %v", err)
	}

	writeJSON(w, http.StatusCreated, map[string]bool{"ok": true})
}

// isHEIC checks for the ISOBMFF "ftyp" box and a HEIC/HEIF brand, since
// net/http.DetectContentType does not recognize this container format.
func isHEIC(b []byte) bool {
	if len(b) < 12 || string(b[4:8]) != "ftyp" {
		return false
	}
	switch string(b[8:12]) {
	case "heic", "heix", "hevc", "hevx", "heim", "heis", "hevm", "hevs", "mif1", "msf1":
		return true
	default:
		return false
	}
}

func randomHex(n int) string {
	b := make([]byte, n)
	_, _ = rand.Read(b)
	return hex.EncodeToString(b)
}
