package api

import (
	"encoding/json"
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"sort"
	"strings"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/shared/mail"
)

const (
	maxBodyBytes = 32 << 10
	perIPLimit   = 5
	perIPWindow  = 10 * time.Minute
)

type Server struct {
	mail    mail.Sender
	contact string // where enquiries go; the sender itself has no default recipient
	limiter *limiter

	allowOrigin map[string]bool
	trustProxy  bool
}

func New(sender mail.Sender, contactTo string) *Server {
	return &Server{
		mail:        sender,
		contact:     contactTo,
		limiter:     newLimiter(perIPLimit, perIPWindow),
		allowOrigin: parseOrigins(os.Getenv("SITE_ORIGIN")),
		trustProxy:  os.Getenv("TRUST_PROXY") == "true",
	}
}

func (s *Server) Routes() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("POST /api/contact", s.handleContact)
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
	})
	return s.withCORS(mux)
}

func parseOrigins(raw string) map[string]bool {
	out := map[string]bool{}
	for _, o := range strings.Split(raw, ",") {
		if o = strings.TrimSpace(o); o != "" {
			out[o] = true
		}
	}
	return out
}

func (s *Server) withCORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		origin := r.Header.Get("Origin")
		if origin != "" && s.allowOrigin[origin] {
			w.Header().Set("Access-Control-Allow-Origin", origin)
			w.Header().Set("Vary", "Origin")
			w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
			w.Header().Set("Access-Control-Max-Age", "600")
		}
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func (s *Server) clientIP(r *http.Request) string {
	if s.trustProxy {
		if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
			if first, _, ok := strings.Cut(xff, ","); ok {
				return strings.TrimSpace(first)
			}
			return strings.TrimSpace(xff)
		}
	}
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		return r.RemoteAddr
	}
	return host
}

type contactBody struct {
	Name    string `json:"name"`
	Email   string `json:"email"`
	Servers string `json:"servers"`
	Topic   string `json:"topic"`
	Message string `json:"message"`
	Website string `json:"website"`
}

var (
	serverBands = []string{"1–3", "4–25", "26–100", "More than 100"}
	topics      = []string{
		"Evaluating Vantage",
		"Self-hosted licensing",
		"Migrating from something else",
		"Security disclosure",
	}
)

func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) {
	var body contactBody
	if !decode(w, r, &body) {
		return
	}

	if strings.TrimSpace(body.Website) != "" {
		writeJSON(w, http.StatusAccepted, map[string]string{"status": "received"})
		return
	}

	fields := map[string]string{}
	var problems []fieldError

	if v, err := text("name", body.Name, true, maxShort); err != nil {
		problems = append(problems, *err)
	} else {
		fields["name"] = v
	}

	if v, err := text("message", body.Message, true, maxLong); err != nil {
		problems = append(problems, *err)
	} else {
		fields["message"] = v
	}

	addr, emailErr := email("email", body.Email)
	if emailErr != nil {
		problems = append(problems, *emailErr)
	}

	if v, err := oneOf("servers", body.Servers, serverBands); err != nil {
		problems = append(problems, *err)
	} else {
		fields["servers"] = v
	}

	if v, err := oneOf("topic", body.Topic, topics); err != nil {
		problems = append(problems, *err)
	} else {
		fields["topic"] = v
	}

	if len(problems) > 0 {
		sort.Slice(problems, func(i, j int) bool { return problems[i].Field < problems[j].Field })
		writeJSON(w, http.StatusUnprocessableEntity, map[string]any{
			"error":  "Some fields need another look.",
			"fields": problems,
		})
		return
	}

	if !s.limiter.allow(s.clientIP(r)) {
		w.Header().Set("Retry-After", fmt.Sprintf("%d", int(perIPWindow.Seconds())))
		writeJSON(w, http.StatusTooManyRequests, map[string]string{
			"error": "That is a lot of messages in a short time. Try again shortly.",
		})
		return
	}

	if !s.mail.Enabled() || s.contact == "" {
		log.Println("contact submission dropped: smtp is not configured")
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "The contact form is unavailable right now. Email support@hostxtra.co.uk directly.",
		})
		return
	}

	if err := s.mail.SendEnquiry(s.contact, mail.Enquiry{
		Name:    fields["name"],
		Email:   addr,
		Servers: fields["servers"],
		Topic:   fields["topic"],
		Message: fields["message"],
	}); err != nil {
		log.Printf("contact send: %v", err)
		writeJSON(w, http.StatusBadGateway, map[string]string{
			"error": "We could not send that. Try again, or email support@hostxtra.co.uk directly.",
		})
		return
	}

	writeJSON(w, http.StatusAccepted, map[string]string{"status": "received"})
}

func decode(w http.ResponseWriter, r *http.Request, dst any) bool {
	r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	if err := dec.Decode(dst); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "We could not read that submission.",
		})
		return false
	}
	return true
}

func writeJSON(w http.ResponseWriter, status int, payload any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.Header().Set("X-Content-Type-Options", "nosniff")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(payload); err != nil {
		log.Printf("write response: %v", err)
	}
}
