/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 commands

import (
	"errors"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/spf13/cobra"

	"github.com/apache/openwhisk-cli/wski18n"
	"github.com/apache/openwhisk-client-go/whisk"
)

// sdkCmd represents the sdk command
var sdkCmd = &cobra.Command{
	Use:   "sdk",
	Short: wski18n.T("work with the sdk"),
}

type sdkInfo struct {
	UrlPath   string
	FileName  string
	isGzTar   bool
	IsGzip    bool
	IsZip     bool
	IsTar     bool
	Unpack    bool
	UnpackDir string
}

var sdkMap map[string]*sdkInfo

const SDK_DOCKER_COMPONENT_NAME string = "docker"
const SDK_IOS_COMPONENT_NAME string = "ios"
const BASH_AUTOCOMPLETE_FILENAME string = "wsk_cli_bash_completion.sh"

var sdkInstallCmd = &cobra.Command{
	Use:           "install COMPONENT",
	Short:         wski18n.T("install SDK artifacts"),
	Long:          wski18n.T("install SDK artifacts, where valid COMPONENT values are docker, ios, and bashauto"),
	SilenceUsage:  true,
	SilenceErrors: true,
	PreRunE:       SetupClientConfig,
	RunE: func(cmd *cobra.Command, args []string) error {
		var err error
		if len(args) != 1 {
			whisk.Debug(whisk.DbgError, "Invalid number of arguments: %d\n", len(args))
			errStr := wski18n.T("The SDK component argument is missing. One component (docker, ios, or bashauto) must be specified")
			werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
			return werr
		}
		component := strings.ToLower(args[0])
		switch component {
		case "docker":
			err = dockerInstall()
		case "ios":
			err = iOSInstall()
		case "bashauto":
			if Flags.sdk.stdout {
				if err = WskCmd.GenBashCompletion(os.Stdout); err != nil {
					whisk.Debug(whisk.DbgError, "GenBashCompletion error: %s\n", err)
					errStr := wski18n.T("Unable to output bash command completion {{.err}}",
						map[string]interface{}{"err": err})
					werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
					return werr
				}
			} else {
				err = WskCmd.GenBashCompletionFile(BASH_AUTOCOMPLETE_FILENAME)
				if err != nil {
					whisk.Debug(whisk.DbgError, "GenBashCompletionFile('%s`) error: %s\n", BASH_AUTOCOMPLETE_FILENAME, err)
					errStr := wski18n.T("Unable to generate '{{.name}}': {{.err}}",
						map[string]interface{}{"name": BASH_AUTOCOMPLETE_FILENAME, "err": err})
					werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
					return werr
				}
				fmt.Printf(
					wski18n.T("bash_completion_msg",
						map[string]interface{}{"name": BASH_AUTOCOMPLETE_FILENAME}))
			}
		default:
			whisk.Debug(whisk.DbgError, "Invalid component argument '%s'\n", component)
			errStr := wski18n.T("The SDK component argument '{{.component}}' is invalid. Valid components are docker, ios and bashauto",
				map[string]interface{}{"component": component})
			err = whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
		}

		if err != nil {
			return err
		}
		return nil
	},
}

func dockerInstall() error {
	var err error

	targetFile := sdkMap[SDK_DOCKER_COMPONENT_NAME].FileName
	if _, err = os.Stat(targetFile); err == nil {
		whisk.Debug(whisk.DbgError, "os.Stat reports file '%s' exists\n", targetFile)
		errStr := wski18n.T("The file '{{.name}}' already exists.  Delete it and retry.",
			map[string]interface{}{"name": targetFile})
		werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
		return werr
	}

	if err = sdkInstall(SDK_DOCKER_COMPONENT_NAME); err != nil {
		whisk.Debug(whisk.DbgError, "sdkInstall(%s) failed: %s\n", SDK_DOCKER_COMPONENT_NAME, err)
		errStr := wski18n.T("The {{.component}} SDK installation failed: {{.err}}",
			map[string]interface{}{"component": SDK_DOCKER_COMPONENT_NAME, "err": err})
		werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
		return werr
	}

	fmt.Println(wski18n.T("The docker skeleton is now installed at the current directory."))
	return nil
}

func iOSInstall() error {
	var err error

	if err = sdkInstall(SDK_IOS_COMPONENT_NAME); err != nil {
		whisk.Debug(whisk.DbgError, "sdkInstall(%s) failed: %s\n", SDK_IOS_COMPONENT_NAME, err)
		errStr := wski18n.T("The {{.component}} SDK installation failed: {{.err}}",
			map[string]interface{}{"component": SDK_IOS_COMPONENT_NAME, "err": err})
		werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
		return werr
	}

	fmt.Printf(
		wski18n.T("Downloaded OpenWhisk iOS starter app. Unzip '{{.name}}' and open the project in Xcode.\n",
			map[string]interface{}{"name": sdkMap[SDK_IOS_COMPONENT_NAME].FileName}))
	return nil
}

