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

/*
 * Copyright (C) 2015-2020 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 (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"

	"github.com/snapcore/snapd/client/clientutil"
	"github.com/snapcore/snapd/osutil/user"
	"github.com/snapcore/snapd/overlord/auth"
	"github.com/snapcore/snapd/overlord/servicestate"
	"github.com/snapcore/snapd/overlord/state"
	"github.com/snapcore/snapd/overlord/swfeats"
	"github.com/snapcore/snapd/progress"
	"github.com/snapcore/snapd/snap"
	"github.com/snapcore/snapd/strutil"
)

var (
	appsCmd = &Command{
		Path:        "/v2/apps",
		GET:         getAppsInfo,
		POST:        postApps,
		Actions:     []string{"start", "stop", "restart"},
		ReadAccess:  interfaceOpenAccess{Interfaces: []string{"ros-snapd-support"}},
		WriteAccess: interfaceAuthenticatedAccess{Interfaces: []string{"ros-snapd-support"}, Polkit: polkitActionManage},
	}

	logsCmd = &Command{
		Path:       "/v2/logs",
		GET:        getLogs,
		ReadAccess: authenticatedAccess{Polkit: polkitActionManage},
	}
)

var serviceControlChangeKind = swfeats.RegisterChangeKind("service-control")

var newStatusDecorator = func(ctx context.Context, isGlobal bool, uid string) clientutil.StatusDecorator {
	if isGlobal {
		return servicestate.NewStatusDecorator(progress.Null)
	} else {
		return servicestate.NewStatusDecoratorForUid(progress.Null, ctx, uid)
	}
}

func readMaybeBoolValue(query url.Values, name string) (bool, error) {
	if sel := query.Get(name); sel != "" {
		if v, err := strconv.ParseBool(sel); err != nil {
			return false, fmt.Errorf("invalid %s parameter: %q", name, sel)
		} else {
			return v, nil
		}
	}
	return false, nil
}

func getAppsInfo(c *Command, r *http.Request, user *auth.UserState) Response {
	query := r.URL.Query()
	opts := appInfoOptions{}
	switch sel := query.Get("select"); sel {
	case "":
		// nothing to do
	case "service":
		opts.service = true
	default:
		return BadRequest("invalid select parameter: %q", sel)
	}

	global, err := readMaybeBoolValue(query, "global")
	if err != nil {
		return BadRequest(err.Error())
	}

	appInfos, rspe := appInfosFor(c.d.overlord.State(), strutil.CommaSeparatedList(query.Get("names")), opts)
	if rspe != nil {
		return rspe
	}

	u, err := systemUserFromRequest(r)
	if err != nil {
		return BadRequest("cannot retrieve services: %v", err)
	}

	// For the root user, default to global to preserve the normal
	// behaviour and we make sure to match behaviour for snapctl.
	if u.Uid == "0" {
		global = true
	}

	sd := newStatusDecorator(r.Context(), global, u.Uid)
	clientAppInfos, err := clientutil.ClientAppInfosFromSnapAppInfos(appInfos, sd)
	if err != nil {
		return InternalError("%v", err)
	}

	return SyncResponse(clientAppInfos)
}

type appInfoOptions struct {
	service bool
}

func (opts appInfoOptions) String() string {
	if opts.service {
		return "service"
	}

	return "app"
}

// appInfosFor returns a sorted list apps described by names.
//
//   - If names is empty, returns all apps of the wanted kinds (which
//     could be an empty list).
//   - An element of names can be a snap name, in which case all apps
//     from the snap of the wanted kind are included in the result (and
//     it's an error if the snap has no apps of the wanted kind).
//   - An element of names can instead be snap.app, in which case that app is
//     included in the result (and it's an error if the snap and app don't
//     both exist, or if the app is not a wanted kind)
//
// On error an appropriate *apiError is returned; a nil *apiError means
// no error.
//
// It's a programming error to call this with wanted having neither
// services nor commands set.
func appInfosFor(st *state.State, names []string, opts appInfoOptions) ([]*snap.AppInfo, *apiError) {
	snapNames := make(map[string]bool)
	requested := make(map[string]bool)
	for _, name := range names {
		requested[name] = true
		name, _ = splitAppName(name)
		snapNames[name] = true
	}

	snaps, err := allLocalSnapInfos(st, snapSelectNone, snapNames)
	if err != nil {
		return nil, InternalError("cannot list local snaps! %v", err)
	}

	found := make(map[string]bool)
	appInfos := make([]*snap.AppInfo, 0, len(requested))
	for _, snp := range snaps {
		snapName := snp.info.InstanceName()
		apps := make([]*snap.AppInfo, 0, len(snp.info.Apps))
		for _, app := range snp.info.Apps {
			if !opts.service || app.IsService() {
				apps = append(apps, app)
			}
		}

		if len(apps) == 0 && requested[snapName] {
			return nil, AppNotFound("snap %q has no %ss", snapName, opts)
		}

		includeAll := len(requested) == 0 || requested[snapName]
		if includeAll {
			// want all services in a snap
			found[snapName] = true
		}

		for _, app := range apps {
			appName := snapName + "." + app.Name
			if includeAll || requested[appName] {
				appInfos = append(appInfos, app)
				found[appName] = true
			}
		}
	}

	for k := range requested {
		if !found[k] {
			if snapNames[k] {
				return nil, SnapNotFound(k, fmt.Errorf("snap %q not found", k))
			} else {
				snap, app := splitAppName(k)
				return nil, AppNotFound("snap %q has no %s %q", snap, opts, app)
			}
		}
	}

	sort.Sort(snap.AppInfoBySnapApp(appInfos))

	return appInfos, nil
}

// this differs from snap.SplitSnapApp in the handling of the
// snap-only case:
//
//	snap.SplitSnapApp("foo") is ("foo", "foo"),
//	splitAppName("foo") is ("foo", "").
func splitAppName(s string) (snap, app string) {
	if before, after, ok := strings.Cut(s, "."); ok {
		return before, after
	}

	return s, ""
}

func getLogs(c *Command, r *http.Request, user *auth.UserState) Response {
	query := r.URL.Query()
	n := 10
	if s := query.Get("n"); s != "" {
		m, err := strconv.ParseInt(s, 0, 32)
		if err != nil {
			return BadRequest(`invalid value for n: %q: %v`, s, err)
		}
		n = int(m)
		// The special value -1 represents "snap logs -n=all".
		// The backend handles negative values as "all the log" by passing --no-tail to journalctl.
		if n != -1 && n <= 0 {
			return BadRequest(`invalid value for n: %v`, n)
		}
	}
	follow := false
	if s := query.Get("follow"); s != "" {
		f, err := strconv.ParseBool(s)
		if err != nil {
			return BadRequest(`invalid value for follow: %q: %v`, s, err)
		}
		follow = f
	}

	// only services have logs for now
	opts := appInfoOptions{service: true}
	appInfos, rspe := appInfosFor(c.d.overlord.State(), strutil.CommaSeparatedList(query.Get("names")), opts)
	if rspe != nil {
		return rspe
	}
	if len(appInfos) == 0 {
		return AppNotFound("no matching services")
	}

	reader, err := servicestate.LogReader(appInfos, n, follow)
	if err != nil {
		return InternalError("cannot get logs: %v", err)
	}

	return &journalLineReaderSeqResponse{
		ReadCloser: reader,
		follow:     follow,
	}
}

var servicestateControl = servicestate.Control

func decodeServiceInstruction(body io.ReadCloser, u *user.User) (*servicestate.Instruction, error) {
	var inst servicestate.Instruction
	decoder := json.NewDecoder(body)
	if err := decoder.Decode(&inst); err != nil {
		return nil, err
	}
	return &inst, nil
}

var systemUserFromRequest = func(r *http.Request) (*user.User, error) {
	uid, err := uidFromRequest(r)
	if err != nil {
		return nil, err
	}

	u, err := user.LookupId(strconv.Itoa(int(uid)))
	if err != nil {
		return nil, err
	}

	// ensure that we only get the root user on a uid == 0 input
	if u.Uid == "0" && uid != 0 {
		return nil, fmt.Errorf("unknown uid: %d", uid)
	}
	return u, nil
}

func postApps(c *Command, r *http.Request, user *auth.UserState) Response {
	u, err := systemUserFromRequest(r)
	if err != nil {
		return BadRequest("cannot perform operation on services: %v", err)
	}
	inst, err := decodeServiceInstruction(r.Body, u)
	if err != nil {
		return BadRequest("cannot decode request body into service operation: %v", err)
	}
	// XXX: decoder.More()
	if len(inst.Names) == 0 {
		// on POST, don't allow empty to mean all
		return BadRequest("cannot perform operation on services without a list of services to operate on")
	}

	st := c.d.overlord.State()
	appInfos, rspe := appInfosFor(st, inst.Names, appInfoOptions{service: true})
	if rspe != nil {
		return rspe
	}
	if len(appInfos) == 0 {
		// can't happen: appInfosFor with a non-empty list of services
		// shouldn't ever return an empty appInfos with no error response
		return InternalError("no services found")
	}

	// Now that we know the services we are affecting, do some additional checks/fixups
	if err := inst.Validate(u, appInfos); err != nil {
		return BadRequest("cannot perform operation on services: %v", err)
	}
	inst.EnsureDefaultScopeForUser(u)

	// do not pass flags - only create service-control tasks, do not create
	// exec-command tasks for old snapd. These are not needed since we are
	// handling momentary snap service commands.
	st.Lock()
	defer st.Unlock()
	tss, err := servicestateControl(st, appInfos, inst, u, nil, nil)
	if err != nil {
		// TODO: use errToResponse here too and introduce a proper error kind ?
		if _, ok := err.(servicestate.ServiceActionConflictError); ok {
			return Conflict(err.Error())
		}
		return BadRequest(err.Error())
	}
	// names received in the request can be snap or snap.app, we need to
	// extract the actual snap names before associating them with a change
	chg := newChange(st, serviceControlChangeKind, "Running service command", tss, namesToSnapNames(inst))
	st.EnsureBefore(0)
	return AsyncResponse(nil, chg.ID())
}

func namesToSnapNames(inst *servicestate.Instruction) []string {
	seen := make(map[string]struct{}, len(inst.Names))
	for _, snapOrSnapDotApp := range inst.Names {
		snapName, _ := snap.SplitSnapApp(snapOrSnapDotApp)
		seen[snapName] = struct{}{}
	}
	names := make([]string, 0, len(seen))
	for k := range seen {
		names = append(names, k)
	}
	// keep stable ordering
	sort.Strings(names)
	return names
}
