package handler

import (
	"bytes"
	"fmt"
	"image"
	"io"
	"net/http"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/warmbly/warmbly/internal/api/middleware"
	"github.com/warmbly/warmbly/internal/config"
	"github.com/warmbly/warmbly/internal/errx"
	"github.com/warmbly/warmbly/internal/models"
)

// formScope pulls the org and the :id path param once per handler.
func formScope(c *gin.Context) (uuid.UUID, uuid.UUID, bool) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.Handle(c, errx.New(errx.BadRequest, "no organization selected"))
		return uuid.Nil, uuid.Nil, false
	}
	raw := c.Param("id")
	if raw == "" {
		return *orgID, uuid.Nil, true
	}
	id, err := uuid.Parse(raw)
	if err != nil {
		errx.Handle(c, errx.New(errx.BadRequest, "invalid form id"))
		return uuid.Nil, uuid.Nil, false
	}
	return *orgID, id, true
}

// withFormShareURL stamps the hosted page URL onto the response, on the
// organization's verified custom forms domain when it has one.
func (h *Handler) withFormShareURL(c *gin.Context, orgID uuid.UUID, f *models.Form) *models.Form {
	f.ShareURL = config.FormURLOn(h.FormService.FormsHost(c.Request.Context(), orgID), f.PublicID)
	return f
}

func (h *Handler) ListForms(c *gin.Context) {
	orgID, _, ok := formScope(c)
	if !ok {
		return
	}
	out, xerr := h.FormService.List(c.Request.Context(), orgID)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	host := h.FormService.FormsHost(c.Request.Context(), orgID)
	for i := range out {
		out[i].ShareURL = config.FormURLOn(host, out[i].PublicID)
	}
	c.JSON(http.StatusOK, gin.H{"data": out})
}

// GetFormsConfig tells the builder what this instance can do, so the UI
// never shows a captcha toggle that cannot work.
func (h *Handler) GetFormsConfig(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"base_url":          config.FormsBaseURL(),
		"captcha_available": config.CaptchaProvider() != "none" && config.TurnstileSiteKey() != "",
	})
}

func (h *Handler) GetForm(c *gin.Context) {
	orgID, id, ok := formScope(c)
	if !ok {
		return
	}
	out, xerr := h.FormService.Get(c.Request.Context(), orgID, id)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	c.JSON(http.StatusOK, h.withFormShareURL(c, orgID, out))
}

func (h *Handler) CreateForm(c *gin.Context) {
	orgID, _, ok := formScope(c)
	if !ok {
		return
	}
	var in struct {
		Name string `json:"name"`
	}
	if err := c.ShouldBindJSON(&in); err != nil {
		errx.Handle(c, errx.ErrInvalid)
		return
	}
	var createdBy *uuid.UUID
	if uid, err := middleware.GetUserUUID(c); err == nil {
		createdBy = &uid
	}
	out, xerr := h.FormService.Create(c.Request.Context(), orgID, createdBy, in.Name)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	h.auditOrg(c, models.AuditActionCreate, models.AuditEntityForm, &out.ID, nil, map[string]string{"name": out.Name})
	c.JSON(http.StatusCreated, h.withFormShareURL(c, orgID, out))
}

func (h *Handler) UpdateForm(c *gin.Context) {
	orgID, id, ok := formScope(c)
	if !ok {
		return
	}
	var in models.FormWrite
	if err := c.ShouldBindJSON(&in); err != nil {
		errx.Handle(c, errx.ErrInvalid)
		return
	}
	out, xerr := h.FormService.Update(c.Request.Context(), orgID, id, &in)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityForm, &out.ID, nil, map[string]string{"name": out.Name, "status": string(out.Status)})
	c.JSON(http.StatusOK, h.withFormShareURL(c, orgID, out))
}

func (h *Handler) DeleteForm(c *gin.Context) {
	orgID, id, ok := formScope(c)
	if !ok {
		return
	}
	if xerr := h.FormService.Delete(c.Request.Context(), orgID, id); xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	h.auditOrg(c, models.AuditActionDelete, models.AuditEntityForm, &id, nil, nil)
	c.Status(http.StatusNoContent)
}

