package patterns

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/user"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"

	"github.com/godbus/dbus"
	authenticate "github.com/linuxdeepin/go-dbus-factory/com.deepin.daemon.authenticate"
	login1 "github.com/linuxdeepin/go-dbus-factory/org.freedesktop.login1"
	"github.com/linuxdeepin/go-lib/dbusutil"
	"github.com/linuxdeepin/go-lib/procfs"
)

var patternChangerUserName = "deepin-password-admin"                              //#nosec G101
const resetPatternDialogPath = "/usr/lib/dde-control-center/reset-pattern-dialog" //#nosec G101

type Reset interface {
	Clean()                          // clean all stuff
	RunDialog() (*os.Process, error) // run the dialog
	Wait()                           // wait dialog close
}

type Caller struct {
	Name     string
	FullName string
	Uid      string
	Locale   string
	Sender   string
	display  string
	xauth    string
	app      string
	wayland  string // wayland socket, almost "/run/user/{UID}/wayland-0"
	proc     procfs.Process
	user     *user.User // 启动更改密码过程的用户, 不是被改密码的用户
}

type ResetManager struct {
	sysConn *dbus.Conn
	caller  *Caller
	selfUid int
	selfGid int
	cmd     *exec.Cmd
	stdin   io.WriteCloser
	stdout  io.ReadCloser
	stderr  io.ReadCloser
	oldFd   [2]*os.File
	newFd   [2]*os.File
}

type ResetManagerX struct {
	*ResetManager
	xauthDir string
}

type ResetManagerWayland struct {
	*ResetManager
	waylandSocket string
}

func init() {
	fileInfo, err := os.Stat(resetPatternDialogPath)
	if err != nil {
		logger.Warning(err)
		return
	}

	stat := fileInfo.Sys().(*syscall.Stat_t)
	user, err := user.LookupId(fmt.Sprintf("%d", stat.Uid))
	if err != nil {
		logger.Warning(err)
		return
	}
	patternChangerUserName = user.Username
}

// copy from golang 1.17, comment out some code because of unexported member
//
// String returns a human-readable description of c.
// It is intended only for debugging.
// In particular, it is not suitable for use as input to a shell.
// The output of String may vary across Go releases.
func cmdToString(c *exec.Cmd) string {
	// if c.lookPathErr != nil {
	// // failed to resolve path; report the original requested path (plus args)
	// return strings.Join(c.Args, " ")
	// }
	// report the exact executable path (plus args)
	b := new(strings.Builder)
	b.WriteString(c.Path)
	for _, a := range c.Args[1:] {
		b.WriteByte(' ')
		b.WriteString(a)
	}
	return b.String()
}

func getWaylandSocket(envs procfs.EnvVars) (waylandSocket string) {
	run, found := envs.Lookup("XDG_RUNTIME_DIR")
	if !found {
		return ""
	}
	display, found := envs.Lookup("WAYLAND_DISPLAY")
	if !found {
		return ""
	}
	return path.Join(run, display)
}

func (c *Caller) init(service *dbusutil.Service) error {
	pid, err := service.GetConnPID(c.Sender)
	if err != nil {
		return fmt.Errorf("fail to get sender PID: %v", err)
	}
	proc := procfs.Process(pid)
	login1Manager := login1.NewManager(service.Conn())
	sessionPath, err := login1Manager.GetSessionByPID(0, pid)
	if err != nil {
		return fmt.Errorf("fail to get sender session path: %v", err)
	}
	session, err := login1.NewSession(service.Conn(), sessionPath)
	if err != nil {
		return fmt.Errorf("fail to create session object proxy: %v", err)
	}
	display, err := session.Display().Get(0)
	if err != nil {
		return fmt.Errorf("fail to get sender display: %v", err)
	}
	exe, err := proc.Exe()
	if err != nil {
		return fmt.Errorf("get sender exe error: %v", err)
	}
	// 只允许来自控制中心, 锁屏和 greetter 的调用
	var app string
	switch exe {
	case "/usr/bin/dde-control-center":
		app = "control-center"
	case "/usr/bin/dde-lock":
		app = "lock"
	case "/usr/bin/lightdm-deepin-greeter":
		app = "greeter"
	default:
		return fmt.Errorf("set password with Union ID called by %s, which is not allow", exe)
	}
	status, err := proc.Status()
	if err != nil {
		return fmt.Errorf("failed to get sender status: %v", err)
	}
	uids, err := status.Uids()
	if err != nil {
		return fmt.Errorf("failed to get sender euid: %v", err)
	}
	uid := uids[1] // EUID
	user, err := user.LookupId(fmt.Sprint(uid))
	if err != nil {
		return fmt.Errorf("failed to lookup sender username: %v", err)
	}
	envs, err := proc.Environ()
	if err != nil {
		return fmt.Errorf("failed to get sender environment variables: %v", err)
	}
	sessionType, err := session.Type().Get(0)
	if err != nil {
		return fmt.Errorf("fail to get sender session type: %v", err)
	}
	waylandSocket := getWaylandSocket(envs)
	if sessionType == "wayland" && waylandSocket == "" {
		return fmt.Errorf("failed to get wayland socket")
	}
	xauth, found := envs.Lookup("XAUTHORITY")
	if !found {
		// $HOME/.Xauthority is default authority file if XAUTHORITY isn't defined.
		xauth = filepath.Join(user.HomeDir, ".Xauthority")
	}

	c.display = display
	c.xauth = xauth
	c.proc = proc
	c.user = user
	c.app = app
	c.wayland = waylandSocket

	return nil
}

