/*
Copyright 2023 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 hanainsights implements the one time execution mode for HANA
// insights.
package hanainsights

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

	"flag"
	"github.com/google/subcommands"
	"github.com/GoogleCloudPlatform/sapagent/internal/databaseconnector"
	"github.com/GoogleCloudPlatform/sapagent/internal/hanainsights/preprocessor"
	"github.com/GoogleCloudPlatform/sapagent/internal/hanainsights/ruleengine"
	"github.com/GoogleCloudPlatform/sapagent/internal/onetime"
	"github.com/GoogleCloudPlatform/sapagent/internal/usagemetrics"
	rpb "github.com/GoogleCloudPlatform/sapagent/protos/hanainsights"
	"github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/gce"
	"github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/log"
)

// HANAInsights has args for hanainsights subcommands.
type HANAInsights struct {
	project, host, port, sid                        string
	user, password, passwordSecret, hdbuserstoreKey string
	gceService                                      onetime.GCEInterface
	status                                          bool
	db                                              *databaseconnector.DBHandle
	help                                            bool
	logLevel, logPath                               string
	oteLogger                                       *onetime.OTELogger
}

const (
	localInsightsDir = "/var/log/google-cloud-sap-agent/"
)

type writeFile func(string, []byte, os.FileMode) error

type createDir func(string, os.FileMode) error

// Name implements the subcommand interface for hanainsights.
func (*HANAInsights) Name() string { return "hanainsights" }

// Synopsis implements the subcommand interface for hanainsights.
func (*HANAInsights) Synopsis() string { return "invoke HANA local insights workflow" }

// Usage implements the subcommand interface for hanainsights.
func (*HANAInsights) Usage() string {
	return `Usage: hanainsights -project=<project-name> -host=<hostname> -port=<port-number> -sid=<HANA-SID> -user=<user-name>
	[-password=<passwd> | -password-secret=<secret-name>] [-h] [-loglevel=<debug|info|warn|error>] [-log-path=<log-path>]` + "\n"
}

// SetFlags implements the subcommand interface for hanainsights.
func (h *HANAInsights) SetFlags(fs *flag.FlagSet) {
	fs.StringVar(&h.project, "project", "", "GCP project. (required)")
	fs.StringVar(&h.host, "host", "", "HANA host. (required if hdbuserstore-key not provided)")
	fs.StringVar(&h.port, "port", "", "HANA port. (required if hdbuserstore-key not provided)")
	fs.StringVar(&h.sid, "sid", "", "HANA SID. (required)")
	fs.StringVar(&h.user, "user", "", "HANA username. (required)")
	fs.StringVar(&h.password, "password", "", "HANA password. (discouraged - use password-secret instead)")
	fs.StringVar(&h.passwordSecret, "password-secret", "", "Secret Manager secret name that holds HANA Password")
	fs.StringVar(&h.hdbuserstoreKey, "hdbuserstore-key", "", "HANA Userstore key specific to HANA instance")
	fs.StringVar(&h.logPath, "log-path", "", "The log path to write the log file (optional), default value is /var/log/google-cloud-sap-agent/hanainsights.log")
	fs.BoolVar(&h.help, "h", false, "Display help")
	fs.StringVar(&h.logLevel, "loglevel", "info", "Sets the logging level for a log file")
}

// Execute implements the subcommand interface for hanainsights.
func (h *HANAInsights) Execute(ctx context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus {
	_, _, exitStatus, completed := onetime.Init(ctx, onetime.InitOptions{
		Name:     h.Name(),
		Help:     h.help,
		LogLevel: h.logLevel,
		LogPath:  h.logPath,
		Fs:       f,
	}, args...)
	if !completed {
		return exitStatus
	}

	return h.Run(ctx, onetime.CreateRunOptions(nil, false), args...)
}

// Run executes the command and returns the status.
func (h *HANAInsights) Run(ctx context.Context, runOpts *onetime.RunOptions, args ...any) subcommands.ExitStatus {
	h.oteLogger = onetime.CreateOTELogger(runOpts.DaemonMode)
	exitStatus := h.hanaInsightsHandler(ctx, gce.NewGCEClient, os.WriteFile, os.MkdirAll)
	if exitStatus == subcommands.ExitFailure {
		h.oteLogger.LogUsageError(usagemetrics.HANAInsightsOTEFailure)
	}
	return exitStatus
}

func (h *HANAInsights) validateParameters(os string) error {
	switch {
	case os == "windows":
		return fmt.Errorf("hanainsights is only supported on Linux systems")
	case (h.hdbuserstoreKey == "" && (h.host == "" || h.port == "")) || h.sid == "" || h.user == "":
		return fmt.Errorf("%s", "required arguments not passed. Usage:"+h.Usage())
	case h.password == "" && h.passwordSecret == "" && h.hdbuserstoreKey == "":
		return fmt.Errorf("%s", "either -password, -password-secret or -hdbuserstore-key is required. Usage:"+h.Usage())
	}

	log.Logger.Info("Parameter validation successful.")
	return nil
}

func (h *HANAInsights) hanaInsightsHandler(ctx context.Context, gceServiceCreator onetime.GCEServiceFunc, wf writeFile, c createDir) subcommands.ExitStatus {
	var err error
	if err = h.validateParameters(runtime.GOOS); err != nil {
		h.oteLogger.LogMessageToConsole(err.Error())
		return subcommands.ExitUsageError
	}

	h.oteLogger.LogUsageAction(usagemetrics.HANAInsightsOTEStarted)
	h.gceService, err = gceServiceCreator(ctx)
	if err != nil {
		h.oteLogger.LogErrorToFileAndConsole(ctx, "ERROR: Failed to create GCE service", err)
		return subcommands.ExitFailure
	}

	if h.hdbuserstoreKey != "" {
		h.oteLogger.LogUsageAction(usagemetrics.HANAInsightsOTEUserstoreKey)
	}
	dbp := databaseconnector.Params{
		Username:       h.user,
		Password:       h.password,
		PasswordSecret: h.passwordSecret,
		HDBUserKey:     h.hdbuserstoreKey,
		Host:           h.host,
		Port:           h.port,
		GCEService:     h.gceService,
		Project:        h.project,
		SID:            h.sid,
	}
	if h.db, err = databaseconnector.CreateDBHandle(ctx, dbp); err != nil {
		h.oteLogger.LogErrorToFileAndConsole(ctx, "ERROR: Failed to connect to database", err)
		return subcommands.ExitFailure
	}

	rules, err := preprocessor.ReadRules(preprocessor.RuleFilenames)
	if err != nil {
		log.CtxLogger(ctx).Errorw("Failure to read HANA rules", "error", err)
		return subcommands.ExitFailure
	}

	insights, err := ruleengine.Run(ctx, h.db, rules)
	if err != nil {
		h.oteLogger.LogErrorToFileAndConsole(ctx, "ERROR: Failure in rule engine", err)
		return subcommands.ExitFailure
	}
	log.CtxLogger(ctx).Infow("Generating HANA insights", insights)
	if err = generateLocalHANAInsights(rules, insights, wf, c); err != nil {
		log.CtxLogger(ctx).Errorw("ERROR: Failed to generate local HANA insights", "error", err)
		return subcommands.ExitFailure
	}
	h.oteLogger.LogUsageAction(usagemetrics.HANAInsightsOTEFinished)
	return subcommands.ExitSuccess
}

// generateLocalHANAInsights will create the HANA Insights in a markdown file stored under the
// directory /var/log/google-cloud-sap-agent/.
func generateLocalHANAInsights(rules []*rpb.Rule, insights ruleengine.Insights, wf writeFile, c createDir) error {
	write := false
	sb := new(strings.Builder)
	fmt.Fprintf(sb, "# Recommendations\n")
	ruleWiseRecs := buildRuleWiseRecs(rules)
	for _, rule := range rules {
		if _, ok := insights[rule.GetId()]; ok {
			content, writeRule := checkForRecommendation(insights, rule, ruleWiseRecs)
			write = write || writeRule
			fmt.Fprint(sb, content)
		}
	}
	file := fmt.Sprintf("%s/local-hana-insights-%s.md", localInsightsDir, time.Now().UTC().Format(time.RFC3339))
	contentBytes := []byte(sb.String())
	var err error
	if write {
		if err = createDirHelper(c, localInsightsDir, os.FileMode(0755)); err != nil {
			return err
		}
		err = writeFileHelper(wf, file, contentBytes, os.FileMode(0644))
	}
	return err
}

func checkForRecommendation(insights ruleengine.Insights, rule *rpb.Rule, ruleWiseRecs map[string]map[string]*rpb.Recommendation) (string, bool) {
	write := false
	vrs := insights[rule.GetId()]
	content := new(strings.Builder)
	for _, vr := range vrs {
		if vr.Result {
			recommendation := ruleWiseRecs[rule.GetId()][vr.RecommendationID]
			fmt.Fprintf(content, "## %s\n", rule.GetId())
			fmt.Fprintf(content, "### Actions\n")
			for _, action := range recommendation.GetActions() {
				fmt.Fprintf(content, "- %s\n", action.GetDescription())
				write = true
			}
			fmt.Fprintf(content, "### References\n")
			for _, reference := range recommendation.GetReferences() {
				fmt.Fprintf(content, "- %s\n", reference)
			}
		}
	}
	if !write {
		return "", write
	}
	return content.String(), write
}

func writeFileHelper(w writeFile, name string, content []byte, perm os.FileMode) error {
	return w(name, content, perm)
}

func createDirHelper(c createDir, path string, perm os.FileMode) error {
	return c(path, perm)
}

func buildRuleWiseRecs(rules []*rpb.Rule) map[string]map[string]*rpb.Recommendation {
	result := make(map[string]map[string]*rpb.Recommendation)
	for _, rule := range rules {
		result[rule.GetId()] = make(map[string]*rpb.Recommendation)
		for _, recommendation := range rule.GetRecommendations() {
			result[rule.GetId()][recommendation.GetId()] = recommendation
		}
	}
	return result
}
