// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2015-2019 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package daemon

import (
	"encoding/json"
	"net/http"
	"regexp"
	"time"

	"github.com/snapcore/snapd/client"
	"github.com/snapcore/snapd/overlord/auth"
	"github.com/snapcore/snapd/overlord/configstate/config"
	"github.com/snapcore/snapd/overlord/devicestate"
	"github.com/snapcore/snapd/overlord/state"
	"github.com/snapcore/snapd/release"
	"github.com/snapcore/snapd/store"
)

var (
	loginCmd = &Command{
		Path:        "/v2/login",
		POST:        loginUser,
		WriteAccess: authenticatedAccess{Polkit: polkitActionLogin},
	}

	logoutCmd = &Command{
		Path:        "/v2/logout",
		POST:        logoutUser,
		WriteAccess: authenticatedAccess{Polkit: polkitActionLogin},
	}

	// backwards compat; to-be-deprecated
	createUserCmd = &Command{
		Path:        "/v2/create-user",
		POST:        postCreateUser,
		WriteAccess: rootAccess{},
	}

	usersCmd = &Command{
		Path:        "/v2/users",
		GET:         getUsers,
		POST:        postUsers,
		Actions:     []string{"create", "remove"},
		ReadAccess:  rootAccess{},
		WriteAccess: rootAccess{},
	}
)

var (
	deviceStateCreateUser       = devicestate.CreateUser
	deviceStateCreateKnownUsers = devicestate.CreateKnownUsers
	deviceStateRemoveUser       = devicestate.RemoveUser
)

// userResponseData contains the data releated to user creation/login/query
type userResponseData struct {
	ID       int      `json:"id,omitempty"`
	Username string   `json:"username,omitempty"`
	Email    string   `json:"email,omitempty"`
	SSHKeys  []string `json:"ssh-keys,omitempty"`

	Macaroon   string   `json:"macaroon,omitempty"`
	Discharges []string `json:"discharges,omitempty"`
}

var isEmailish = regexp.MustCompile(`.@.*\..`).MatchString

func loginUser(c *Command, r *http.Request, user *auth.UserState) Response {
	var loginData struct {
		Username string `json:"username"`
		Email    string `json:"email"`
		Password string `json:"password"`
		Otp      string `json:"otp"`
	}

	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&loginData); err != nil {
		return BadRequest("cannot decode login data from request body: %v", err)
	}

	if loginData.Email == "" && isEmailish(loginData.Username) {
		// for backwards compatibility, if no email is provided assume username is the email
		loginData.Email = loginData.Username
		loginData.Username = ""
	}

	if loginData.Email == "" && user != nil && user.Email != "" {
		loginData.Email = user.Email
	}

	// the "username" needs to look a lot like an email address
	if !isEmailish(loginData.Email) {
		return &apiError{
			Status:  400,
			Message: "please use a valid email address.",
			Kind:    client.ErrorKindInvalidAuthData,
			Value:   map[string][]string{"email": {"invalid"}},
		}
	}

	overlord := c.d.overlord
	st := overlord.State()
	theStore := storeFrom(c.d)
	macaroon, discharge, err := theStore.LoginUser(loginData.Email, loginData.Password, loginData.Otp)
	switch err {
	case store.ErrAuthenticationNeeds2fa:
		return &apiError{
			Status:  401,
			Message: err.Error(),
			Kind:    client.ErrorKindTwoFactorRequired,
		}
	case store.Err2faFailed:
		return &apiError{
			Status:  401,
			Message: err.Error(),
			Kind:    client.ErrorKindTwoFactorFailed,
		}
	default:
		switch err := err.(type) {
		case store.InvalidAuthDataError:
			return &apiError{
				Status:  400,
				Message: err.Error(),
				Kind:    client.ErrorKindInvalidAuthData,
				Value:   err,
			}
		case store.PasswordPolicyError:
			return &apiError{
				Status:  401,
				Message: err.Error(),
				Kind:    client.ErrorKindPasswordPolicy,
				Value:   err,
			}
		}
		return Unauthorized(err.Error())
	case nil:
		// continue
	}
	st.Lock()
	if user != nil {
		// local user logged-in, set its store macaroons
		user.StoreMacaroon = macaroon
		user.StoreDischarges = []string{discharge}
		// user's email address authenticated by the store
		user.Email = loginData.Email
		err = auth.UpdateUser(st, user)
	} else {
		user, err = auth.NewUser(st, auth.NewUserParams{
			Username:   loginData.Username,
			Email:      loginData.Email,
			Macaroon:   macaroon,
			Discharges: []string{discharge},
		})
	}
	st.Unlock()
	if err != nil {
		return InternalError("cannot persist authentication details: %v", err)
	}

	result := userResponseData{
		ID:         user.ID,
		Username:   user.Username,
		Email:      user.Email,
		Macaroon:   user.Macaroon,
		Discharges: user.Discharges,
	}
	return SyncResponse(result)
}

func logoutUser(c *Command, r *http.Request, user *auth.UserState) Response {
	state := c.d.overlord.State()
	state.Lock()
	defer state.Unlock()

	if user == nil {
		return BadRequest("not logged in")
	}
	_, err := auth.RemoveUser(state, user.ID)
	if err != nil {
		return InternalError(err.Error())
	}

	return SyncResponse(nil)
}

// this might need to become a function, if having user admin becomes a config option
var hasUserAdmin = !release.OnClassic

const noUserAdmin = "system user administration via snapd is not allowed on this system"