// GetFormStats serves the analytics tab: funnel totals, daily series,
// per-page drop-off, breakdowns and identified visitors.
func (h *Handler) GetFormStats(c *gin.Context) {
	orgID, id, ok := formScope(c)
	if !ok {
		return
	}
	days := 30
	switch c.Query("range") {
	case "", "30d":
	case "7d":
		days = 7
	case "90d":
		days = 90
	default:
		errx.Handle(c, errx.New(errx.BadRequest, "range must be 7d, 30d or 90d"))
		return
	}
	out, xerr := h.FormService.Stats(c.Request.Context(), orgID, id, days)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	c.JSON(http.StatusOK, out)
}

// GetCampaignForms reports how the forms linked from a campaign performed for
// that campaign's recipients. It hangs off the campaign group, so the id
// param is a campaign, not a form.
func (h *Handler) GetCampaignForms(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.Handle(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	campaignID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.Handle(c, errx.New(errx.BadRequest, "invalid campaign id"))
		return
	}
	out, xerr := h.FormService.CampaignForms(c.Request.Context(), *orgID, campaignID)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	c.JSON(http.StatusOK, gin.H{"data": out})
}

// GetFormsDomain, SetFormsDomain and VerifyFormsDomain manage the
// organization's custom forms domain. Reading is a stored-state lookup; the
// other two resolve DNS, which is why they are not on the read path.
func (h *Handler) GetFormsDomain(c *gin.Context) {
	orgID, _, ok := formScope(c)
	if !ok {
		return
	}
	out, xerr := h.FormService.FormsDomainStatus(c.Request.Context(), orgID)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	c.JSON(http.StatusOK, out)
}

func (h *Handler) SetFormsDomain(c *gin.Context) {
	orgID, _, ok := formScope(c)
	if !ok {
		return
	}
	var in struct {
		FormsDomain string `json:"forms_domain"`
	}
	if err := c.ShouldBindJSON(&in); err != nil {
		errx.Handle(c, errx.ErrInvalid)
		return
	}
	out, xerr := h.FormService.SetFormsDomain(c.Request.Context(), orgID, in.FormsDomain)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	h.auditOrg(c, models.AuditActionUpdate, models.AuditEntitySettings, nil, nil,
		map[string]string{"forms_domain": out.FormsDomain, "verified": strconv.FormatBool(out.FormsDomainVerified)})
	c.JSON(http.StatusOK, out)
}

func (h *Handler) VerifyFormsDomain(c *gin.Context) {
	orgID, _, ok := formScope(c)
	if !ok {
		return
	}
	out, xerr := h.FormService.VerifyFormsDomain(c.Request.Context(), orgID)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	c.JSON(http.StatusOK, out)
}

// MintFormLink returns the personalized URL for one contact. GET on top of
// an upsert: retries always land on the same token, so it is naturally safe.
func (h *Handler) MintFormLink(c *gin.Context) {
	orgID, id, ok := formScope(c)
	if !ok {
		return
	}
	contactID, err := uuid.Parse(c.Param("contactID"))
	if err != nil {
		errx.Handle(c, errx.New(errx.BadRequest, "invalid contact id"))
		return
	}
	url, xerr := h.FormService.MintLink(c.Request.Context(), orgID, id, contactID)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	c.JSON(http.StatusOK, gin.H{"url": url})
}

// Form image caps: logos stay small, covers may be a full panel.
const (
	formLogoMaxBytes  int64 = 1 * 1024 * 1024
	formLogoMaxDim          = 1024
	formCoverMaxBytes int64 = 4 * 1024 * 1024
	formCoverMaxDim         = 2560
)

