// Copyright 2016-2021, Pulumi Corporation.
//
// 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
//
//     http://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 workspace

import (
	"fmt"
	"os"
	"strings"

	"github.com/pulumi/pulumi/sdk/v3/go/common/encoding"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)

// readFileStripUTF8BOM wraps os.ReadFile and also strips the UTF-8 Byte-order Mark (BOM) if present.
func readFileStripUTF8BOM(path string) ([]byte, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	// Strip UTF-8 BOM bytes if present to avoid problems with downstream parsing.
	// References:
	//   https://github.com/spkg/bom
	//   https://en.wikipedia.org/wiki/Byte_order_mark
	if len(b) >= 3 &&
		b[0] == 0xef &&
		b[1] == 0xbb &&
		b[2] == 0xbf {
		b = b[3:]
	}

	return b, nil
}

// Rewrite config values to make them namespaced. Using the project name as the default namespace
// for example:
//
//	config:
//	  instanceSize: t3.micro
//
// is valid configuration and will be rewritten in the form
//
//	config:
//	  {projectName}:instanceSize:t3.micro
func stackConfigNamespacedWithProject(project *Project, projectStack map[string]interface{}) map[string]interface{} {
	if project == nil {
		// return the original config if we don't have a project
		return projectStack
	}

	config, ok := projectStack["config"]
	if ok {
		configAsMap, isMap := config.(map[string]interface{})
		if isMap {
			modifiedConfig := make(map[string]interface{})
			for key, value := range configAsMap {
				if strings.Contains(key, ":") {
					// key is already namespaced
					// use it as is
					modifiedConfig[key] = value
				} else {
					namespacedKey := fmt.Sprintf("%s:%s", project.Name, key)
					modifiedConfig[namespacedKey] = value
				}
			}

			projectStack["config"] = modifiedConfig
			return projectStack
		}
	}

	return projectStack
}

// LoadProject reads a project definition from a file.
func LoadProject(path string) (*Project, error) {
	contract.Requiref(path != "", "path", "must not be empty")

	marshaller, err := marshallerForPath(path)
	if err != nil {
		return nil, fmt.Errorf("can not read '%s': %w", path, err)
	}

	b, err := readFileStripUTF8BOM(path)
	if err != nil {
		return nil, fmt.Errorf("could not read '%s': %w", path, err)
	}

	return LoadProjectBytes(b, path, marshaller)
}

// LoadProjectBytes reads a project definition from a byte slice.
func LoadProjectBytes(b []byte, path string, marshaller encoding.Marshaler) (*Project, error) {
	var raw interface{}
	err := marshaller.Unmarshal(b, &raw)
	if err != nil {
		return nil, fmt.Errorf("could not unmarshal '%s': %w", path, err)
	}

	err = ValidateProject(raw)
	if err != nil {
		return nil, fmt.Errorf("could not validate '%s': %w", path, err)
	}

	// just before marshalling, we will rewrite the config values
	projectDef, err := SimplifyMarshalledProject(raw)
	if err != nil {
		return nil, err
	}

	projectDef, rewriteError := RewriteConfigPathIntoStackConfigDir(projectDef)
	if rewriteError != nil {
		return nil, rewriteError
	}

	projectDef = RewriteShorthandConfigValues(projectDef)
	modifiedProject, _ := marshaller.Marshal(projectDef)

	var project Project
	err = marshaller.Unmarshal(modifiedProject, &project)
	if err != nil {
		return nil, err
	}

	err = project.Validate()
	if err != nil {
		return nil, fmt.Errorf("could not unmarshal '%s': %w", path, err)
	}

	project.raw = b
	return &project, nil
}

// LoadProjectStack reads a stack definition from a file.
func LoadProjectStack(project *Project, path string) (*ProjectStack, error) {
	contract.Requiref(path != "", "path", "must not be empty")

	marshaller, err := marshallerForPath(path)
	if err != nil {
		return nil, err
	}

	b, err := readFileStripUTF8BOM(path)
	if os.IsNotExist(err) {
		defaultProjectStack := ProjectStack{
			Config: make(config.Map),
		}
		return &defaultProjectStack, nil
	} else if err != nil {
		return nil, err
	}

	return LoadProjectStackBytes(project, b, path, marshaller)
}

// LoadProjectStack reads a stack definition from a byte slice.
func LoadProjectStackBytes(
	project *Project,
	b []byte,
	path string,
	marshaller encoding.Marshaler,
) (*ProjectStack, error) {
	var projectStackRaw interface{}
	err := marshaller.Unmarshal(b, &projectStackRaw)
	if err != nil {
		return nil, err
	}

	if projectStackRaw == nil {
		// for example when reading an empty stack file
		defaultProjectStack := ProjectStack{
			Config: make(config.Map),
		}
		return &defaultProjectStack, nil
	}

	simplifiedStackForm, err := SimplifyMarshalledProject(projectStackRaw)
	if err != nil {
		return nil, err
	}

	// rewrite config values to make them namespaced
	// for example:
	//     config:
	//       instanceSize: t3.micro
	//
	// is valid configuration and will be rewritten in the form
	//
	//     config:
	//       {projectName}:instanceSize: t3.micro
	projectStackWithNamespacedConfig := stackConfigNamespacedWithProject(project, simplifiedStackForm)
	modifiedProjectStack, _ := marshaller.Marshal(projectStackWithNamespacedConfig)

	var projectStack ProjectStack
	err = marshaller.Unmarshal(modifiedProjectStack, &projectStack)
	if err != nil {
		return nil, err
	}

	if projectStack.Config == nil {
		projectStack.Config = make(config.Map)
	}

	projectStack.raw = b
	return &projectStack, nil
}

// LoadPluginProject reads a plugin project definition from a file.
func LoadPluginProject(path string) (*PluginProject, error) {
	contract.Requiref(path != "", "path", "must not be empty")

	marshaller, err := marshallerForPath(path)
	if err != nil {
		return nil, err
	}

	b, err := readFileStripUTF8BOM(path)
	if err != nil {
		return nil, err
	}

	var pluginProject PluginProject
	err = marshaller.Unmarshal(b, &pluginProject)
	if err != nil {
		return nil, err
	}

	err = pluginProject.Validate()
	if err != nil {
		return nil, err
	}

	return &pluginProject, nil
}

// LoadPolicyPack reads a policy pack definition from a file.
func LoadPolicyPack(path string) (*PolicyPackProject, error) {
	contract.Requiref(path != "", "path", "must not be empty")

	marshaller, err := marshallerForPath(path)
	if err != nil {
		return nil, err
	}

	b, err := readFileStripUTF8BOM(path)
	if err != nil {
		return nil, err
	}

	var policyPackProject PolicyPackProject
	err = marshaller.Unmarshal(b, &policyPackProject)
	if err != nil {
		return nil, err
	}

	err = policyPackProject.Validate()
	if err != nil {
		return nil, err
	}

	return &policyPackProject, nil
}