//  Copyright 2023 Google Inc. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

// Package command facilitates calling commands within the guest-agent.
package command

import (
	"context"
	"encoding/json"
	"fmt"
	"io"

	"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg"
)

// Get returns the current command monitor which can be used to register command handlers.
func Get() *Monitor {
	return cmdMonitor
}

// Handler functions are the business logic of commands. They must process json
// encoded as a byte slice which contains a Command field and optional arbitrary
// data, and return json which contains a Status, StatusMessage, and optional
// arbitrary data (again encoded as a byte slice). Returned errors will be
// passed onto the command requester.
type Handler func([]byte) ([]byte, error)

// Request is the basic request structure. Command determines which handler the
// request is routed to. Callers may set additional arbitrary fields.
type Request struct {
	Command string
}

// Response is the basic response structure. Handlers may set additional
// arbitrary fields.
type Response struct {
	// Status code for the request. Meaning is defined by the caller, but
	// conventially zero is success.
	Status int
	// StatusMessage is an optional message defined by the caller. Should generally
	// help a human understand what happened.
	StatusMessage string
}

var (
	// CmdNotFoundError is return when there is no handler for the request command
	CmdNotFoundError = Response{
		Status:        101,
		StatusMessage: "Could not find a handler for the requested command",
	}
	// BadRequestError is returned for invalid or unparseable JSON
	BadRequestError = Response{
		Status:        102,
		StatusMessage: "Could not parse valid JSON from request",
	}
	// ConnError is returned for errors from the underlying communication protocol
	ConnError = Response{
		Status:        103,
		StatusMessage: "Connection error",
	}
	// TimeoutError is returned when the timeout period elapses before valid JSON is receieved
	TimeoutError = Response{
		Status:        104,
		StatusMessage: "Connection timeout before reading valid request",
	}
	// HandlerError is returned when the handler function returns an non-nil error. The status message will be replaced with the returnd error string.
	HandlerError = Response{
		Status:        105,
		StatusMessage: "The command handler encountered an error processing your request",
	}
	// InternalErrorCode is the error code for internal command server errors. Returned when failing to marshal a response.
	InternalErrorCode = 106
	internalError     = []byte(`{"Status":106,"StatusMessage":"The command server encountered an internal error trying to respond to your request"}`)
)

// RegisterHandler registers f as the handler for cmd. If a command.Server has
// been initialized, it will be signalled to start listening for commands.
func (m *Monitor) RegisterHandler(cmd string, f Handler) error {
	m.handlersMu.Lock()
	defer m.handlersMu.Unlock()
	if _, ok := m.handlers[cmd]; ok {
		return fmt.Errorf("cmd %s is already handled", cmd)
	}
	m.handlers[cmd] = f
	return nil
}

// UnregisterHandler clears the handlers for cmd. If a command.Server has been
// intialized and there are no more handlers registered, the server will be
// signalled to stop listening for commands.
func (m *Monitor) UnregisterHandler(cmd string) error {
	m.handlersMu.Lock()
	defer m.handlersMu.Unlock()
	if _, ok := m.handlers[cmd]; !ok {
		return fmt.Errorf("cmd %s is not registered", cmd)
	}
	delete(m.handlers, cmd)
	return nil
}

// SendCommand sends a command request over the configured pipe.
func SendCommand(ctx context.Context, req []byte) []byte {
	pipe := cfg.Get().Unstable.CommandPipePath
	if pipe == "" {
		pipe = DefaultPipePath
	}
	return SendCmdPipe(ctx, pipe, req)
}

// SendCmdPipe sends a command request over a specific pipe. Most callers
// should use SendCommand() instead.
func SendCmdPipe(ctx context.Context, pipe string, req []byte) []byte {
	conn, err := dialPipe(ctx, pipe)
	if err != nil {
		if b, err := json.Marshal(ConnError); err == nil {
			return b
		}
		return internalError
	}
	i, err := conn.Write(req)
	if err != nil || i != len(req) {
		if b, err := json.Marshal(ConnError); err == nil {
			return b
		}
		return internalError
	}
	data, err := io.ReadAll(conn)
	if err != nil {
		if b, err := json.Marshal(ConnError); err == nil {
			return b
		}
		return internalError
	}
	return data
}
