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

/*
 * Copyright (C) 2021 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"
	"mime"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/snapcore/snapd/asserts"
	"github.com/snapcore/snapd/client"
	"github.com/snapcore/snapd/client/clientutil"
	"github.com/snapcore/snapd/dirs"
	"github.com/snapcore/snapd/overlord/assertstate"
	"github.com/snapcore/snapd/overlord/auth"
	"github.com/snapcore/snapd/overlord/devicestate"
	"github.com/snapcore/snapd/overlord/snapstate"
	"github.com/snapcore/snapd/overlord/state"
)

var (
	serialModelCmd = &Command{
		Path:        "/v2/model/serial",
		GET:         getSerial,
		POST:        postSerial,
		Actions:     []string{"forget"},
		ReadAccess:  openAccess{},
		WriteAccess: rootAccess{},
	}
	modelCmd = &Command{
		Path:        "/v2/model",
		POST:        postModel,
		GET:         getModel,
		ReadAccess:  openAccess{},
		WriteAccess: rootAccess{},
	}
)

var (
	devicestateRemodel = devicestate.Remodel
	sideloadSnapsInfo  = sideloadInfo
)

type postModelData struct {
	NewModel string `json:"new-model"`
	Offline  bool   `json:"offline"`
}

func postModel(c *Command, r *http.Request, _ *auth.UserState) Response {
	contentType := r.Header.Get("Content-Type")
	mediaType, params, err := mime.ParseMediaType(contentType)
	if err != nil {
		// assume json body, as type was not enforced in the past
		mediaType = "application/json"
	}

	switch mediaType {
	case "application/json":
		// If json content type we get only the new model assertion and
		// the rest is either downloaded from the store or already installed.
		return remodelJSON(c, r)
	case "multipart/form-data":
		// multipart/form-data content type can be used to sideload
		// part of the things necessary for a remodel.
		return remodelForm(c, r, params)
	default:
		return BadRequest("unexpected media type %q", mediaType)
	}
}

func modelFromData(data []byte) (*asserts.Model, error) {
	rawNewModel, err := asserts.Decode(data)
	if err != nil {
		return nil, fmt.Errorf("cannot decode new model assertion: %v", err)
	}
	newModel, ok := rawNewModel.(*asserts.Model)
	if !ok {
		return nil, fmt.Errorf("new model is not a model assertion: %v", rawNewModel.Type())
	}

	return newModel, nil
}

func remodelJSON(c *Command, r *http.Request) Response {
	var data postModelData
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&data); err != nil {
		return BadRequest("cannot decode request body into remodel operation: %v", err)
	}
	newModel, err := modelFromData([]byte(data.NewModel))
	if err != nil {
		return BadRequest(err.Error())
	}

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

	chg, err := devicestateRemodel(st, newModel, devicestate.RemodelOptions{
		Offline: data.Offline,
	})
	if err != nil {
		return BadRequest("cannot remodel device: %v", err)
	}
	ensureStateSoon(st)

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

func readOfflineRemodelForm(form *Form) (*asserts.Model, []*uploadedContainer, *asserts.Batch, *apiError) {
	// New model
	model := form.Values["new-model"]
	if len(model) != 1 {
		return nil, nil, nil,
			BadRequest("one model assertion is expected (%d found)", len(model))
	}
	newModel, err := modelFromData([]byte(model[0]))
	if err != nil {
		return nil, nil, nil, BadRequest(err.Error())
	}

	// Snap files
	var snapFiles []*uploadedContainer
	if len(form.FileRefs["snap"]) > 0 {
		snaps, errRsp := form.GetSnapFiles()
		if errRsp != nil {
			return nil, nil, nil, errRsp
		}
		snapFiles = snaps
	}

	// Assertions
	formAsserts := form.Values["assertion"]
	batch := asserts.NewBatch(nil)
	for _, a := range formAsserts {
		_, err := batch.AddStream(strings.NewReader(a))
		if err != nil {
			return nil, nil, nil, BadRequest("cannot decode assertion: %v", err)
		}
	}

	return newModel, snapFiles, batch, nil
}

func startOfflineRemodelChange(st *state.State, newModel *asserts.Model,
	snapFiles []*uploadedContainer, batch *asserts.Batch, pathsToNotRemove *[]string) (
	*state.Change, *apiError) {

	st.Lock()
	defer st.Unlock()

	// Include assertions in the DB, we need them as soon as
	// we create the snap.SideInfo struct in sideloadSnapsInfo.
	if err := assertstate.AddBatch(st, batch,
		&asserts.CommitOptions{Precheck: true}); err != nil {
		return nil, BadRequest("error committing assertions: %v", err)
	}

	// Build snaps information. Note that here we do not set flags as we
	// expect all snaps to have assertions (although maybe we will need to
	// consider the classic snaps case in the future).
	slInfo, apiErr := sideloadSnapsInfo(st, snapFiles, sideloadFlags{})
	if apiErr != nil {
		return nil, apiErr
	}

	*pathsToNotRemove = make([]string, 0, len(slInfo.snaps))
	localSnaps := make([]snapstate.PathSnap, 0, len(slInfo.snaps))
	localComponents := make([]snapstate.PathComponent, 0)
	for _, psi := range slInfo.snaps {
		// Move file to the same name of what a downloaded one would have
		dest := filepath.Join(dirs.SnapBlobDir,
			fmt.Sprintf("%s_%s.snap", psi.info.RealName, psi.info.Revision))
		if err := os.Rename(psi.tmpPath, dest); err != nil {
			return nil, InternalError("cannot move uploaded snap file: %v", err)
		}

		// Avoid trying to remove a file that does not exist anymore
		*pathsToNotRemove = append(*pathsToNotRemove, psi.tmpPath)

		localSnaps = append(localSnaps, snapstate.PathSnap{
			SideInfo: &psi.info.SideInfo,
			Path:     dest,
		})

		for _, comp := range psi.components {
			dest := filepath.Join(dirs.SnapBlobDir,
				fmt.Sprintf("%s_%s.comp", comp.sideInfo.Component, comp.sideInfo.Revision))

			if err := os.Rename(comp.tmpPath, dest); err != nil {
				return nil, InternalError("cannot move uploaded component file: %v", err)
			}

			// Avoid trying to remove a file that does not exist anymore
			*pathsToNotRemove = append(*pathsToNotRemove, comp.tmpPath)

			localComponents = append(localComponents, snapstate.PathComponent{
				SideInfo: comp.sideInfo,
				Path:     dest,
			})
		}
	}

	for _, comp := range slInfo.components {
		dest := filepath.Join(dirs.SnapBlobDir,
			fmt.Sprintf("%s_%s.comp", comp.sideInfo.Component, comp.sideInfo.Revision))

		if err := os.Rename(comp.tmpPath, dest); err != nil {
			return nil, InternalError("cannot move uploaded component file: %v", err)
		}

		// Avoid trying to remove a file that does not exist anymore
		*pathsToNotRemove = append(*pathsToNotRemove, comp.tmpPath)

		localComponents = append(localComponents, snapstate.PathComponent{
			SideInfo: comp.sideInfo,
			Path:     dest,
		})
	}

	// Now create and start the remodel change
	chg, err := devicestateRemodel(st, newModel, devicestate.RemodelOptions{
		// since this is the codepath that parses the form, offline is implicit
		// because local snaps are being provided.
		Offline:         true,
		LocalSnaps:      localSnaps,
		LocalComponents: localComponents,
	})
	if err != nil {
		return nil, BadRequest("cannot remodel device: %v", err)
	}
	ensureStateSoon(st)

	return chg, nil
}

func remodelForm(c *Command, r *http.Request, contentTypeParams map[string]string) Response {
	boundary := contentTypeParams["boundary"]
	mpReader := multipart.NewReader(r.Body, boundary)
	form, errRsp := readForm(mpReader)
	if errRsp != nil {
		return errRsp
	}

	// we are in charge of the temp files, until they're handed off to the change
	var pathsToNotRemove []string

	// TODO: temp files are not removed if devicestate.Remodel returns an error
	// right now. change this to work how postSystemsActionForm does it.
	defer func() {
		form.RemoveAllExcept(pathsToNotRemove)
	}()

	// Read needed form data
	newModel, snapFiles, batch, errRsp := readOfflineRemodelForm(form)
	if errRsp != nil {
		return errRsp
	}

	// Create and start the change using the form data
	chg, errRsp := startOfflineRemodelChange(c.d.overlord.State(),
		newModel, snapFiles, batch, &pathsToNotRemove)
	if errRsp != nil {
		return errRsp
	}

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

// getModel gets the current model assertion using the DeviceManager
func getModel(c *Command, r *http.Request, _ *auth.UserState) Response {
	opts, err := parseHeadersFormatOptionsFromURL(r.URL.Query())
	if err != nil {
		return BadRequest(err.Error())
	}

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

	devmgr := c.d.overlord.DeviceManager()

	model, err := devmgr.Model()
	if errors.Is(err, state.ErrNoState) {
		return &apiError{
			Status:  404,
			Message: "no model assertion yet",
			Kind:    client.ErrorKindAssertionNotFound,
			Value:   "model",
		}
	}
	if err != nil {
		return InternalError("accessing model failed: %v", err)
	}

	if opts.jsonResult {
		modelJSON := clientutil.ModelAssertJSON{}

		modelJSON.Headers = model.Headers()
		if !opts.headersOnly {
			modelJSON.Body = string(model.Body())
		}

		return SyncResponse(modelJSON)
	}

	return AssertResponse([]asserts.Assertion{model}, false)
}

// getSerial gets the current serial assertion using the DeviceManager
func getSerial(c *Command, r *http.Request, _ *auth.UserState) Response {
	opts, err := parseHeadersFormatOptionsFromURL(r.URL.Query())
	if err != nil {
		return BadRequest(err.Error())
	}

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

	devmgr := c.d.overlord.DeviceManager()

	serial, err := devmgr.Serial()
	if errors.Is(err, state.ErrNoState) {
		return &apiError{
			Status:  404,
			Message: "no serial assertion yet",
			Kind:    client.ErrorKindAssertionNotFound,
			Value:   "serial",
		}
	}
	if err != nil {
		return InternalError("accessing serial failed: %v", err)
	}

	if opts.jsonResult {
		serialJSON := clientutil.ModelAssertJSON{}

		serialJSON.Headers = serial.Headers()
		if !opts.headersOnly {
			serialJSON.Body = string(serial.Body())
		}

		return SyncResponse(serialJSON)
	}

	return AssertResponse([]asserts.Assertion{serial}, false)
}

type postSerialData struct {
	Action                    string `json:"action"`
	NoRegistrationUntilReboot bool   `json:"no-registration-until-reboot"`
}

var devicestateDeviceManagerUnregister = (*devicestate.DeviceManager).Unregister

func postSerial(c *Command, r *http.Request, _ *auth.UserState) Response {
	var postData postSerialData

	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&postData); err != nil {
		return BadRequest("cannot decode serial action data from request body: %v", err)
	}
	if decoder.More() {
		return BadRequest("spurious content after serial action")
	}
	switch postData.Action {
	case "forget":
	case "":
		return BadRequest("missing serial action")
	default:
		return BadRequest("unsupported serial action %q", postData.Action)
	}

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

	devmgr := c.d.overlord.DeviceManager()

	unregOpts := &devicestate.UnregisterOptions{
		NoRegistrationUntilReboot: postData.NoRegistrationUntilReboot,
	}
	err := devicestateDeviceManagerUnregister(devmgr, unregOpts)
	if err != nil {
		return InternalError("forgetting serial failed: %v", err)
	}

	return SyncResponse(nil)
}
