package webui

import (
	"net/http"
	"strconv"
	"strings"

	"mailgoserver/internal/db"
	"mailgoserver/internal/mailstore"
)

const bytesPerGB = 1024 * 1024 * 1024

// buildMailboxEmail mirrors buildSenderEmail exactly: the domain is always resolved
// server-side by ID, never trusted as free text, so a mailbox can never end up
// assigned to a domain its own address doesn't belong to.
func (a *App) buildMailboxEmail(localPart string, domainID int64) (string, error) {
	dom, err := a.DB.GetDomainByID(domainID)
	if err != nil {
		return "", err
	}
	if dom == nil || !validLocalPart.MatchString(localPart) {
		return "", nil
	}
	return localPart + "@" + dom.DomainName, nil
}

func (a *App) mailboxesList(w http.ResponseWriter, r *http.Request) {
	mailboxes, err := a.DB.ListMailboxes()
	if err != nil {
		setFlash(w, "error", "Error loading mailboxes")
	}
	scope := scopeFromContext(r)
	var pairs [][2]any
	for _, m := range mailboxes {
		if !scope.Allowed(m.DomainID) {
			continue
		}
		pctFull := 0.0
		if m.QuotaBytes > 0 {
			pctFull = float64(m.UsedBytes) / float64(m.QuotaBytes) * 100
		}
		pairs = append(pairs, [2]any{m.Mailbox, M{"domain_name": m.DomainName, "pct_full": pctFull}})
	}
	a.render(w, r, "mailboxes.html", M{"active": "mailboxes", "mailboxes": pairs})
}

func (a *App) addMailboxForm(w http.ResponseWriter, r *http.Request) {
	domains, _ := a.accessibleDomains(r)
	a.render(w, r, "add_mailbox.html", M{"active": "mailboxes", "domains": domains})
}

// addMailbox mirrors addSender's shape: local_part + domain_id resolved server-side
// into the real email, a random per-mailbox encryption key generated and sealed with
// the server master key (see internal/mailstore), and quota defaulting to the owning
// domain's configured default when left blank.
func (a *App) addMailbox(w http.ResponseWriter, r *http.Request) {
	localPart := strings.TrimSpace(r.FormValue("local_part"))
	password := r.FormValue("password")
	domainID := int64(atoi(r.FormValue("domain_id")))
	quotaGB := r.FormValue("quota_gb")

	if !requireDomainAccess(w, r, domainID) {
		return
	}
	email, err := a.buildMailboxEmail(localPart, domainID)
	if err != nil {
		setFlash(w, "error", "Error creating mailbox")
		http.Redirect(w, r, Prefix+"/mailboxes/add", http.StatusFound)
		return
	}
	if email == "" || password == "" {
		setFlash(w, "error", "All fields are required and the local part may only contain letters, numbers, and . _ % + -")
		http.Redirect(w, r, Prefix+"/mailboxes/add", http.StatusFound)
		return
	}
	if exists, _ := a.DB.MailboxEmailExists(email, -1); exists {
		setFlash(w, "error", "A mailbox with this email already exists")
		http.Redirect(w, r, Prefix+"/mailboxes/add", http.StatusFound)
		return
	}

	quotaBytes := parseQuotaGB(quotaGB)
	if quotaBytes <= 0 {
		quotaBytes, _ = a.DB.GetDomainDefaultQuota(domainID)
	}

	hash, err := db.HashPassword(password)
	if err != nil {
		setFlash(w, "error", "Error creating mailbox")
		http.Redirect(w, r, Prefix+"/mailboxes/add", http.StatusFound)
		return
	}
	dek := mailstore.GenerateDEK()
	wrapped, nonce, err := a.Mailstore.WrapDEK(dek)
	if err != nil {
		a.Logger.Error("wrap mailbox DEK: %v", err)
		setFlash(w, "error", "Error creating mailbox")
		http.Redirect(w, r, Prefix+"/mailboxes/add", http.StatusFound)
		return
	}
	if _, err := a.DB.CreateMailbox(email, hash, domainID, quotaBytes, wrapped, nonce); err != nil {
		setFlash(w, "error", "Error creating mailbox")
		http.Redirect(w, r, Prefix+"/mailboxes/add", http.StatusFound)
		return
	}
	setFlash(w, "success", "Mailbox added successfully")
	http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
}

func parseQuotaGB(s string) int64 {
	gb, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
	if err != nil || gb <= 0 {
		return 0
	}
	return int64(gb * bytesPerGB)
}

// mailboxWithAccess mirrors senderWithAccess.
func (a *App) mailboxWithAccess(w http.ResponseWriter, r *http.Request) (mailbox *db.Mailbox, ok bool) {
	mailbox, err := a.DB.GetMailboxByID(pathID(r))
	if err != nil || mailbox == nil {
		http.NotFound(w, r)
		return nil, false
	}
	if !requireDomainAccess(w, r, mailbox.DomainID) {
		return nil, false
	}
	return mailbox, true
}

