//  Copyright 2024 Google LLC
//
//  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 main implements ACS CLI for testing dynamic plugins manually.
package main

import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/GoogleCloudPlatform/galog"
	acmpb "github.com/GoogleCloudPlatform/google-guest-agent/internal/acp/proto/google_guest_agent/acp"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/acs/testserver"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/logger"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
	dpb "google.golang.org/protobuf/types/known/durationpb"
)

const (
	// galogShutdownTimeout is the period of time we should wait for galog to
	// shutdown.
	galogShutdownTimeout = time.Second
)

var (
	// archiveFile is the plugin archive that Guest Agent will be served for
	// downloading/installing a dynamic plugin. Normally this is served by GCS
	// signed URL but for local testing CLI stands up a HTTP server which serves
	// the plugin archive for download.
	archiveFile = flag.String("archive_file", "", "Path to the plugin archive file")
	// cliLogFile is the log file where Guest Agent sent messages will be captured.
	// These are agent sent messages on ACS channel.
	cliLogFile = flag.String("logfile", filepath.Join(os.TempDir(), "acs_cli.log"), "Path to the cli log file")
	// acsHost is the address to start test ACS server on. Make sure
	// agent [instance_configs.cfg] has a [ACS] section with [host] option set
	// with this same address to make sure Guest is communicating on same
	// overridden address.
	acsHost = flag.String("acs_host", filepath.Join(os.TempDir(), "acs_host.sock"), "Path to the ACS address")
)

// serveArchive starts a [httptest.Server] which serves the plugin archive.
func serveArchive() (*httptest.Server, error) {
	bytes, err := os.ReadFile(*archiveFile)
	if err != nil {
		return nil, fmt.Errorf("os.ReadFile(%s) failed: %w", *archiveFile, err)
	}

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write(bytes)
	}))

	return ts, nil
}

// captureLogs captures agent sent messages. Periodically it looks for any
// new messages on the channel and writes to a [cliLogFile] if found.
func captureLogs(ctx context.Context, s *testserver.Server) {
	ticker := time.NewTicker(time.Second * 5)
	defer ticker.Stop()
	read := 0

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			msgs := s.AgentSentMessages()
			write := msgs[read:]
			for _, msg := range write {
				galog.Info(msg.String())
			}
			read = len(msgs)
		}
	}
}

// requireFlags checks if any of the required flags is unset.
func requireFlags() {
	switch "" {
	case *archiveFile:
		galog.Fatal("-archive_file flag is not set")
	case *cliLogFile:
		galog.Fatal("-logfile flag is not set")
	case *acsHost:
		galog.Fatal("-acs_host flag is not set")
	}
}

func main() {
	flag.Parse()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	logOpts := logger.Options{
		Ident:   filepath.Base(os.Args[0]),
		LogFile: *cliLogFile,
		Level:   4,
	}

	if err := logger.Init(ctx, logOpts); err != nil {
		fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err)
		os.Exit(1)
	}
	defer galog.Shutdown(galogShutdownTimeout)

	requireFlags()

	if err := cfg.Load(nil); err != nil {
		galog.Fatalf("Failed to load Guest Agent configuration: %v", err)
	}

	cksum, err := file.SHA256FileSum(*archiveFile)
	if err != nil {
		galog.Fatalf("Failed to compute SHA256 hash of plugin archive: %v", err)
	}

	server, err := serveArchive()
	if err != nil {
		galog.Fatalf("Failed to serve plugin archive via httptest.Server: %v", err)
	}
	defer server.Close()

	url := server.URL
	galog.Infof("Serving plugin archive on: %s", url)

	s := testserver.NewTestServer(*acsHost)
	if err := s.Start(); err != nil {
		galog.Fatalf("Failed to start test ACS server on %s: %v", *acsHost, err)
	}

	// Start capturing logs periodically. This go routine exits when context is
	// cancelled.
	go captureLogs(ctx, s)

	reader := bufio.NewReader(os.Stdin)

	for {
		fmt.Print("Enter command(install, remove, apply <text_config>, or list): ")
		text, err := reader.ReadString('\n')
		if err != nil {
			galog.Fatalf("Failed to read command: %v", err)
		}

		text = strings.TrimSpace(text)
		inputs := strings.Split(text, " ")
		var config string
		if len(inputs) == 2 {
			config = inputs[1]
		}

		switch inputs[0] {
		case "install":
			install(s, url, cksum)
		case "remove":
			remove(s)
		case "list":
			list(s)
		case "apply":
			apply(s, config)
		default:
			fmt.Println("Unknown command:", text)
		}
	}
}

