// -*- 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 (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"sort"
	"strings"

	"github.com/snapcore/snapd/interfaces"
	"github.com/snapcore/snapd/overlord/auth"
	"github.com/snapcore/snapd/overlord/ifacestate"
	"github.com/snapcore/snapd/overlord/snapstate"
	"github.com/snapcore/snapd/overlord/state"
)

var (
	interfacesCmd = &Command{
		Path:        "/v2/interfaces",
		GET:         interfacesConnectionsMultiplexer,
		POST:        changeInterfaces,
		ReadAccess:  openAccess{},
		WriteAccess: authenticatedAccess{Polkit: polkitActionManageInterfaces},
	}
)

// interfacesConnectionsMultiplexer multiplexes to either legacy (connection) or modern behavior (interfaces).
func interfacesConnectionsMultiplexer(c *Command, r *http.Request, user *auth.UserState) Response {
	query := r.URL.Query()
	qselect := query.Get("select")
	if qselect == "" {
		return getLegacyConnections(c, r, user)
	} else {
		return getInterfaces(c, r, user)
	}
}

func getInterfaces(c *Command, r *http.Request, user *auth.UserState) Response {
	// Collect query options from request arguments.
	q := r.URL.Query()
	pselect := q.Get("select")
	if pselect != "all" && pselect != "connected" {
		return BadRequest("unsupported select qualifier")
	}
	var names []string // Interface names
	namesStr := q.Get("names")
	if namesStr != "" {
		names = strings.Split(namesStr, ",")
	}
	opts := &interfaces.InfoOptions{
		Names:     names,
		Doc:       q.Get("doc") == "true",
		Plugs:     q.Get("plugs") == "true",
		Slots:     q.Get("slots") == "true",
		Connected: pselect == "connected",
	}
	// Query the interface repository (this returns []*interface.Info).
	infos := c.d.overlord.InterfaceManager().Repository().Info(opts)
	infoJSONs := make([]*interfaceJSON, 0, len(infos))

	for _, info := range infos {
		// Convert interfaces.Info into interfaceJSON
		plugs := make([]*plugJSON, 0, len(info.Plugs))
		for _, plug := range info.Plugs {
			plugs = append(plugs, &plugJSON{
				Snap:  plug.Snap.InstanceName(),
				Name:  plug.Name,
				Attrs: plug.Attrs,
				Label: plug.Label,
			})
		}
		slots := make([]*slotJSON, 0, len(info.Slots))
		for _, slot := range info.Slots {
			slots = append(slots, &slotJSON{
				Snap:  slot.Snap.InstanceName(),
				Name:  slot.Name,
				Attrs: slot.Attrs,
				Label: slot.Label,
			})
		}
		infoJSONs = append(infoJSONs, &interfaceJSON{
			Name:    info.Name,
			Summary: info.Summary,
			DocURL:  info.DocURL,
			Plugs:   plugs,
			Slots:   slots,
		})
	}
	return SyncResponse(infoJSONs)
}

func getLegacyConnections(c *Command, r *http.Request, user *auth.UserState) Response {
	connsjson, err := collectConnections(c.d.overlord.InterfaceManager(), collectFilter{})
	if err != nil {
		return InternalError("collecting connection information failed: %v", err)
	}
	legacyconnsjson := legacyConnectionsJSON{
		Plugs: connsjson.Plugs,
		Slots: connsjson.Slots,
	}
	return SyncResponse(legacyconnsjson)
}