// readFormImageUpload mirrors readAvatarUpload with per-kind caps and copy;
// the MIME allowlist (PNG/JPG only) is shared for the same CVE reasons.
func readFormImageUpload(c *gin.Context, kind string) (body []byte, mime, ext string, xerr *errx.Error) {
	maxBytes, maxDim := formLogoMaxBytes, formLogoMaxDim
	// Covers and page backgrounds are both full-bleed art, so they share caps.
	if kind == "cover" || kind == "background" {
		maxBytes, maxDim = formCoverMaxBytes, formCoverMaxDim
	}
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxBytes+1024)
	fh, err := c.FormFile("file")
	if err != nil {
		return nil, "", "", errx.New(errx.BadRequest, "file is required")
	}
	if fh.Size > maxBytes {
		return nil, "", "", errx.New(errx.BadRequest, fmt.Sprintf("the %s must be smaller than %d MB", kind, maxBytes>>20))
	}
	src, err := fh.Open()
	if err != nil {
		return nil, "", "", errx.InternalError()
	}
	defer src.Close()
	buf := &bytes.Buffer{}
	if _, err := io.Copy(buf, src); err != nil {
		return nil, "", "", errx.InternalError()
	}
	body = buf.Bytes()
	mime = http.DetectContentType(body)
	var ok bool
	if ext, ok = allowedAvatarMIME[mime]; !ok {
		return nil, "", "", errx.New(errx.BadRequest, fmt.Sprintf("the %s must be a PNG or JPG", kind))
	}
	cfg, _, err := image.DecodeConfig(bytes.NewReader(body))
	if err != nil {
		return nil, "", "", errx.New(errx.BadRequest, "image could not be parsed")
	}
	if cfg.Width > maxDim || cfg.Height > maxDim {
		return nil, "", "", errx.New(errx.BadRequest, fmt.Sprintf("the %s must be %dpx or smaller", kind, maxDim))
	}
	return body, mime, ext, nil
}

func formAssetKind(c *gin.Context) (string, bool) {
	kind := c.Param("kind")
	if kind != "logo" && kind != "cover" && kind != "background" {
		errx.Handle(c, errx.New(errx.BadRequest, "kind must be logo, cover or background"))
		return "", false
	}
	return kind, true
}

// UploadFormAsset — POST /forms/:id/assets/:kind (multipart, field "file").
func (h *Handler) UploadFormAsset(c *gin.Context) {
	orgID, id, ok := formScope(c)
	if !ok {
		return
	}
	kind, ok := formAssetKind(c)
	if !ok {
		return
	}
	body, mime, ext, xerr := readFormImageUpload(c, kind)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	key := fmt.Sprintf("form-assets/%s/%s-%s-%d%s", orgID, id, kind, time.Now().Unix(), ext)
	url, xerr := putPublicObject(c.Request.Context(), h.Storage, key, body, mime)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	out, xerr := h.FormService.SetAsset(c.Request.Context(), orgID, id, kind, url)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityForm, &id, nil, map[string]string{"asset": kind})
	c.JSON(http.StatusOK, h.withFormShareURL(c, orgID, out))
}

// DeleteFormAsset — DELETE /forms/:id/assets/:kind.
func (h *Handler) DeleteFormAsset(c *gin.Context) {
	orgID, id, ok := formScope(c)
	if !ok {
		return
	}
	kind, ok := formAssetKind(c)
	if !ok {
		return
	}
	out, xerr := h.FormService.SetAsset(c.Request.Context(), orgID, id, kind, "")
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityForm, &id, nil, map[string]string{"asset": kind, "removed": "true"})
	c.JSON(http.StatusOK, h.withFormShareURL(c, orgID, out))
}

func (h *Handler) ListFormSubmissions(c *gin.Context) {
	orgID, id, ok := formScope(c)
	if !ok {
		return
	}
	limit := 50
	if raw := c.Query("limit"); raw != "" {
		n, err := strconv.Atoi(raw)
		if err != nil || n < 1 || n > 100 {
			errx.Handle(c, errx.New(errx.BadRequest, "limit must be between 1 and 100"))
			return
		}
		limit = n
	}
	var before *time.Time
	if raw := c.Query("before"); raw != "" {
		t, err := time.Parse(time.RFC3339Nano, raw)
		if err != nil {
			errx.Handle(c, errx.New(errx.BadRequest, "before must be an RFC 3339 timestamp"))
			return
		}
		before = &t
	}
	out, hasMore, xerr := h.FormService.ListSubmissions(c.Request.Context(), orgID, id, limit, before)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	c.JSON(http.StatusOK, gin.H{"data": out, "has_more": hasMore})
}

func (h *Handler) DeleteFormSubmission(c *gin.Context) {
	orgID, id, ok := formScope(c)
	if !ok {
		return
	}
	subID, err := uuid.Parse(c.Param("sid"))
	if err != nil {
		errx.Handle(c, errx.New(errx.BadRequest, "invalid submission id"))
		return
	}
	if xerr := h.FormService.DeleteSubmission(c.Request.Context(), orgID, id, subID); xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	h.auditOrg(c, models.AuditActionDelete, models.AuditEntityForm, &id, nil, map[string]string{"submission_id": subID.String()})
	c.Status(http.StatusNoContent)
}
