// Copyright 2018 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 main

import (
	"context"
	"encoding/json"
	"reflect"
	"runtime"
	"slices"
	"sync/atomic"

	"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg"
	"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/run"
	"github.com/GoogleCloudPlatform/guest-agent/utils"
	"github.com/GoogleCloudPlatform/guest-logging-go/logger"
)

const diagnosticsCmd = `C:\Program Files\Google\Compute Engine\diagnostics\diagnostics.exe`

var (
	diagnosticsRegKey   = "Diagnostics"
	diagnosticsDisabled = false
	// Indicate whether an existing job is runing to collect logs
	// 0 -> not running, 1 -> running
	isDiagnosticsRunning int32 = 0
)

type diagnosticsEntry struct {
	SignedURL string
	ExpireOn  string
	Trace     bool
}

type diagnosticsMgr struct {
	// fakeWindows forces Disabled to run as if it was running in a windows system.
	// mostly target for unit tests.
	fakeWindows bool
}

func (d *diagnosticsMgr) Diff(ctx context.Context) (bool, error) {
	return !reflect.DeepEqual(newMetadata.Instance.Attributes.Diagnostics, oldMetadata.Instance.Attributes.Diagnostics), nil
}

func (d *diagnosticsMgr) Timeout(ctx context.Context) (bool, error) {
	return false, nil
}

func (d *diagnosticsMgr) Disabled(ctx context.Context) (bool, error) {
	var disabled bool
	config := cfg.Get()

	if !d.fakeWindows && runtime.GOOS != "windows" {
		return true, nil
	}

	defer func() {
		if disabled != diagnosticsDisabled {
			diagnosticsDisabled = disabled
			logStatus("diagnostics", disabled)
		}
	}()

	// Diagnostics are opt-in and enabled by default.
	if config.Diagnostics != nil {
		return !config.Diagnostics.Enable, nil
	}

	if newMetadata.Instance.Attributes.EnableDiagnostics != nil {
		return !*newMetadata.Instance.Attributes.EnableDiagnostics, nil
	}
	if newMetadata.Project.Attributes.EnableDiagnostics != nil {
		return !*newMetadata.Project.Attributes.EnableDiagnostics, nil
	}
	return diagnosticsDisabled, nil
}

func (d *diagnosticsMgr) Set(ctx context.Context) error {
	logger.Infof("Diagnostics: logs export requested.")
	diagnosticsEntries, err := readRegMultiString(regKeyBase, diagnosticsRegKey)
	if err != nil && err != errRegNotExist {
		return err
	}

	strEntry := newMetadata.Instance.Attributes.Diagnostics
	if slices.Contains(diagnosticsEntries, strEntry) {
		return nil
	}
	diagnosticsEntries = append(diagnosticsEntries, strEntry)

	var entry diagnosticsEntry
	if err := json.Unmarshal([]byte(strEntry), &entry); err != nil {
		return err
	}

	expired, _ := utils.CheckExpired(entry.ExpireOn)
	if entry.SignedURL == "" || expired {
		return nil
	}

	args := []string{
		"-signedUrl",
		entry.SignedURL,
	}
	if entry.Trace {
		args = append(args, "-trace")
	}
	// If no existing running job, set it to 1 and block other requests
	if !atomic.CompareAndSwapInt32(&isDiagnosticsRunning, 0, 1) {
		logger.Infof("Diagnostics: reject the request, as an existing process is collecting logs from the system")
		return nil
	}

	go func() {
		logger.Infof("Diagnostics: collecting logs from the system.")
		res := run.WithCombinedOutput(ctx, diagnosticsCmd, args...)
		logger.Infof("%v", res.Combined)
		if res.ExitCode != 0 {
			logger.Warningf("Error collecting logs: %v", res.Error())
		}
		// Job is done, unblock the following requests
		atomic.SwapInt32(&isDiagnosticsRunning, 0)
	}()

	return writeRegMultiString(regKeyBase, diagnosticsRegKey, diagnosticsEntries)
}