func NewReset(service *dbusutil.Service, caller *Caller) (Reset, error) {
	err := caller.init(service)
	if err != nil {
		return nil, err
	}
	if caller.wayland == "" {
		return NewResetManagerX(caller)
	} else {
		return NewResetManagerWayland(caller)
	}
}

func NewResetManager(caller *Caller) (*ResetManager, error) {
	resetUser, err := user.Lookup(patternChangerUserName)
	if err != nil {
		return nil, fmt.Errorf("fail to get user info of %s: %v", patternChangerUserName, err)
	}

	selfUid, err := strconv.Atoi(resetUser.Uid)
	if err != nil {
		return nil, err
	}

	selfGid, err := strconv.Atoi(resetUser.Gid)
	if err != nil {
		return nil, err
	}

	// 检验可执行文件的属性
	fInfo, err := os.Stat(resetPatternDialogPath)
	if err != nil {
		return nil, err
	}

	fileSys := fInfo.Sys()
	stat, ok := fileSys.(*syscall.Stat_t)
	if !ok {
		return nil, err
	}

	if fInfo.Mode() != 0500 || stat.Uid != uint32(selfUid) {
		return nil, errors.New("permission check failed")
	}

	oldFd := [2]*os.File{}
	newFd := [2]*os.File{}
	defer func() {
		for _, f := range oldFd {
			if f != nil {
				f.Close()
			}
		}
		for _, f := range newFd {
			if f != nil {
				f.Close()
			}
		}
	}()

	// create pipe for dialog to send shadowed pattern
	oldFd[0], oldFd[1], err = os.Pipe()
	if err != nil {
		return nil, err
	}

	newFd[0], newFd[1], err = os.Pipe()
	if err != nil {
		return nil, err
	}

	// -u 用户名
	// -a 应用类型
	// --old 读取旧pattern
	// --new 写入新pattern
	cmd := exec.Command(
		"runuser", "-u", resetUser.Username, "--",
		resetPatternDialogPath, "-u", caller.Name, "-f", caller.FullName, "-a", caller.app,
		"--old", "3", "--new", "4") //#nosec G204

	cmd.ExtraFiles = []*os.File{oldFd[0], newFd[1]}

	stdin, err := cmd.StdinPipe()
	if err != nil {
		return nil, err
	}

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}

	stderr, err := cmd.StderrPipe()
	if err != nil {
		return nil, err
	}

	cmd.Env = append(
		cmd.Env,
		"LANG="+caller.Locale,
		"LANGUAGE="+caller.Locale,
	)

	ret := &ResetManager{
		caller:  caller,
		selfUid: selfUid,
		selfGid: selfGid,
		cmd:     cmd,
		stdin:   stdin,
		stdout:  stdout,
		stderr:  stderr,
		oldFd:   oldFd,
		newFd:   newFd,
	}

	oldFd = [2]*os.File{}
	newFd = [2]*os.File{}

	return ret, nil
}

