//  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"
	"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/ps"
	tpb "google.golang.org/protobuf/types/known/timestamppb"
)

// Metric is a struct to store the plugin's current memory and CPU usage at a
// specific timestamp.
type Metric struct {
	// timestamp is the time when the metric is recorded.
	timestamp *tpb.Timestamp
	// memoryUsage is the memory usage of the plugin at the timestamp in kB.
	memoryUsage int64
	// cpuUsage is the percent CPU usage of the plugin at the timestamp.
	cpuUsage float32
}

// PluginMetrics is a struct to monitor and store a plugin's metrics.
type PluginMetrics struct {
	// plugin is the plugin to be monitored.
	plugin *Plugin
	// interval is the interval of getting the plugin's metrics.
	interval time.Duration
}

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

// ID returns the ID of the plugin metric.
func (p *PluginMetrics) ID() string {
	return fmt.Sprintf("%s-metrics", p.plugin.FullName())
}

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

// Interval returns the interval of the getting plugin metrics.
func (p *PluginMetrics) Interval() (time.Duration, bool) {
	return p.interval, true
}

// ShouldEnable returns true if this job should be scheduled or not by the
// scheduler.
func (*PluginMetrics) ShouldEnable(ctx context.Context) bool {
	return true
}

// Run gets and caches the plugin's metrics.
func (p *PluginMetrics) Run(ctx context.Context) (bool, error) {
	currentState := p.plugin.State()
	if currentState != acpb.CurrentPluginStates_RUNNING {
		// Skip metric collection if process is not in running state. Reading
		// [/proc] for example on Linux would fail anyways.
		return true, fmt.Errorf("plugin %q found in state %v, skipping metric collection", p.plugin.FullName(), currentState)
	}

	pid := p.plugin.pid()
	if pid == 0 {
		// Stop metric collection if pid found as 0. This can happen if the plugin
		// is being stopped/removed and previous scheduled metric collection job
		// happened to attempt metric collection at the same time.
		return false, fmt.Errorf("plugin %q is being stopped/removed, got pid 0, skipping metric collection", p.plugin.FullName())
	}

	galog.V(2).Debugf("Getting CPU and memory usage for plugin %s", p.plugin.FullName())
	currentCPUUsage, err := ps.CPUUsage(ctx, pid)
	if err != nil {
		galog.Warnf("Failed to get CPU usage for plugin %s: %v", p.plugin.FullName(), err)
	}
	currentMemoryUsage, err := ps.Memory(pid)
	if err != nil {
		galog.Warnf("Failed to get memory usage for plugin %s: %v", p.plugin.FullName(), err)
	}
	galog.V(3).Debugf("CPU usage for plugin %s: %f%%", p.plugin.FullName(), currentCPUUsage)
	galog.V(3).Debugf("Memory usage for plugin %s: %d kB", p.plugin.FullName(), currentMemoryUsage)

	p.plugin.RuntimeInfo.metricsMu.Lock()
	defer p.plugin.RuntimeInfo.metricsMu.Unlock()

	// Cache the metrics.
	p.plugin.RuntimeInfo.metrics.Add(Metric{
		timestamp:   tpb.Now(),
		memoryUsage: int64(currentMemoryUsage),
		cpuUsage:    float32(currentCPUUsage),
	})

	return true, nil
}
