package hooks

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"regexp"

	"github.com/cloudfoundry/libbuildpack"
)

const (
	appDynamicsServiceNameRegex = "app(-)?dynamics"
)

type Command interface {
	Execute(string, io.Writer, io.Writer, string, ...string) error
}

type AppdynamicsHook struct {
	libbuildpack.DefaultHook
	Log     *libbuildpack.Logger
	Command Command
}

type Plan struct {
	Credentials Credential `json:"credentials"`
	Name        string     `json:"name,omitempty"`
}

type Credential struct {
	ControllerHost   string `json:"host-name"`
	ControllerPort   string `json:"port"`
	SslEnabled       bool   `json:"ssl-enabled"`
	AccountAccessKey string `json:"account-access-key"`
	AccountName      string `json:"account-name"`
}

type VcapApplication struct {
	ApplicationName string `json:"application_name"`
	Name            string `json:"name"`
	ProcessType     string `json:"process_type"`
	Limits          struct {
		Mem int `json:"mem"`
	} `json:"limits"`
}

func (h AppdynamicsHook) getEnv(key, fallback string) string {
	if value, ok := os.LookupEnv(key); ok {
		return value
	}
	return fallback
}

func (h AppdynamicsHook) GenerateAppdynamicsScript(envVars map[string]string) string {
	var envKeys []string
	for k := range envVars {
		envKeys = append(envKeys, k)
	}
	sort.Strings(envKeys) // unnecessary but just to be deterministic for tests

	scriptContents := "# Autogenerated Appdynamics Script\n"
	for _, envKey := range envKeys {
		envStr := fmt.Sprintf("export %s=%s", envKey, envVars[envKey])
		scriptContents += "\n" + envStr
	}
	return scriptContents
}

func (h AppdynamicsHook) GenerateStartUpCommand(startCommand string) (string, error) {
	webCommands := strings.SplitN(startCommand, ":", 2)
	if len(webCommands) != 2 {
		return "", errors.New("improper format found in Procfile")
	}
	return fmt.Sprintf("web: pyagent run -- %s", webCommands[1]), nil
}

func (h AppdynamicsHook) RewriteProcFile(procFilePath string) error {
	startCommand, err := os.ReadFile(procFilePath)
	if err != nil {
		return fmt.Errorf("Error reading file %s: %v", procFilePath, err)
	}
	newCommand, err := h.GenerateStartUpCommand(string(startCommand))
	if err != nil {
		return err
	}

	if err := os.WriteFile(procFilePath, []byte(newCommand), 0666); err != nil {
		return fmt.Errorf("Error writing file %s: %v", procFilePath, err)
	}
	return nil
}

func (h AppdynamicsHook) RewriteRequirementsFile(stager *libbuildpack.Stager) error {
	h.Log.BeginStep("Rewriting Requirements file with appdynamics package")

	reqFile := filepath.Join(stager.BuildDir(), "requirements.txt")
	writeFlag := os.O_APPEND | os.O_WRONLY
	packageName := "\n" + "appdynamics"

	if exists, err := libbuildpack.FileExists(reqFile); err != nil {
		return err
	} else if !exists {
		h.Log.Info("Requirements file not found creating one with appdynamics packages")
		writeFlag = os.O_CREATE | os.O_WRONLY
		packageName = "appdynamics"
	}

	f, err := os.OpenFile(reqFile, writeFlag, 0666)
	if err != nil {
		panic(err)
	}

	defer f.Close()

	if _, err = f.WriteString(packageName); err != nil {
		panic(err)
	}
	fileContents, _ := os.ReadFile(f.Name())
	h.Log.Info(string(fileContents))

	return nil
}

func (h AppdynamicsHook) RewriteProcFileWithAppdynamics(stager *libbuildpack.Stager) error {
	h.Log.BeginStep("Rewriting ProcFile to start with Appdynamics")

	file := filepath.Join(stager.BuildDir(), "Procfile")

	if exists, _ := libbuildpack.FileExists(file); exists {
		if err := h.RewriteProcFile(file); err != nil {
			return err
		}
		fileContents, _ := os.ReadFile(file)
		h.Log.Info(string(fileContents))
	} else {
		h.Log.Info("Cannot find Procfile, skipping this step!")
	}
	return nil
}

func (h AppdynamicsHook) CreateAppDynamicsEnv(stager *libbuildpack.Stager, environmentVars map[string]string) error {
	scriptContents := h.GenerateAppdynamicsScript(environmentVars)
	h.Log.BeginStep("Writing Appdynamics Environment")
	h.Log.Debug(scriptContents)
	return stager.WriteProfileD("appdynamics.sh", scriptContents)
}