func apply(s *testserver.Server, config string) {
	req := &acmpb.ConfigurePluginStates{
		ConfigurePlugins: []*acmpb.ConfigurePluginStates_ConfigurePlugin{
			&acmpb.ConfigurePluginStates_ConfigurePlugin{
				Action: acmpb.ConfigurePluginStates_APPLY,
				Plugin: &acmpb.ConfigurePluginStates_Plugin{
					Name:       "test_plugin",
					RevisionId: "1",
				},
				Manifest: &acmpb.ConfigurePluginStates_Manifest{
					Config: &acmpb.ConfigurePluginStates_Manifest_StringConfig{StringConfig: config},
				},
			},
		},
	}

	if config == "" {
		req.GetConfigurePlugins()[0].GetManifest().Config = nil
	}

	labels := map[string]string{"message_type": "agent_controlplane.ConfigurePluginStates"}
	if err := s.SendToAgent(req, labels); err != nil {
		galog.Errorf("Failed to send ConfigurePluginStates apply request: %v", err)
	}
}

// list sends [ListPluginStates] request to Guest Agent.
func list(s *testserver.Server) {
	req := &acmpb.ListPluginStates{}
	labels := map[string]string{"message_type": "agent_controlplane.ListPluginStates"}
	if err := s.SendToAgent(req, labels); err != nil {
		galog.Errorf("Failed to send ListPluginStates request: %v", err)
	}
}

// remove sends [ConfigurePluginStates] remove request to Guest Agent.
func remove(s *testserver.Server) {
	req := &acmpb.ConfigurePluginStates{
		ConfigurePlugins: []*acmpb.ConfigurePluginStates_ConfigurePlugin{
			&acmpb.ConfigurePluginStates_ConfigurePlugin{
				Action: acmpb.ConfigurePluginStates_REMOVE,
				Plugin: &acmpb.ConfigurePluginStates_Plugin{
					Name:       "test_plugin",
					RevisionId: "1",
				}},
		},
	}

	labels := map[string]string{"message_type": "agent_controlplane.ConfigurePluginStates"}
	if err := s.SendToAgent(req, labels); err != nil {
		galog.Errorf("Failed to send ConfigurePluginStates remove request: %v", err)
	}
}

// install sends [ConfigurePluginStates] install request to Guest Agent.
func install(s *testserver.Server, url string, cksum string) {
	req := &acmpb.ConfigurePluginStates{
		ConfigurePlugins: []*acmpb.ConfigurePluginStates_ConfigurePlugin{
			&acmpb.ConfigurePluginStates_ConfigurePlugin{
				Action: acmpb.ConfigurePluginStates_INSTALL,
				Plugin: &acmpb.ConfigurePluginStates_Plugin{
					Name:         "test_plugin",
					RevisionId:   "1",
					GcsSignedUrl: url,
					EntryPoint:   "basic_plugin",
					Checksum:     cksum,
				},
				Manifest: &acmpb.ConfigurePluginStates_Manifest{
					DownloadTimeout:      &dpb.Duration{Seconds: 60},
					DownloadAttemptCount: 2,
					StartTimeout:         &dpb.Duration{Seconds: 30},
					StartAttemptCount:    2,
					StopTimeout:          &dpb.Duration{Seconds: 30},
				},
			},
		},
	}

	labels := map[string]string{"message_type": "agent_controlplane.ConfigurePluginStates"}
	if err := s.SendToAgent(req, labels); err != nil {
		galog.Errorf("Failed to send ConfigurePluginStates install request: %v", err)
	}
}