func sdkInstall(componentName string) error {
	targetFile := sdkMap[componentName].FileName
	if _, err := os.Stat(targetFile); err == nil {
		whisk.Debug(whisk.DbgError, "os.Stat reports file '%s' exists\n", targetFile)
		errStr := wski18n.T("The file '{{.name}}' already exists.  Delete it and retry.",
			map[string]interface{}{"name": targetFile})
		werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
		return werr
	}

	resp, err := Client.Sdks.Install(sdkMap[componentName].UrlPath)
	if err != nil {
		whisk.Debug(whisk.DbgError, "Client.Sdks.Install(%s) failed: %s\n", sdkMap[componentName].UrlPath, err)
		errStr := wski18n.T("Unable to retrieve '{{.urlpath}}' SDK: {{.err}}",
			map[string]interface{}{"urlpath": sdkMap[componentName].UrlPath, "err": err})
		werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
		return werr
	}

	if resp.Body == nil {
		whisk.Debug(whisk.DbgError, "SDK Install HTTP response has no body\n")
		errStr := wski18n.T("Server failed to send the '{{.component}}' SDK: {{.err}}",
			map[string]interface{}{"name": componentName, "err": err})
		werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_NETWORK, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
		return werr
	}

	// Create the SDK file
	sdkfile, err := os.Create(targetFile)
	if err != nil {
		whisk.Debug(whisk.DbgError, "os.Create(%s) failure: %s\n", targetFile, err)
		errStr := wski18n.T("Error creating SDK file '{{.name}}': {{.err}}",
			map[string]interface{}{"name": targetFile, "err": err})
		werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
		return werr
	}

	// Read the HTTP response body and write it to the SDK file
	whisk.Debug(whisk.DbgInfo, "Reading SDK file from HTTP response body\n")
	_, err = io.Copy(sdkfile, resp.Body)
	if err != nil {
		whisk.Debug(whisk.DbgError, "io.Copy() of resp.Body into sdkfile failure: %s\n", err)
		errStr := wski18n.T("Error copying server response into file: {{.err}}",
			map[string]interface{}{"err": err})
		werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
		sdkfile.Close()
		return werr

	}
	sdkfile.Close() // Don't use 'defer' since this file might need to be deleted after unpack

	// At this point, the entire file is downloaded from the server
	// Check if there is any special post-download processing (i.e. unpack)
	if sdkMap[componentName].Unpack {
		// Make sure the target directory does not already exist
		defer os.Remove(targetFile)
		targetdir := sdkMap[componentName].UnpackDir
		if _, err = os.Stat(targetdir); err == nil {
			whisk.Debug(whisk.DbgError, "os.Stat reports that directory '%s' exists\n", targetdir)
			errStr := wski18n.T("The directory '{{.name}}' already exists.  Delete it and retry.",
				map[string]interface{}{"name": targetdir})
			werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
			return werr
		}

		// If the packed SDK is a .tgz file, unpack it in two steps
		// 1. UnGzip into temp .tar file
		// 2. Untar the contents into the current folder
		if sdkMap[componentName].isGzTar {
			whisk.Debug(whisk.DbgInfo, "unGzipping downloaded file\n")
			err := unpackGzip(targetFile, "temp.tar")
			if err != nil {
				whisk.Debug(whisk.DbgError, "unpackGzip(%s,temp.tar) failure: %s\n", targetFile, err)
				errStr := wski18n.T("Error unGzipping file '{{.name}}': {{.err}}",
					map[string]interface{}{"name": targetFile, "err": err})
				werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
				return werr
			}
			defer os.Remove("temp.tar")

			whisk.Debug(whisk.DbgInfo, "unTarring unGzipped file\n")
			err = unpackTar("temp.tar")
			if err != nil {
				whisk.Debug(whisk.DbgError, "unpackTar(temp.tar) failure: %s\n", err)
				errStr := wski18n.T("Error untarring file '{{.name}}': {{.err}}",
					map[string]interface{}{"name": "temp.tar", "err": err})
				werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
				return werr
			}
		}

		// Future SDKs may require other unpacking procedures not yet covered here....
	}

	return nil
}

func init() {
	sdkInstallCmd.Flags().BoolVarP(&Flags.sdk.stdout, "stdout", "s", false, wski18n.T("prints bash command completion script to stdout"))

	sdkCmd.AddCommand(sdkInstallCmd)

	sdkMap = make(map[string]*sdkInfo)
	sdkMap["docker"] = &sdkInfo{UrlPath: "blackbox.tar.gz", FileName: "blackbox.tar.gz", isGzTar: true, Unpack: true, UnpackDir: "dockerSkeleton"}
	sdkMap["ios"] = &sdkInfo{UrlPath: "OpenWhiskIOSStarterApp.zip", FileName: "OpenWhiskIOSStarterApp.zip", IsZip: true, Unpack: false}
}
