/*
 * prompt.go - Functions for handling user input and options
 *
 * Copyright 2017 Google Inc.
 * Author: Joe Richey (joerichey@google.com)
 *
 * 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 main

import (
	"fmt"
	"log"
	"os"
	"os/user"
	"strconv"
	"strings"

	"github.com/pkg/errors"

	"github.com/google/fscrypt/actions"
	"github.com/google/fscrypt/metadata"
	"github.com/google/fscrypt/util"
)

const (
	// Suffixes for questions with a yes or no default
	defaultYesSuffix = " [Y/n] "
	defaultNoSuffix  = " [y/N] "
)

// Descriptions for each of the protector sources
var sourceDescriptions = map[metadata.SourceType]string{
	metadata.SourceType_pam_passphrase:    "Your login passphrase",
	metadata.SourceType_custom_passphrase: "A custom passphrase",
	metadata.SourceType_raw_key:           "A raw 256-bit key",
}

// askQuestion asks the user a yes or no question. Returning a boolean on a
// successful answer and an error if there was not a response from the user.
// Returns the defaultChoice on empty input (or in quiet mode).
func askQuestion(question string, defaultChoice bool) (bool, error) {
	// If in quiet mode, we just use the default
	if quietFlag.Value {
		return defaultChoice, nil
	}
	// Loop until failure or valid input
	for {
		if defaultChoice {
			fmt.Print(question + defaultYesSuffix)
		} else {
			fmt.Print(question + defaultNoSuffix)
		}

		input, err := util.ReadLine()
		if err != nil {
			return false, err
		}

		switch strings.ToLower(input) {
		case "y", "yes":
			return true, nil
		case "n", "no":
			return false, nil
		case "":
			return defaultChoice, nil
		}
	}
}

// askConfirmation asks the user for confirmation of a specific action. An error
// is returned if the user declines or IO fails.
func askConfirmation(question string, defaultChoice bool, warning string) error {
	// All confirmations are "yes" if we are forcing.
	if forceFlag.Value {
		return nil
	}

	// Defaults of "no" require forcing.
	if !defaultChoice {
		if quietFlag.Value {
			return ErrNoDestructiveOps
		}
	}

	if warning != "" && !quietFlag.Value {
		fmt.Println(wrapText("WARNING: "+warning, 0))
	}

	confirmed, err := askQuestion(question, defaultChoice)
	if err != nil {
		return err
	}
	if !confirmed {
		return ErrCanceled
	}
	return nil
}

// usernameFromID returns the username for the provided UID. If the UID does not
// correspond to a user or the username is blank, an error is returned.
func usernameFromID(uid int64) (string, error) {
	u, err := user.LookupId(strconv.Itoa(int(uid)))
	if err != nil || u.Username == "" {
		return "", errors.Wrapf(ErrUnknownUser, "uid %d", uid)
	}
	return u.Username, nil
}

// formatUsername either returns the username for the provided UID, or a string
// containing the error for unknown UIDs.
func formatUsername(uid int64) string {
	username, err := usernameFromID(uid)
	if err != nil {
		return fmt.Sprintf("[%v]", err)
	}
	return username
}

// formatInfo gives a string description of metadata.ProtectorData.
func formatInfo(data actions.ProtectorInfo) string {
	switch data.Source() {
	case metadata.SourceType_pam_passphrase:
		return "login protector for " + formatUsername(data.UID())
	case metadata.SourceType_custom_passphrase:
		return fmt.Sprintf("custom protector %q", data.Name())
	case metadata.SourceType_raw_key:
		return fmt.Sprintf("raw key protector %q", data.Name())
	default:
		panic(ErrInvalidSource)
	}
}

// promptForName gets a name from user input (or flags) and returns it.
func promptForName(ctx *actions.Context) (string, error) {
	// A name flag means we do not need to prompt
	if nameFlag.Value != "" {
		return nameFlag.Value, nil
	}

	// Don't ask for a name if we do not need it
	if quietFlag.Value || ctx.Config.Source == metadata.SourceType_pam_passphrase {
		return "", nil
	}

	for {
		fmt.Print("Enter a name for the new protector: ")
		name, err := util.ReadLine()
		if err != nil {
			return "", err
		}
		if name != "" {
			return name, nil
		}
	}
}

// promptForSource gets a source type from user input (or flags) and modifies
// the context to use that source.
func promptForSource(ctx *actions.Context) error {
	// A source flag overrides everything else.
	if sourceFlag.Value != "" {
		val, ok := metadata.SourceType_value[sourceFlag.Value]
		if !ok || val == 0 {
			return ErrInvalidSource
		}
		ctx.Config.Source = metadata.SourceType(val)
		return nil
	}

	// Just use the default in quiet mode
	if quietFlag.Value {
		return nil
	}

	// We print all the sources with their number, description, and name.
	fmt.Println("The following protector sources are available:")
	for idx := 1; idx < len(metadata.SourceType_value); idx++ {
		source := metadata.SourceType(idx)
		description := sourceDescriptions[source]
		fmt.Printf("%d - %s (%s)\n", idx, description, source)
	}

	for {
		fmt.Printf("Enter the source number for the new protector [%d - %s]: ",
			ctx.Config.Source, ctx.Config.Source)
		input, err := util.ReadLine()
		if err != nil {
			return err
		}

		// Use the default if the user just hits enter
		if input == "" {
			return nil
		}

		// Check for a valid index, reprompt if invalid.
		index, err := strconv.Atoi(input)
		if err == nil && index >= 1 && index < len(metadata.SourceType_value) {
			ctx.Config.Source = metadata.SourceType(index)
			return nil
		}
	}
}

// promptForKeyFile returns an open file that should be used to create or unlock
// a raw_key protector. Be sure to close the file when done.
func promptForKeyFile(prompt string) (*os.File, error) {
	// If specified on the command line, we only try no open it once.
	if keyFileFlag.Value != "" {
		return os.Open(keyFileFlag.Value)
	}
	if quietFlag.Value {
		return nil, ErrSpecifyKeyFile
	}

	// Prompt for a valid path until we get a file we can open.
	for {
		fmt.Print(prompt)
		filename, err := util.ReadLine()
		if err != nil {
			return nil, err
		}
		file, err := os.Open(filename)
		if err == nil {
			return file, nil
		}
		fmt.Println(err)
	}

}

// promptForProtector, given a non-empty list of protector options, uses user
// input to select the desired protector. If there is only one option to choose
// from, that protector is automatically selected.
func promptForProtector(options []*actions.ProtectorOption) (int, error) {
	numOptions := len(options)
	log.Printf("selecting from %s", pluralize(numOptions, "protector"))

	// Get the number of load errors.
	numLoadErrors := 0
	for _, option := range options {
		if option.LoadError != nil {
			log.Printf("when loading option: %v", option.LoadError)
			numLoadErrors++
		}
	}

	if numLoadErrors == numOptions {
		return 0, ErrAllLoadsFailed
	}
	if numOptions == 1 {
		return 0, nil
	}
	if quietFlag.Value {
		return 0, ErrSpecifyProtector
	}

	// List all of the protector options which did not have a load error.
	fmt.Println("The available protectors are: ")
	for idx, option := range options {
		if option.LoadError != nil {
			continue
		}

		description := fmt.Sprintf("%d - %s", idx, formatInfo(option.ProtectorInfo))
		if option.LinkedMount != nil {
			description += fmt.Sprintf(" (linked protector on %q)", option.LinkedMount.Path)
		}
		fmt.Println(description)
	}

	if numLoadErrors > 0 {
		loadWarning := fmt.Sprintf("NOTE: %d of the %d protectors failed to load. ", numLoadErrors, numOptions)
		fmt.Print(wrapText(loadWarning+loadHelpText, 0) + "\n")
	}

	for {
		fmt.Print("Enter the number of protector to use: ")
		input, err := util.ReadLine()
		if err != nil {
			return 0, err
		}

		// Check for a valid index, reprompt if invalid.
		index, err := strconv.Atoi(input)
		if err == nil && index >= 0 && index < len(options) {
			return index, nil
		}
	}
}

// optionFn is an actions.OptionFunc which handles selecting an option for a
// specific policy. This is either done interactively, or by deferring to the
// protectorFlag.
func optionFn(policyDescriptor string, options []*actions.ProtectorOption) (int, error) {
	// If we have an unlock-with flag, we directly select the specified
	// protector to unlock the policy.
	if unlockWithFlag.Value != "" {
		log.Printf("optionFn(%s) w/ unlock flag", policyDescriptor)
		protector, err := getProtectorFromFlag(unlockWithFlag.Value, nil)
		if err != nil {
			return 0, err
		}

		for idx, option := range options {
			if option.Descriptor() == protector.Descriptor() {
				return idx, nil
			}
		}
		return 0, &actions.ErrNotProtected{PolicyDescriptor: policyDescriptor,
			ProtectorDescriptor: protector.Descriptor()}
	}

	log.Printf("optionFn(%s)", policyDescriptor)
	return promptForProtector(options)
}
