package http

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	_ "embed"

	"github.com/mjl-/sherpa"
	"github.com/mjl-/sherpaprom"

	"github.com/mjl-/mox/metrics"
	"github.com/mjl-/mox/mlog"
	"github.com/mjl-/mox/mox-"
	"github.com/mjl-/mox/moxvar"
	"github.com/mjl-/mox/store"
)

//go:embed accountapi.json
var accountapiJSON []byte

//go:embed account.html
var accountHTML []byte

var accountDoc = mustParseAPI(accountapiJSON)

var accountSherpaHandler http.Handler

func init() {
	collector, err := sherpaprom.NewCollector("moxaccount", nil)
	if err != nil {
		xlog.Fatalx("creating sherpa prometheus collector", err)
	}

	accountSherpaHandler, err = sherpa.NewHandler("/account/api/", moxvar.Version, Account{}, &accountDoc, &sherpa.HandlerOpts{Collector: collector, AdjustFunctionNames: "none"})
	if err != nil {
		xlog.Fatalx("sherpa handler", err)
	}
}

// Account exports web API functions for the account web interface. All its
// methods are exported under /account/api/. Function calls require valid HTTP
// Authentication credentials of a user.
type Account struct{}

func accountHandle(w http.ResponseWriter, r *http.Request) {
	ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
	log := xlog.WithContext(ctx).Fields(mlog.Field("userauth", ""))
	var accountName string
	authResult := "error"
	defer func() {
		metrics.AuthenticationInc("httpaccount", "httpbasic", authResult)
	}()
	// todo: should probably add a cache here instead of looking up password in database all the time, just like in admin.go
	if auth := r.Header.Get("Authorization"); auth == "" || !strings.HasPrefix(auth, "Basic ") {
	} else if authBuf, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic ")); err != nil {
		log.Infox("parsing base64", err)
	} else if t := strings.SplitN(string(authBuf), ":", 2); len(t) != 2 {
		log.Info("bad user:pass form")
	} else if acc, err := store.OpenEmailAuth(t[0], t[1]); err != nil {
		if errors.Is(err, store.ErrUnknownCredentials) {
			authResult = "badcreds"
		}
		log.Infox("open account", err)
	} else {
		accountName = acc.Name
		authResult = "ok"
	}
	if accountName == "" {
		w.Header().Set("WWW-Authenticate", `Basic realm="mox account - login with email address and password"`)
		w.WriteHeader(http.StatusUnauthorized)
		fmt.Fprintln(w, "http 401 - unauthorized - mox account - login with email address and password")
		return
	}

	if r.Method == "GET" && r.URL.Path == "/account/" {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.Header().Set("Cache-Control", "no-cache; max-age=0")
		f, err := os.Open("http/account.html")
		if err == nil {
			defer f.Close()
			io.Copy(w, f)
		} else {
			w.Write(accountHTML)
		}
		return
	}
	accountSherpaHandler.ServeHTTP(w, r.WithContext(context.WithValue(ctx, authCtxKey, accountName)))
}

type ctxKey string

var authCtxKey ctxKey = "account"

// SetPassword saves a new password for the account, invalidating the previous password.
// Sessions are not interrupted, and will keep working. New login attempts must use the new password.
// Password must be at least 8 characters.
func (Account) SetPassword(ctx context.Context, password string) {
	if len(password) < 8 {
		panic(&sherpa.Error{Code: "user:error", Message: "password must be at least 8 characters"})
	}
	accountName := ctx.Value(authCtxKey).(string)
	acc, err := store.OpenAccount(accountName)
	xcheckf(ctx, err, "open account")
	defer acc.Close()
	err = acc.SetPassword(password)
	xcheckf(ctx, err, "setting password")
}