// resetMailboxMFA clears a mailbox owner's TOTP and passkeys — e.g. after a lost
// device, or (under enforce_mailbox_mfa) to unblock their webmail login without
// needing a domain/mailbox exemption — so they can sign back in and, if MFA is
// enforced, re-enroll from the webmail portal on their next login.
func (a *App) resetMailboxMFA(w http.ResponseWriter, r *http.Request) {
	mailbox, ok := a.mailboxWithAccess(w, r)
	if !ok {
		return
	}
	if err := a.DB.ResetMailboxMFA(mailbox.ID); err != nil {
		setFlash(w, "error", "Error resetting MFA")
	} else {
		_ = a.DB.LogAuthAttempt("mailbox_mfa", mailbox.Email, a.requestIP(r), true, "MFA reset by admin "+userFromContext(r).Username)
		setFlash(w, "success", "MFA reset for "+mailbox.Email)
	}
	http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
}

func (a *App) disableMailbox(w http.ResponseWriter, r *http.Request) {
	mailbox, ok := a.mailboxWithAccess(w, r)
	if !ok {
		return
	}
	if err := a.DB.SetMailboxActive(mailbox.ID, false); err != nil {
		setFlash(w, "error", "Error disabling mailbox")
	} else {
		setFlash(w, "success", "Mailbox disabled")
	}
	http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
}

func (a *App) enableMailbox(w http.ResponseWriter, r *http.Request) {
	mailbox, ok := a.mailboxWithAccess(w, r)
	if !ok {
		return
	}
	if err := a.DB.SetMailboxActive(mailbox.ID, true); err != nil {
		setFlash(w, "error", "Error enabling mailbox")
	} else {
		setFlash(w, "success", "Mailbox enabled")
	}
	http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
}

// removeMailbox deletes every stored message's on-disk ciphertext via mailstore first
// (so nothing is orphaned on disk), then hard-deletes the mailbox row and everything
// that references it.
func (a *App) removeMailbox(w http.ResponseWriter, r *http.Request) {
	mailbox, ok := a.mailboxWithAccess(w, r)
	if !ok {
		return
	}
	uids, err := a.DB.ListMessageUIDsForMailbox(mailbox.ID)
	if err != nil {
		setFlash(w, "error", "Error removing mailbox")
		http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
		return
	}
	for _, uid := range uids {
		if err := a.Mailstore.DeleteMessage(mailbox.ID, uid); err != nil {
			a.Logger.Error("delete message %d for mailbox %d: %v", uid, mailbox.ID, err)
		}
	}
	if err := a.DB.RemoveMailboxCascade(mailbox.ID); err != nil {
		setFlash(w, "error", "Error removing mailbox")
	} else {
		setFlash(w, "success", "Mailbox permanently removed")
	}
	http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
}

func (a *App) editMailboxForm(w http.ResponseWriter, r *http.Request) {
	mailbox, ok := a.mailboxWithAccess(w, r)
	if !ok {
		return
	}
	domains, _ := a.accessibleDomains(r)
	a.render(w, r, "edit_mailbox.html", M{
		"active": "mailboxes", "mailbox": mailbox, "domains": domains,
		"local_part": localPartOf(mailbox.Email), "quota_gb": float64(mailbox.QuotaBytes) / bytesPerGB,
	})
}

// editMailbox allows changing the portal password and quota. The local part/domain
// (and so the address itself) are intentionally NOT editable here — the IMAP/SMTP app
// passwords and encryption key are already bound to this mailbox's identity, and
// renaming it out from under those would orphan them. Remove and recreate instead.
func (a *App) editMailbox(w http.ResponseWriter, r *http.Request) {
	mailbox, ok := a.mailboxWithAccess(w, r)
	if !ok {
		return
	}
	password := r.FormValue("password")
	quotaBytes := parseQuotaGB(r.FormValue("quota_gb"))
	if quotaBytes <= 0 {
		quotaBytes = mailbox.QuotaBytes
	}
	if err := a.DB.SetMailboxQuota(mailbox.ID, quotaBytes); err != nil {
		setFlash(w, "error", "Error updating mailbox")
		http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
		return
	}
	if password != "" {
		hash, err := db.HashPassword(password)
		if err != nil {
			setFlash(w, "error", "Error updating mailbox")
			http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
			return
		}
		if err := a.DB.SetMailboxPasswordHash(mailbox.ID, hash); err != nil {
			setFlash(w, "error", "Error updating mailbox")
			http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
			return
		}
	}
	if err := a.DB.SetMailboxMFAExempt(mailbox.ID, r.FormValue("mfa_exempt") == "on"); err != nil {
		setFlash(w, "error", "Error updating mailbox")
		http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
		return
	}
	setFlash(w, "success", "Mailbox updated successfully")
	http.Redirect(w, r, Prefix+"/mailboxes", http.StatusFound)
}
