//  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
//
//     https://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 manager

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/GoogleCloudPlatform/galog"
	acpb "github.com/GoogleCloudPlatform/google-guest-agent/internal/acp/proto/google_guest_agent/acp"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
	pcpb "github.com/GoogleCloudPlatform/google-guest-agent/pkg/proto/plugin_comm"
)

const (
	// healthCheckRequest is the request to get the health status of the plugin.
	// This is empty as its periodic health check and not specialized status request
	// for some well-defined thing. [GetStatusRequest] defines the convention
	// and request in further detail.
	healthCheckRequest = ""
	// numOfLines is the number of lines to read from the plugin log file when a
	// crash is detected.
	numOfLines = 30
)

// PluginMonitor is a monitor for a plugin which implements scheduler job
// interface. It runs a health check and restarts the plugin if found unhealthy.
type PluginMonitor struct {
	// plugin is the Plugin this monitor is monitoring.
	plugin *Plugin
	// interval is the interval for scheduler to run a health check.
	interval time.Duration
	// alreadyStopped is a boolean to track if the plugin has already been stopped.
	// or is already stopping. This is to prevent log spam in case of plugin
	// crash/stop.
	alreadyStopped bool
}

// NewPluginMonitor creates a new plugin monitor.
func NewPluginMonitor(plugin *Plugin, interval time.Duration) *PluginMonitor {
	return &PluginMonitor{
		plugin:   plugin,
		interval: interval,
	}
}

// ID returns the plugin monitor ID.
func (m *PluginMonitor) ID() string {
	return fmt.Sprintf("plugin_%s_monitor", m.plugin.FullName())
}

// MetricName returns the metric name for the job.
func (m *PluginMonitor) MetricName() acpb.GuestAgentModuleMetric_Metric {
	return acpb.GuestAgentModuleMetric_MODULE_UNSPECIFIED
}

// Interval returns the interval for scheduler to run this job.
func (m *PluginMonitor) Interval() (time.Duration, bool) {
	return m.interval, true
}

// ShouldEnable informs scheduler if this job should be scheduled job or not.
// Always return true to have plugin monitoring.
func (m *PluginMonitor) ShouldEnable(ctx context.Context) bool {
	return true
}

// Run runs the plugin health check. Always return true to continue monitoring.
func (m *PluginMonitor) Run(ctx context.Context) (bool, error) {
	s := m.healthCheck(ctx)
	if s != nil {
		// Set the health info for the plugin.
		m.plugin.setHealthInfo(&healthCheck{responseCode: s.GetCode(), messages: s.GetResults(), timestamp: time.Now()})
	}
	return true, nil
}

// readPluginLogs reads the last 10 lines of the plugin log file and returns
// the string representation of the error logs. It also truncates the log file
// to avoid reading the same error logs again.
func readPluginLogs(path string) string {
	errLogs, readErr := file.ReadLastNLines(path, numOfLines)
	if readErr != nil {
		galog.Warnf("Unable to read plugin log file %s: %v", path, readErr)
		return ""
	}

	if err := os.Truncate(path, 0); err != nil {
		galog.Errorf("Failed to truncate plugin log file %s: %v", path, err)
	}

	return strings.Join(errLogs, "\n")
}

// healthCheck returns the health status of the plugin.
// If the plugin is not healthy, it will restart the plugin.
func (m *PluginMonitor) healthCheck(ctx context.Context) *pcpb.Status {
	currentState := m.plugin.State()
	if currentState == acpb.CurrentPluginStates_STOPPING || currentState == acpb.CurrentPluginStates_STOPPED {
		// Plugin is explicitly being stopped. Do not perform health check as it
		// would fail and try to restart the plugin. Health check runs in a separate
		// goroutine and could race with stop request.
		if !m.alreadyStopped {
			galog.Infof("Plugin %s is stopping or stopped, skipping health check", m.plugin.FullName())
			m.alreadyStopped = true
		}
		return nil
	}
	m.alreadyStopped = false

	s, err := m.plugin.GetStatus(ctx, healthCheckRequest)
	if err == nil {
		// GetStatus() did not return error, simply return the response.
		return s
	}

	galog.Warnf("Plugin health check failed for %s: %v", m.ID(), err)

	sendEvent(ctx, m.plugin, acpb.PluginEventMessage_PLUGIN_CRASHED, fmt.Sprintf("Plugin health check failed: [%v]. Plugin logs: %s", err, readPluginLogs(m.plugin.logfile())))
	m.plugin.setState(acpb.CurrentPluginStates_CRASHED)
	if err := connectOrReLaunch(ctx, m.plugin); err != nil {
		// Each crash or failed attempt would send an ACS event.
		// ACS will send a message to remove plugin/stop trying based on failed attempts.
		// Just log the error and keep retrying to launch a plugin on next execution.
		galog.Errorf("Plugin monitor %s failed to relaunch plugin: %v", m.ID(), err)
	}
	return nil
}