// changeInterfaces controls the interfaces system.
// Plugs can be connected to and disconnected from slots.
func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Response {
	var a interfaceAction
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&a); err != nil {
		return BadRequest("cannot decode request body into an interface action: %v", err)
	}
	if a.Action == "" {
		return BadRequest("interface action not specified")
	}
	if len(a.Plugs) > 1 || len(a.Slots) > 1 {
		return NotImplemented("many-to-many operations are not implemented")
	}
	if a.Action != "connect" && a.Action != "disconnect" {
		return BadRequest("unsupported interface action: %q", a.Action)
	}
	if len(a.Plugs) == 0 || len(a.Slots) == 0 {
		return BadRequest("at least one plug and slot is required")
	}

	var summary string
	var err error

	var tasksets []*state.TaskSet
	var affected []string

	st := c.d.overlord.State()
	st.Lock()
	defer st.Unlock()

	checkInstalled := func(snapName string) error {
		// empty snap name is fine, ResolveConnect/ResolveDisconnect handles it.
		if snapName == "" {
			return nil
		}
		var snapst snapstate.SnapState
		err := snapstate.Get(st, snapName, &snapst)
		if (err == nil && !snapst.IsInstalled()) || errors.Is(err, state.ErrNoState) {
			return fmt.Errorf("snap %q is not installed", snapName)
		}
		if err == nil {
			return nil
		}
		return fmt.Errorf("internal error: cannot get state of snap %q: %v", snapName, err)
	}

	for i := range a.Plugs {
		a.Plugs[i].Snap = ifacestate.RemapSnapFromRequest(a.Plugs[i].Snap)
		if err := checkInstalled(a.Plugs[i].Snap); err != nil {
			return errToResponse(err, nil, BadRequest, "%v")
		}
	}
	for i := range a.Slots {
		a.Slots[i].Snap = ifacestate.RemapSnapFromRequest(a.Slots[i].Snap)
		if err := checkInstalled(a.Slots[i].Snap); err != nil {
			return errToResponse(err, nil, BadRequest, "%v")
		}
	}

	switch a.Action {
	case "connect":
		var connRef *interfaces.ConnRef
		repo := c.d.overlord.InterfaceManager().Repository()
		connRef, err = repo.ResolveConnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name)
		if err == nil {
			var ts *state.TaskSet
			affected = snapNamesFromConns([]*interfaces.ConnRef{connRef})
			summary = fmt.Sprintf("Connect %s:%s to %s:%s", connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name)
			ts, err = ifacestate.Connect(st, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name)
			if _, ok := err.(*ifacestate.ErrAlreadyConnected); ok {
				change := newChange(st, a.Action+"-snap", summary, nil, affected)
				change.SetStatus(state.DoneStatus)
				return AsyncResponse(nil, change.ID())
			}
			tasksets = append(tasksets, ts)
		}
	case "disconnect":
		var conns []*interfaces.ConnRef
		summary = fmt.Sprintf("Disconnect %s:%s from %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name)
		conns, err = c.d.overlord.InterfaceManager().ResolveDisconnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name, a.Forget)
		if err == nil {
			if len(conns) == 0 {
				return InterfacesUnchanged("nothing to do")
			}
			repo := c.d.overlord.InterfaceManager().Repository()
			for _, connRef := range conns {
				var ts *state.TaskSet
				var conn *interfaces.Connection
				if a.Forget {
					ts, err = ifacestate.Forget(st, repo, connRef)
				} else {
					conn, err = repo.Connection(connRef)
					if err != nil {
						break
					}
					ts, err = ifacestate.Disconnect(st, conn)
					if err != nil {
						break
					}
				}
				if err != nil {
					break
				}
				ts.JoinLane(st.NewLane())
				tasksets = append(tasksets, ts)
			}
			affected = snapNamesFromConns(conns)
		}
	}
	if err != nil {
		return errToResponse(err, nil, BadRequest, "%v")
	}

	change := newChange(st, a.Action+"-snap", summary, tasksets, affected)
	st.EnsureBefore(0)

	return AsyncResponse(nil, change.ID())
}

func snapNamesFromConns(conns []*interfaces.ConnRef) []string {
	m := make(map[string]bool)
	for _, conn := range conns {
		m[conn.PlugRef.Snap] = true
		m[conn.SlotRef.Snap] = true
	}
	l := make([]string, 0, len(m))
	for name := range m {
		l = append(l, name)
	}
	sort.Strings(l)
	return l
}
