package handler

import (
	"html/template"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"

	"github.com/warmbly/warmbly/internal/app/unsublink"
	"github.com/warmbly/warmbly/internal/errx"
)

// The recipient-facing unsubscribe endpoints. PUBLIC and unauthenticated by
// design: the only credential is the signed token in the path, minted per
// recipient when the email was sent.
//
//   GET  /unsubscribe/:token              a click on the link: a confirm page
//   POST /unsubscribe/:token              the confirm button, or the mail
//                                         client's RFC 8058 one-click POST
//                                         (body List-Unsubscribe=One-Click),
//                                         which suppresses with no page
//   POST /unsubscribe/:token/resubscribe  the "unsubscribed by mistake" button
//
// A GET never changes anything, because link scanners and preview fetchers
// follow every link in an email; only a POST suppresses.

func (h *Handler) UnsubscribePage(c *gin.Context) {
	claims, ok := h.unsubscribeClaims(c)
	if !ok {
		return
	}
	if claims.ContactID == uuid.Nil {
		renderUnsubPage(c, http.StatusOK, unsubView{Title: "This was a test email", Body: "Test sends carry a link that is not tied to anyone, so there is nothing to unsubscribe."})
		return
	}
	renderUnsubPage(c, http.StatusOK, unsubView{
		Title:   "Unsubscribe from these emails?",
		Body:    "Confirm and you will not receive further emails from this sender.",
		Confirm: c.Request.URL.Path,
	})
}

// unsubscribeBodyLimit caps the public POST bodies. The engine-wide limit is
// registered after these routes, so it does not cover them; a one-click or
// confirm body is a few bytes.
const unsubscribeBodyLimit = 16 << 10

func (h *Handler) UnsubscribeSubmit(c *gin.Context) {
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, unsubscribeBodyLimit)
	oneClick := strings.EqualFold(strings.TrimSpace(c.PostForm("List-Unsubscribe")), "One-Click")
	confirmed := c.PostForm("confirm") == "1"

	claims, err := h.verifyUnsubscribeToken(c.Param("token"))
	if err != nil {
		if oneClick {
			// RFC 8058: a bad or expired link is terminal, so 200 stops the
			// provider retrying; only a genuine server failure gets a 5xx.
			c.Status(http.StatusOK)
			return
		}
		renderUnsubPage(c, http.StatusBadRequest, unsubInvalid(err))
		return
	}
	if claims.ContactID == uuid.Nil {
		if oneClick {
			c.Status(http.StatusOK)
			return
		}
		renderUnsubPage(c, http.StatusOK, unsubView{Title: "This was a test email", Body: "Test sends carry a link that is not tied to anyone, so there is nothing to unsubscribe."})
		return
	}

	// A browser POST without the confirm field is not the button: show the
	// confirm page again rather than act on it.
	if !oneClick && !confirmed {
		renderUnsubPage(c, http.StatusOK, unsubView{
			Title:   "Unsubscribe from these emails?",
			Body:    "Confirm and you will not receive further emails from this sender.",
			Confirm: c.Request.URL.Path,
		})
		return
	}

	via := "link"
	if oneClick {
		via = "one_click"
	}
	xerr := h.AdvancedService.UnsubscribeFromLink(c.Request.Context(), claims.OrgID, claims.CampaignID, claims.ContactID, via)

	if oneClick {
		if xerr != nil && xerr.Code != errx.BadRequest {
			c.Status(http.StatusBadGateway)
			return
		}
		c.Status(http.StatusOK)
		return
	}
	if xerr != nil {
		renderUnsubPage(c, http.StatusOK, unsubView{Title: "We couldn't process that link", Body: "The link is no longer valid. Reply to the email instead and the sender will stop."})
		return
	}
	renderUnsubPage(c, http.StatusOK, unsubView{
		Title:       "You've been unsubscribed",
		Body:        "You will not receive further emails from this sender.",
		Resubscribe: c.Request.URL.Path + "/resubscribe",
	})
}

func (h *Handler) UnsubscribeUndo(c *gin.Context) {
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, unsubscribeBodyLimit)
	claims, ok := h.unsubscribeClaims(c)
	if !ok {
		return
	}
	if claims.ContactID == uuid.Nil {
		renderUnsubPage(c, http.StatusBadRequest, unsubInvalid(unsublink.ErrInvalid))
		return
	}
	if xerr := h.AdvancedService.Resubscribe(c.Request.Context(), claims.OrgID, claims.ContactID); xerr != nil {
		renderUnsubPage(c, http.StatusOK, unsubView{Title: "We couldn't process that link", Body: "The link is no longer valid. Reply to the email and the sender can add you back."})
		return
	}
	renderUnsubPage(c, http.StatusOK, unsubView{Title: "You're subscribed again", Body: "The sender can email you as before. You can unsubscribe from any later email."})
}

func (h *Handler) verifyUnsubscribeToken(token string) (unsublink.Claims, error) {
	if h.UnsubscribeLinks == nil {
		return unsublink.Claims{}, unsublink.ErrInvalid
	}
	return h.UnsubscribeLinks.Verify(token, time.Now())
}

func (h *Handler) unsubscribeClaims(c *gin.Context) (unsublink.Claims, bool) {
	claims, err := h.verifyUnsubscribeToken(c.Param("token"))
	if err != nil {
		renderUnsubPage(c, http.StatusBadRequest, unsubInvalid(err))
		return claims, false
	}
	return claims, true
}

func unsubInvalid(err error) unsubView {
	if err == unsublink.ErrExpired {
		return unsubView{Title: "This link has expired", Body: "Reply to the email instead and the sender will stop."}
	}
	return unsubView{Title: "This unsubscribe link is invalid", Body: "Reply to the email instead and the sender will stop."}
}

type unsubView struct {
	Title       string
	Body        string
	Confirm     string // POST target of the confirm button, when shown
	Resubscribe string // POST target of the resubscribe button, when shown
}

// A neutral page: the email came from the customer's mailbox, so the page
// names no brand and carries no scripts or external assets.
var unsubTemplate = template.Must(template.New("unsubscribe").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>{{.Title}}</title>
<style>body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;max-width:32rem;margin:4rem auto;padding:0 1.25rem;color:#0f172a;line-height:1.5}
h1{font-size:1.25rem;margin:0 0 .5rem}p{color:#475569;margin:0 0 1.25rem}
button{font:inherit;padding:.55rem 1rem;border-radius:.375rem;border:1px solid #0284c7;background:#0284c7;color:#fff;cursor:pointer}
button.secondary{background:#fff;color:#0f172a;border-color:#cbd5e1}</style></head>
<body><h1>{{.Title}}</h1><p>{{.Body}}</p>
{{if .Confirm}}<form method="post" action="{{.Confirm}}"><input type="hidden" name="confirm" value="1"><button type="submit">Unsubscribe</button></form>{{end}}
{{if .Resubscribe}}<form method="post" action="{{.Resubscribe}}"><button type="submit" class="secondary">Unsubscribed by mistake? Resubscribe</button></form>{{end}}
</body></html>`))

func renderUnsubPage(c *gin.Context, status int, v unsubView) {
	c.Header("Cache-Control", "no-store")
	c.Header("X-Robots-Tag", "noindex")
	c.Status(status)
	c.Header("Content-Type", "text/html; charset=utf-8")
	_ = unsubTemplate.Execute(c.Writer, v)
}