func (m *ResetManager) RunDialog() (*os.Process, error) {
	logger.Debugf("run %v, envs: %v", cmdToString(m.cmd), m.cmd.Env)
	m.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	err := m.cmd.Start()
	if err != nil {
		return m.cmd.Process, fmt.Errorf("fail to start reset-pattern-dialog: %v", err)
	}

	// 写入当前加密密码
	wpipeWriter := bufio.NewWriter(m.oldFd[1])
	dm, err := GetDataManager(m.caller.Name)
	if err == nil {
		_, err = wpipeWriter.WriteString(dm.GetPattern())
	}
	if err != nil {
		logger.Warning(err)
	}
	wpipeWriter.WriteByte('\n')
	wpipeWriter.Flush()

	go m.listen()

	return m.cmd.Process, err
}

func (m *ResetManager) listen() {
	rpipeReader := bufio.NewReader(m.newFd[0])
	stdinWriter := bufio.NewWriter(m.stdin)

	// read shadowed pwd from dialog, this input should end with a '\n'
	line, err := rpipeReader.ReadString('\n')
	if err != nil {
		logger.Warning(err)
		_, _ = fmt.Fprintf(stdinWriter, "fail to read: %v\n", err)
		_ = stdinWriter.Flush()
		return
	}

	// remove '\n'
	line = line[:len(line)-1]

	logger.Info(line)
	dm, err := GetDataManager(m.caller.Name)
	if err != nil {
		logger.Warningf("fail to modify password: %v", err)
		_, _ = fmt.Fprintf(stdinWriter, "fail to modify pattern: %v\n", err)
		_ = stdinWriter.Flush()
		return
	}
	dm.SetPattern(line)
	err = dm.Save()
	if err != nil {
		logger.Warning(err)
		// 保存失败，同步下数据
		dm.Load()
		_, _ = fmt.Fprintf(stdinWriter, "fail to modify pattern: %v\n", err)
		_ = stdinWriter.Flush()
		return
	}

	// send success message to dialog
	_, err = stdinWriter.Write([]byte("success\n"))
	_ = stdinWriter.Flush()
	if err != nil {
		logger.Warningf("fail to write success message: %v", err)
	}

	sysConn, err := dbus.SystemBus()
	if err != nil {
		logger.Warning(err)
	}

	// reset limits
	if sysConn != nil {
		auth := authenticate.NewAuthenticate(sysConn)
		err = auth.ResetLimits(0, m.caller.Name)
		if err != nil {
			logger.Warningf("fail to reset limits: %v", err)
		}
	}
}

func (m *ResetManager) Wait() {
	stderrBuf := new(bytes.Buffer)
	stdoutBuf := new(bytes.Buffer)

	stdoutReader := bufio.NewReader(m.stdout)
	stderrReader := bufio.NewReader(m.stderr)

	_, err := io.Copy(stderrBuf, stderrReader)
	if err != nil {
		logger.Warning(err)
	}

	_, err = io.Copy(stdoutBuf, stdoutReader)
	if err != nil {
		logger.Warning(err)
	}

	err = m.cmd.Wait()
	if err != nil {
		logger.Warningf(" exited: %v\nstderr:\n%v\nstdout:\n%v", err, stderrBuf, stdoutBuf)
	}
}

// clean all stuff relay to pwdChanger
func (m *ResetManager) Clean() {
	for _, f := range m.oldFd {
		if f != nil {
			err := f.Close()
			if err != nil {
				logger.Warningf("fail to close read end of pipe: %v", err)
			}
		}
	}

	for _, f := range m.newFd {
		if f != nil {
			err := f.Close()
			if err != nil {
				logger.Warningf("fail to close read end of pipe: %v", err)
			}
		}
	}
}

