package commands

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"os"
	"os/signal"
	"strings"
	"syscall"

	"github.com/urfave/cli/v2"

	"github.com/photoprism/photoprism/internal/entity"
	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/rnd"
	"github.com/photoprism/photoprism/pkg/txt"
)

// PasswdCommand configures the command name, flags, and action.
var PasswdCommand = &cli.Command{
	Name:      "passwd",
	Usage:     "Changes the local account password of a registered user",
	ArgsUsage: "[username]",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "show",
			Aliases: []string{"s"},
			Usage:   "shows bcrypt hash of new password",
		},
		&cli.BoolFlag{
			Name:    "remove",
			Aliases: []string{"rm"},
			Usage:   "removes password to disable local authentication",
		},
	},
	Action: passwdAction,
}

// passwdAction changes the password of the user specified as command argument.
func passwdAction(ctx *cli.Context) error {
	id := clean.Username(ctx.Args().First())

	// Name or UID provided?
	if id == "" {
		return cli.ShowSubcommandHelp(ctx)
	}

	conf, err := InitConfig(ctx)

	_, cancel := context.WithCancel(context.Background())
	defer cancel()

	if err != nil {
		return err
	}

	conf.InitDb()
	defer conf.Shutdown()

	// Find user record.
	var m *entity.User

	if rnd.IsUID(id, entity.UserUID) {
		m = entity.FindUserByUID(id)
	} else {
		m = entity.FindUserByName(id)
	}

	if m == nil {
		return fmt.Errorf("user %s not found", clean.LogQuote(id))
	} else if m.IsDeleted() {
		return fmt.Errorf("user %s has been deleted", clean.LogQuote(id))
	}

	// Remove account password if the --remove or --rm flag is set.
	if ctx.Bool("remove") {
		err = m.DeletePassword()

		if err == nil {
			log.Infof("password for %s has been removed", clean.Log(m.Username()))
		}

		return err
	}

	// Otherwise, update the account password.
	log.Infof("please enter a new password for %s (%d-%d characters)\n", clean.Log(m.Username()), entity.PasswordLength, txt.ClipPassword)

	newPassword := getPassword("New Password: ")

	if len([]rune(newPassword)) < entity.PasswordLength {
		return fmt.Errorf("password must have at least %d characters", entity.PasswordLength)
	} else if len(newPassword) > txt.ClipPassword {
		return fmt.Errorf("password must have less than %d characters", txt.ClipPassword)
	}

	retypePassword := getPassword("Retype Password: ")

	if newPassword != retypePassword {
		return errors.New("passwords did not match, please try again")
	}

	if err = m.SetPassword(newPassword); err != nil {
		return err
	}

	// Show bcrypt password hash?
	if pw := entity.FindPassword(m.UserUID); ctx.Bool("show") && pw != nil {
		log.Infof("password for %s has been set to %s\n", clean.Log(m.Username()), pw.Hash)
	} else {
		log.Infof("password for %s has been changed\n", clean.Log(m.Username()))
	}

	return nil
}

// License: MIT Open Source
// Copyright (c) Joe Linoff 2016
// Go code to prompt for password using only standard packages by utilizing syscall.ForkExec() and syscall.Wait4().
// Correctly resets terminal echo after ^C interrupts.
//
// techEcho() - turns terminal echo on or off.
func termEcho(on bool) {
	// Common settings and variables for both stty calls.
	attrs := syscall.ProcAttr{
		Dir:   "",
		Env:   []string{},
		Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
		Sys:   nil}
	var ws syscall.WaitStatus
	cmd := "echo"
	if !on {
		cmd = "-echo"
	}

	// Enable/disable echoing.
	pid, err := syscall.ForkExec( //nolint:gosec // uses fixed binary and arguments
		"/bin/stty",
		[]string{"stty", cmd},
		&attrs)
	if err != nil {
		panic(err)
	}

	// Wait for the stty process to complete.
	_, err = syscall.Wait4(pid, &ws, 0, nil)
	if err != nil {
		panic(err)
	}
}

// getPassword - Prompt for password.
func getPassword(prompt string) string {
	fmt.Print(prompt)

	// Catch a ^C interrupt.
	// Make sure that we reset term echo before exiting.
	signalChannel := make(chan os.Signal, 1)
	signal.Notify(signalChannel, os.Interrupt)
	go func() {
		for range signalChannel {
			fmt.Println("")
			termEcho(true)
			os.Exit(1)
		}
	}()

	// Echo is disabled, now grab the data.
	termEcho(false) // disable terminal echo
	reader := bufio.NewReader(os.Stdin)
	text, err := reader.ReadString('\n')
	termEcho(true) // always re-enable terminal echo
	fmt.Println("")
	if err != nil {
		// The terminal has been reset, go ahead and exit.
		fmt.Println("ERROR:", err.Error())
		os.Exit(1)
	}
	return strings.TrimSpace(text)
}