func postUsers(c *Command, r *http.Request, user *auth.UserState) Response {
	if !hasUserAdmin {
		return MethodNotAllowed(noUserAdmin)
	}

	var postData postUserData

	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&postData); err != nil {
		return BadRequest("cannot decode user action data from request body: %v", err)
	}
	if decoder.More() {
		return BadRequest("spurious content after user action")
	}
	switch postData.Action {
	case "create":
		return createUser(c, postData.postUserCreateData)
	case "remove":
		return removeUser(c, postData.Username, postData.postUserDeleteData)
	case "":
		return BadRequest("missing user action")
	}
	return BadRequest("unsupported user action %q", postData.Action)
}

func removeUser(c *Command, username string, opts postUserDeleteData) Response {
	st := c.d.overlord.State()
	st.Lock()
	defer st.Unlock()

	u, err := deviceStateRemoveUser(st, username, &devicestate.RemoveUserOptions{})
	if err != nil {
		if _, ok := err.(*devicestate.UserError); ok {
			return BadRequest(err.Error())
		}
		return InternalError(err.Error())
	}

	result := map[string]any{
		"removed": []userResponseData{
			{ID: u.ID, Username: u.Username, Email: u.Email},
		},
	}
	return SyncResponse(result)
}

func postCreateUser(c *Command, r *http.Request, user *auth.UserState) Response {
	if !hasUserAdmin {
		return Forbidden(noUserAdmin)
	}
	var createData postUserCreateData

	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&createData); err != nil {
		return BadRequest("cannot decode create-user data from request body: %v", err)
	}

	// this is /v2/create-user, meaning we want the
	// backwards-compatible wackiness
	// Request singleUserResultCompat only if the request
	// was *not* to create all known system users.
	// Automatic implies known, which means we have
	// to take that into account as well
	known := createData.Known || createData.Automatic
	if !known || createData.Email != "" {
		createData.singleUserResultCompat = true
	}

	return createUser(c, createData)
}

func createUser(c *Command, createData postUserCreateData) Response {
	var createdUsersResponse []userResponseData
	// verify request
	st := c.d.overlord.State()
	st.Lock()
	users, err := auth.Users(st)
	st.Unlock()
	if err != nil {
		return InternalError("cannot get user count: %s", err)
	}

	if !createData.Expiration.IsZero() {
		// Automatic implies known
		if createData.Known || createData.Automatic {
			// Known users are described by assertions, and assertions provide their own
			// expiration date for users, which we will not allow to overwrite
			return BadRequest("cannot create user: expiration date cannot be provided for known users")
		}
		if createData.Expiration.Before(time.Now()) {
			return BadRequest("cannot create user: expiration date must be set in the future")
		}
	}

	if !createData.ForceManaged {
		if len(users) > 0 && createData.Automatic {
			// no users created but no error with the automatic flag
			return SyncResponse([]userResponseData{})
		}
		if len(users) > 0 {
			return BadRequest("cannot create user: device already managed")
		}
		if release.OnClassic {
			return BadRequest("cannot create user: device is a classic system")
		}
	}
	if createData.Automatic {
		var enabled bool
		st.Lock()
		tr := config.NewTransaction(st)
		err := tr.Get("core", "users.create.automatic", &enabled)
		st.Unlock()
		if err != nil {
			if !config.IsNoOption(err) {
				return InternalError("%v", err)
			}
			// defaults to enabled
			enabled = true
		}
		if !enabled {
			// disabled, do nothing
			return SyncResponse([]userResponseData{})
		}
		// Automatic implies known/sudoers
		createData.Known = true
		createData.Sudoer = true
	}

	createdUsers, err := doCreateUser(st, createData)
	if err != nil {
		if _, ok := err.(*devicestate.UserError); ok {
			return BadRequest(err.Error())
		}
		return InternalError(err.Error())
	}

	if createData.singleUserResultCompat {
		return SyncResponse(&userResponseData{
			Username: createdUsers[0].Username,
			SSHKeys:  createdUsers[0].SSHKeys,
		})
	}

	for _, cu := range createdUsers {
		createdUsersResponse = append(createdUsersResponse, userResponseData{
			Username: cu.Username,
			SSHKeys:  cu.SSHKeys,
		})
	}
	return SyncResponse(createdUsersResponse)
}

func doCreateUser(st *state.State, createData postUserCreateData) ([]*devicestate.CreatedUser, error) {
	st.Lock()
	defer st.Unlock()

	if createData.Known {
		return deviceStateCreateKnownUsers(st, createData.Sudoer, createData.Email)
	}

	user, err := deviceStateCreateUser(st, createData.Sudoer, createData.Email, createData.Expiration)
	return []*devicestate.CreatedUser{user}, err
}

type postUserData struct {
	Action   string `json:"action"`
	Username string `json:"username"`
	postUserCreateData
	postUserDeleteData
}

type postUserCreateData struct {
	Email        string    `json:"email"`
	Sudoer       bool      `json:"sudoer"`
	Known        bool      `json:"known"`
	ForceManaged bool      `json:"force-managed"`
	Automatic    bool      `json:"automatic"`
	Expiration   time.Time `json:"expiration"`

	// singleUserResultCompat indicates whether to preserve
	// backwards compatibility, which results in more clunky
	// return values (userResponseData OR [userResponseData] vs now
	// uniform [userResponseData]); internal, not from JSON.
	singleUserResultCompat bool
}

type postUserDeleteData struct{}

func getUsers(c *Command, r *http.Request, user *auth.UserState) Response {
	st := c.d.overlord.State()
	st.Lock()
	users, err := auth.Users(st)
	st.Unlock()
	if err != nil {
		return InternalError("cannot get users: %s", err)
	}

	resp := make([]userResponseData, len(users))
	for i, u := range users {
		resp[i] = userResponseData{
			Username: u.Username,
			Email:    u.Email,
			ID:       u.ID,
		}
	}
	return SyncResponse(resp)
}