// X
func NewResetManagerX(caller *Caller) (*ResetManagerX, error) {
	base, err := NewResetManager(caller)
	if err != nil {
		return nil, err
	}
	xAuthDir := filepath.Join("/run", "user", fmt.Sprint(base.selfUid))
	defer func() {
		if err != nil {
			err := os.RemoveAll(xAuthDir)
			if err != nil {
				logger.Warningf("fail to remove tmp xauth dir: %v", err)
			}
		}
	}()
	err = os.Mkdir(xAuthDir, os.FileMode(0700))
	if err != nil {
		if os.IsExist(err) {
			logger.Warningf("path %s existed, remove it now", xAuthDir)
			err = os.RemoveAll(xAuthDir)
			if err != nil {
				return nil, fmt.Errorf("fail to remove existed dir that hold .Xauthority: %v", err)
			}
			err = os.Mkdir(xAuthDir, os.FileMode(0700))
		}
		if err != nil {
			return nil, fmt.Errorf("fail to create dir that hold .Xauthority: %v", err)
		}
	}
	err = os.Chown(xAuthDir, base.selfUid, base.selfGid)
	if err != nil {
		return nil, fmt.Errorf("fail to chown dir that hold .Xauthority: %v", err)
	}
	xauthPath := filepath.Join(xAuthDir, ".Xauthority")
	file, err := os.Create(xauthPath)
	if err != nil {
		return nil, fmt.Errorf("fail to create .Xauthority: %v", err)
	}
	err = file.Chown(base.selfUid, base.selfGid)
	if err != nil {
		return nil, fmt.Errorf("fail to chown .Xauthority: %v", err)
	}
	err = file.Close()
	if err != nil {
		return nil, fmt.Errorf("fail to close .Xauthority: %v", err)
	}
	// get caller's xauth
	cmd := exec.Command("runuser", "-u", caller.user.Username, "--", "xauth", "list") //#nosec G204
	cmd.Env = append(cmd.Env, "XAUTHORITY="+caller.xauth)
	logger.Debugf("set password with union id: run \"%s\", envs: %v", cmdToString(cmd), cmd.Env)
	xauths, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("get xauth cookie failed: %v", err)
	}
	// Add all xauth entries from caller to pwdChanger
	// After change hostname, some werid things happen to .Xauthority before a reboot
	// The prefix match not working here. So we have to add all entries to pwdChanger's .Xauthority file
	lines := strings.Split(string(xauths), "\n")
	args := []string{"-u", patternChangerUserName, "--", "xauth", "add"}
	for _, line := range lines {
		fields := strings.Fields(line)
		if len(fields) == 0 {
			continue
		}
		arg := append(args, fields...)
		cmd = exec.Command("runuser", arg...) //#nosec G204
		cmd.Env = append(cmd.Env, "XAUTHORITY="+xauthPath)
		logger.Debugf("set password with union id: run \"%s\", envs: %v", cmdToString(cmd), cmd.Env)
		err = cmd.Run()
		if err != nil {
			return nil, fmt.Errorf("add xauth fail: %v", err)
		}
	}
	base.cmd.Env = append(base.cmd.Env,
		"XAUTHORITY="+xauthPath,
		"DISPLAY="+caller.display,
	)
	return &ResetManagerX{
		ResetManager: base,
		xauthDir:     xAuthDir,
	}, nil
}

func (m *ResetManagerX) Clean() {
	m.ResetManager.Clean()
	err := os.RemoveAll(m.xauthDir)
	if err != nil {
		logger.Warningf("fail to remove tmp xauth dir: %v", err)
	}
}

// Wayland
func runSetfacl(user string, filePath string, isEnable bool) (err error) {
	flag := "-x" // remove
	perms := ""
	if isEnable {
		flag = "-m" // modify
		perms = ":rwx"
	}
	cmd := exec.Command("setfacl", flag, "u:"+user+perms, filePath) //#nosec G204
	logger.Debugf("set password with union id: run \"%s\", envs: %v", cmdToString(cmd), cmd.Env)
	err = cmd.Run()
	return
}

// 此函数负责临时允许 deepin-password-admin 用户访问目标用户的 wayland socket
func NewResetManagerWayland(caller *Caller) (*ResetManagerWayland, error) {
	if err := runSetfacl(patternChangerUserName, caller.wayland, true); err != nil {
		return nil, err
	}
	if err := runSetfacl(patternChangerUserName, filepath.Dir(caller.wayland), true); err != nil {
		return nil, err
	}
	base, err := NewResetManager(caller)
	if err != nil {
		return nil, err
	}
	base.cmd.Env = append(base.cmd.Env,
		"XDG_SESSION_TYPE=wayland",
		"QT_WAYLAND_SHELL_INTEGRATION=kwayland-shell", // env copy from startdde-wayland
		"WAYLAND_DISPLAY="+caller.wayland,
	)
	return &ResetManagerWayland{
		ResetManager:  base,
		waylandSocket: caller.wayland,
	}, nil
}

func (m *ResetManagerWayland) Clean() {
	m.ResetManager.Clean()
	if err := runSetfacl(patternChangerUserName, m.waylandSocket, false); err != nil {
		logger.Warningf("fail to clean acl for wayland socket: %v", err)
	}
	if err := runSetfacl(patternChangerUserName, filepath.Dir(m.waylandSocket), false); err != nil {
		logger.Warningf("fail to clean acl for wayland socket directory: %v", err)
	}
	return
}
