package webui

import (
	"context"
	"net/http"
	"time"

	"mailgoserver/internal/db"
)

// MailboxPrefix is the self-service webmail portal's URL prefix — a mailbox owner's
// login/account area, entirely separate from the admin dashboard at Prefix.
const MailboxPrefix = "/webmail"

const mailboxSessionCookieName = "mailgoserver_mailbox_session"

// mailboxCtxKey is its own type (not webui's ctxKey) so a mailbox session can never
// collide with or be confused for an admin session in request context — the two
// actor types are deliberately kept fully separate, per the parallel-schema design.
type mailboxCtxKey int

const ctxMailboxKey mailboxCtxKey = iota

func setMailboxSessionCookie(w http.ResponseWriter, token string, secure bool) {
	http.SetCookie(w, &http.Cookie{
		Name:     mailboxSessionCookieName,
		Value:    token,
		Path:     "/",
		HttpOnly: true,
		Secure:   secure,
		SameSite: http.SameSiteLaxMode,
		MaxAge:   int(sessionTTL.Seconds()),
	})
}

func clearMailboxSessionCookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{Name: mailboxSessionCookieName, Value: "", Path: "/", MaxAge: -1})
}

// currentMailboxSession loads the session + mailbox for the request's cookie, if any
// and valid. A nil session/mailbox (no error) means "not logged in".
func (a *App) currentMailboxSession(r *http.Request) (*db.MailboxSession, *db.Mailbox, error) {
	c, err := r.Cookie(mailboxSessionCookieName)
	if err != nil || c.Value == "" {
		return nil, nil, nil
	}
	sess, err := a.DB.GetMailboxSession(c.Value)
	if err != nil || sess == nil {
		return nil, nil, err
	}
	if time.Now().After(sess.ExpiresAt) {
		_ = a.DB.DeleteMailboxSession(sess.Token)
		return nil, nil, nil
	}
	mbox, err := a.DB.GetMailboxByID(sess.MailboxID)
	if err != nil || mbox == nil {
		return nil, nil, err
	}
	return sess, mbox, nil
}

func mailboxFromContext(r *http.Request) *db.Mailbox {
	m, _ := r.Context().Value(ctxMailboxKey).(*db.Mailbox)
	return m
}

// requireMailboxAuth gates every webmail route behind a valid, fully-authenticated
// mailbox session: logged in, and second factor satisfied if one is enabled.
func (a *App) requireMailboxAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		sess, mbox, err := a.currentMailboxSession(r)
		if err != nil {
			a.Logger.Error("mailbox session lookup: %v", err)
		}
		if sess == nil || mbox == nil {
			http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound)
			return
		}

		needsMFA := mbox.TOTPEnabled
		if !needsMFA {
			if n, _ := a.DB.CountMailboxWebAuthnCredentials(mbox.ID); n > 0 {
				needsMFA = true
			}
		}
		if needsMFA && !sess.MFAVerified {
			http.Redirect(w, r, MailboxPrefix+"/login/mfa", http.StatusFound)
			return
		}

		// enforce_mailbox_mfa: login itself is never blocked (see webmailLoginSubmit) —
		// instead, a mailbox with no MFA configured and no domain/mailbox exemption is
		// sent to the isolated /mfa-setup page (no other route reachable except the
		// actual totp/passkey setup actions) until they configure one. This never
		// touches IMAP/SMTP app-password auth — a completely separate, non-interactive
		// protocol path this gate has no bearing on; see mailboxNeedsMFASetup's doc
		// comment.
		if a.mailboxNeedsMFASetup(mbox) {
			if !mailboxMFASetupExempt(r.Method, r.URL.Path) {
				http.Redirect(w, r, MailboxPrefix+"/mfa-setup", http.StatusFound)
				return
			}
		}

		ctx := context.WithValue(r.Context(), ctxMailboxKey, mbox)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// mailboxMFASetupExempt mirrors adminMFASetupExempt for the webmail portal: the
// isolated setup page itself, plus the actual form/API actions needed to complete
// TOTP or passkey enrollment. Everything else — including the dashboard itself —
// redirects to /mfa-setup.
func mailboxMFASetupExempt(method, path string) bool {
	if method == http.MethodGet {
		return path == MailboxPrefix+"/mfa-setup"
	}
	if method != http.MethodPost {
		return false
	}
	switch path {
	case MailboxPrefix + "/account/totp/setup", MailboxPrefix + "/account/totp/confirm",
		MailboxPrefix + "/account/passkey/begin", MailboxPrefix + "/account/passkey/finish":
		return true
	}
	return false
}

// mailboxNeedsMFASetup reports whether [Auth] enforce_mailbox_mfa applies to this
// mailbox and it doesn't have a second factor configured yet — false if enforcement
// is off, MFA is already set up, or the mailbox/its domain is explicitly exempt.
// Login is never blocked by this (see webmailLoginSubmit) — it gates every other
// webmail route (see requireMailboxAuth/mailboxMFASetupExempt). Never applies to
// IMAP/SMTP app-password auth, which has no interactive step to enforce MFA on
// regardless.
func (a *App) mailboxNeedsMFASetup(mbox *db.Mailbox) bool {
	if !a.Cfg.Section("Auth").Key("enforce_mailbox_mfa").MustBool(false) {
		return false
	}
	hasMFA := mbox.TOTPEnabled
	if !hasMFA {
		if n, _ := a.DB.CountMailboxWebAuthnCredentials(mbox.ID); n > 0 {
			hasMFA = true
		}
	}
	if hasMFA || mbox.MFAExempt {
		return false
	}
	if dom, err := a.DB.GetDomainByID(mbox.DomainID); err == nil && dom != nil && dom.MFAExempt {
		return false
	}
	return true
}