func (h AppdynamicsHook) BeforeCompile(stager *libbuildpack.Stager) error {
	if os.Getenv("APPD_AGENT") != "" {
		// APPD_AGENT is set => multibuildpack is used to configure appdynamics agent. Do nothing.
		return nil
	}

	// Some env var or something that lets us know that we are using app dynamics?

	vcapServices := os.Getenv("VCAP_SERVICES")
	services := make(map[string][]Plan)

	err := json.Unmarshal([]byte(vcapServices), &services)
	if err != nil {
		h.Log.Debug("Could not unmarshall VCAP_SERVICES JSON exiting: %v", err)
		return nil
	}

	appdServiceName, appdynamicsPlan, err := getAppDynamicsServiceName(services, h.Log)

	if appdServiceName == "" {
		return nil
	}

	h.Log.BeginStep("Setting up Appdynamics")

	vcapApplication := os.Getenv("VCAP_APPLICATION")
	application := VcapApplication{}

	err = json.Unmarshal([]byte(vcapApplication), &application)
	if err != nil {
		h.Log.Debug("Could not unmarshall VCAP_APPLICATION JSON %v", err)
		h.Log.Debug("VCAP_APPLICATION: %s", vcapApplication)
	}

	sslFlag := "off"

	credentials := appdynamicsPlan.Credentials

	if credentials.SslEnabled {
		sslFlag = "on"
	}

	appdEnv := map[string]string{
		"APPD_APP_NAME":           h.getEnv("APPD_APP_NAME", application.ApplicationName),
		"APPD_TIER_NAME":          h.getEnv("APPD_TIER_NAME", application.ApplicationName),
		"APPD_NODE_NAME":          h.getEnv("APPD_NODE_NAME", application.ApplicationName),
		"APPD_CONTROLLER_HOST":    credentials.ControllerHost,
		"APPD_CONTROLLER_PORT":    credentials.ControllerPort,
		"APPD_ACCOUNT_ACCESS_KEY": credentials.AccountAccessKey,
		"APPD_ACCOUNT_NAME":       credentials.AccountName,
		"APPD_SSL_ENABLED":        sslFlag,
	}

	if err := h.RewriteRequirementsFile(stager); err != nil {
		h.Log.Error("Could not write requirements file with Appdynamics packages: %v", err)
		return err
	}

	if err := h.CreateAppDynamicsEnv(stager, appdEnv); err != nil {
		h.Log.Error("Could not create Appdynamics environment: %v", err)
		return err
	}

	if err := h.RewriteProcFileWithAppdynamics(stager); err != nil {
		h.Log.Error("Could not rewrite procfile with Appdynamics start command: %v", err)
		return err
	}

	return nil
}

func getAppDynamicsServiceName(services map[string][]Plan, log *libbuildpack.Logger) (string, Plan, error) {
	// Check if there is a service with name appdynamics or app-dynamics
	for serviceName, servicePlans := range services {
		if isAppDynamicsServiceName(serviceName) {
			appdServiceName := serviceName
			logDeprecationWarning(log)
			return appdServiceName, servicePlans[0], nil
		}
	}

	// If this line is reached, no service with name appdynamics or app-dynamics was found. Check for user-provided services
	userProvidedServices, keyExists := services["user-provided"]
	if !keyExists {
		return "", Plan{}, nil
	}

	for _, plan := range userProvidedServices {
		if isAppDynamicsServiceName(plan.Name) {
			appdServiceName := plan.Name
			logDeprecationWarning(log)
			return appdServiceName, plan, nil
		}
	}

	// If this line is reached, no service with name appdynamics or app-dynamics was found in either the services and user-provided services. Return empty string, empty plan and nil error
	return "", Plan{}, nil
}

func isAppDynamicsServiceName(serviceName string) bool {
	match, err := regexp.MatchString(appDynamicsServiceNameRegex, serviceName)
	if err != nil {
		return false
	}

	return match
}

func logDeprecationWarning(log *libbuildpack.Logger) {
	log.Warning("[DEPRECATION WARNING]:")
	log.Warning("Please use AppDynamics extension buildpack for Python Application instrumentation")
	log.Warning("for more details: https://docs.pivotal.io/partners/appdynamics/multibuildpack.html")
}

func init() {
	logger := libbuildpack.NewLogger(os.Stdout)
	command := &libbuildpack.Command{}
	libbuildpack.AddHook(AppdynamicsHook{
		Log:     logger,
		Command: command,
	})
}
