// Copyright 2018 - 2021 Martin Dosch.
// Use of this source code is governed by the BSD-2-clause
// license that can be found in the LICENSE file.

package main

import (
	"bufio"
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"os/exec"
	"os/user"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/mattn/go-xmpp"     // BSD-3-Clause
	"github.com/pborman/getopt/v2" // BSD-3-Clause
)

type configuration struct {
	username string
	jserver  string
	port     string
	password string
	resource string
}

// Opens the config file and returns the specified values
// for username, server and port.
func parseConfig(configPath string) (configuration, error) {

	var (
		output configuration
		err    error
	)

	// Use $XDG_CONFIG_HOME/.config/go-sendxmpp/sendxmpprc or
	// ~/.sendxmpprc if no config path is specified.
	if configPath == "" {
		// Get systems user config path.
		osConfigDir := os.Getenv("$XDG_CONFIG_HOME")
		if osConfigDir != "" {
			configPath = osConfigDir + "/go-sendxmpp/sendxmpprc"
			// Check that the config file is existing.
			_, err := os.Stat(configPath)
			if os.IsNotExist(err) {
				// If the file is not existing at ~/.config/go-sendxmpp/sendxmpprc
				// check at the perl sendxmpp legacy destination.
				configPath = osConfigDir + "/.sendxmpprc"
				_, err = os.Stat(configPath)
				if os.IsNotExist(err) {
					return output, errors.New("no configuration file found")

				}
			}
		} else {
			// Get the current user.
			curUser, err := user.Current()
			if err != nil {
				return output, err
			}
			// Get home directory.
			home := curUser.HomeDir
			if home == "" {
				return output, errors.New("no home directory found")
			}
			configPath = home + "/.config/go-sendxmpp/sendxmpprc"
			// Check that config file is existing.
			_, err = os.Stat(configPath)
			if os.IsNotExist(err) {
				// If the file is not existing at ~/.config/go-sendxmpp/sendxmpprc
				// check at the perl sendxmpp legacy destination.
				configPath = home + "/.sendxmpprc"
				_, err = os.Stat(configPath)
				if os.IsNotExist(err) {
					return output, errors.New("no configuration file found")
				}
			}

		}
	}

	// Only check file permissions if we are not running on windows.
	if runtime.GOOS != "windows" {
		info, _ := os.Stat(configPath)
		// Check for file permissions. Must be 600, 640, 440 or 400.
		perm := info.Mode().Perm()
		permissions := strconv.FormatInt(int64(perm), 8)
		if permissions != "600" && permissions != "640" && permissions != "440" && permissions != "400" {
			return output, errors.New("Wrong permissions for " + configPath + ": " +
				permissions + " instead of 400, 440, 600 or 640.")
		}
	}

	// Open config file.
	file, err := os.Open(configPath)
	if err != nil {
		return output, err
	}
	scanner := bufio.NewScanner(file)
	scanner.Split(bufio.ScanLines)

	// Read config file per line.
	for scanner.Scan() {
		if strings.HasPrefix(scanner.Text(), "#") {
			continue
		}

		row := strings.SplitN(scanner.Text(), " ", 2)

		switch row[0] {
		case "username:":
			output.username = row[1]
		case "jserver:":
			output.jserver = row[1]
		case "password:":
			output.password = row[1]
		case "eval_password:":
			shell := os.Getenv("SHELL")
			if shell == "" {
				shell = "/bin/sh"
			}
			out, err := exec.Command(shell, "-c", row[1]).Output()
			if err != nil {
				log.Fatal(err)
			}
			output.password = string(out)
			if output.password[len(output.password)-1] == '\n' {
				output.password = output.password[:len(output.password)-1]
			}
		case "port:":
			output.port = row[1]
		case "resource:":
			output.resource = row[1]
		default:
			if len(row) >= 2 {
				if strings.Contains(scanner.Text(), ";") {
					output.username = strings.Split(row[0], ";")[0]
					output.jserver = strings.Split(row[0], ";")[1]
					output.password = row[1]
				} else {
					output.username = strings.Split(row[0], ":")[0]
					output.jserver = strings.Split(row[0], "@")[1]
					output.password = row[1]
				}
			}
		}
	}
	file.Close()

	// Check if the username is a valid JID
	output.username, err = MarshalJID(output.username)
	if err != nil {
		// Check whether only the local part was used by appending an @ and the
		// server part.
		output.username = output.username + "@" + output.jserver
		// Check if the username is a valid JID now
		output.username, err = MarshalJID(output.username)
		if err != nil {
			return output, errors.New("invalid username/JID: " + output.username)
		}
	}

	return output, err
}

func readMessage(messageFilePath string) (string, error) {
	var (
		output string
		err    error
	)

	// Check that message file is existing.
	_, err = os.Stat(messageFilePath)
	if os.IsNotExist(err) {
		return output, err
	}

	// Open message file.
	file, err := os.Open(messageFilePath)
	if err != nil {
		return output, err
	}
	scanner := bufio.NewScanner(file)
	scanner.Split(bufio.ScanLines)

	for scanner.Scan() {

		if output == "" {
			output = scanner.Text()
		} else {
			output = output + "\n" + scanner.Text()
		}
	}

	if err := scanner.Err(); err != nil {
		if err != io.EOF {
			return "", err
		}
	}

	file.Close()

	return output, err
}

func main() {

	var (
		err                                       error
		message, user, server, password, resource string
	)

	// Define command line flags.
	flagHelp := getopt.BoolLong("help", 0, "Show help.")
	flagHttpUpload := getopt.StringLong("http-upload", 0, "", "Send a file via http-upload.")
	flagDebug := getopt.BoolLong("debug", 'd', "Show debugging info.")
	flagServer := getopt.StringLong("jserver", 'j', "", "XMPP server address.")
	flagUser := getopt.StringLong("username", 'u', "", "Username for XMPP account.")
	flagPassword := getopt.StringLong("password", 'p', "", "Password for XMPP account.")
	flagChatroom := getopt.BoolLong("chatroom", 'c', "Send message to a chatroom.")
	flagTLS := getopt.BoolLong("tls", 't', "Use direct TLS.")
	flagStartTLS := getopt.BoolLong("start-tls", 'x', "Use StartTLS. (DEPRECATED)")
	flagResource := getopt.StringLong("resource", 'r', "", "Set resource. "+
		"When sending to a chatroom this is used as 'alias'.")
	flagFile := getopt.StringLong("file", 'f', "", "Set configuration file. (Default: "+
		"~/.config/go-sendxmpp/sendxmpprc)")
	flagMessageFile := getopt.StringLong("message", 'm', "", "Set file including the message.")
	flagInteractive := getopt.BoolLong("interactive", 'i', "Interactive mode (for use with e.g. 'tail -f').")
	flagSkipVerify := getopt.BoolLong("no-tls-verify", 'n',
		"Skip verification of TLS certificates (not recommended).")
	flagRaw := getopt.BoolLong("raw", 0, "Send raw XML.")

	// Parse command line flags.
	getopt.Parse()

	// If requested, show help and quit.
	if *flagHelp {
		getopt.Usage()
		os.Exit(0)
	}

	// Read recipients from command line and quit if none are specified.
	// For sending raw XML it's not required to specify a recipient except
	// for MUCs (go-sendxmpp will join the MUC automatically).
	recipients := getopt.Args()
	if (len(recipients) == 0 && !*flagRaw) || (len(recipients) == 0 && *flagChatroom) {
		log.Fatal("No recipient specified.")
	}

	// Quit if unreasonable TLS setting is set.
	if *flagStartTLS && *flagTLS {
		log.Fatal("Use either TLS or StartTLS.")
	}

	// Check that all recipient JIDs are valid.
	for i, recipient := range recipients {
		validatedJid, err := MarshalJID(recipient)
		if err != nil {
			log.Fatal(err)
		}
		recipients[i] = validatedJid
	}

	// Read configuration file if user or password is not specified.
	if *flagUser == "" || *flagPassword == "" {
		// Read configuration from file.
		config, err := parseConfig(*flagFile)
		if err != nil {
			log.Fatal("Error parsing ", *flagFile, ": ", err)
		}
		// Set connection options according to config.
		user = config.username
		server = config.jserver
		password = config.password
		resource = config.resource
		if config.port != "" {
			server = net.JoinHostPort(server, fmt.Sprint(config.port))
		}
	}

	// Overwrite user if specified via command line flag.
	if *flagUser != "" {
		user = *flagUser
	}

	// Overwrite server if specified via command line flag.
	if *flagServer != "" {
		server = *flagServer
	}

	// Overwrite password if specified via command line flag.
	if *flagPassword != "" {
		password = *flagPassword
	}

	// Overwrite resource if specified via command line flag
	if *flagResource != "" {
		resource = *flagResource
	} else if resource == "" {
		// Use "go-sendxmpp" plus a random string if no other resource is specified
		resource = "go-sendxmpp." + getID()
	}

	if (*flagHttpUpload != "") && (*flagInteractive || (*flagMessageFile != "")) {
		if *flagInteractive {
			log.Fatal("Interactive mode and http upload can't" +
				" be used at the same time.")
		}
		if *flagMessageFile != "" {
			log.Fatal("You can't send a message while using" +
				" http upload.")
		}
	}

	// Use ALPN
	var tlsConfig tls.Config
	tlsConfig.ServerName = user[strings.Index(user, "@")+1:]
	tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client")
	tlsConfig.InsecureSkipVerify = *flagSkipVerify

	// Set XMPP connection options.
	options := xmpp.Options{
		Host: server,
		User: user,
		// TODO: Check whether the timeout is reasonable or maybe make
		// it configurable
		DialTimeout: 1 * time.Second,
		Resource:    resource,
		Password:    password,
		NoTLS:       !*flagTLS,
		StartTLS:    !*flagTLS,
		Debug:       *flagDebug,
		TLSConfig:   &tlsConfig,
	}

	// Read message from file.
	if *flagMessageFile != "" {
		message, err = readMessage(*flagMessageFile)
		if err != nil {
			log.Fatal(err)
		}
	}

	// Connect to server.
	client, err := connect(options, *flagTLS)
	if err != nil {
		log.Fatal(err)
	}

	if *flagHttpUpload != "" {
		message = httpUpload(client, tlsConfig.ServerName,
			*flagHttpUpload)
	}

	// Skip reading message if '-i' or '--interactive' is set to work with e.g. 'tail -f'.
	if !*flagInteractive {
		if message == "" {

			scanner := bufio.NewScanner(os.Stdin)
			for scanner.Scan() {

				if message == "" {
					message = scanner.Text()
				} else {
					message = message + "\n" + scanner.Text()
				}
			}

			if err := scanner.Err(); err != nil {
				if err != io.EOF {
					// Close connection and quit.
					_ = client.Close()
					log.Fatal(err)
				}
			}
		}
	}

	// Send raw XML to chatroom
	if *flagChatroom && *flagRaw {

		// Join the MUCs.
		for _, recipient := range recipients {
			_, err := client.JoinMUCNoHistory(recipient, *flagResource)
			if err != nil {
				// Try to nicely close connection,
				// even if there was an error joining.
				_ = client.Close()
				log.Fatal(err)
			}
		}
		// Send raw XMP
		_, err = client.SendOrg(message)
		if err != nil {
			// Try to nicely close connection,
			// even if there was an error sending.
			_ = client.Close()
			log.Fatal(err)
		}
		// After sending the message, leave the MUCs
		for _, recipient := range recipients {
			_, err = client.LeaveMUC(recipient)
			if err != nil {
				log.Println(err)
			}
		}
		// Wait for a short time as some messages are not delievered by the server
		// if the connection is closed immediately after sending a message.
		time.Sleep(100 * time.Millisecond)
		return
	}

	// Send message to chatroom(s) if the flag is set.
	if *flagChatroom {

		for _, recipient := range recipients {

			// Join the MUC.
			_, err := client.JoinMUCNoHistory(recipient, *flagResource)
			if err != nil {
				// Try to nicely close connection,
				// even if there was an error joining.
				_ = client.Close()
				log.Fatal(err)
			}
		}

		// Send in endless loop (for usage with e.g. "tail -f").
		if *flagInteractive {
			for {
				scanner := bufio.NewScanner(os.Stdin)
				scanner.Scan()
				message = scanner.Text()
				for _, recipient := range recipients {
					_, err = client.Send(xmpp.Chat{Remote: recipient,
						Type: "groupchat", Text: message})
					if err != nil {
						// Try to nicely close connection,
						// even if there was an error sending.
						_ = client.Close()
						log.Fatal(err)
					}
				}
			}
		} else {
			// Send the message.
			for _, recipient := range recipients {
				if *flagHttpUpload != "" {
					_, err = client.Send(xmpp.Chat{Remote: recipient,
						Type: "groupchat", Ooburl: message, Text: message})
				} else {
					_, err = client.Send(xmpp.Chat{Remote: recipient,
						Type: "groupchat", Text: message})
				}
				if err != nil {
					// Try to nicely close connection,
					// even if there was an error sending.
					_ = client.Close()
					log.Fatal(err)
				}
			}
		}

		for _, recipient := range recipients {
			// After sending the message, leave the Muc
			_, err = client.LeaveMUC(recipient)
			if err != nil {
				log.Println(err)
			}
		}
	} else {
		// Send raw XML
		if *flagRaw {
			_, err = client.SendOrg(message)
			if err != nil {
				// Try to nicely close connection,
				// even if there was an error sending.
				_ = client.Close()
				log.Fatal(err)
			}
			// Wait for a short time as some messages are not delievered by the server
			// if the connection is closed immediately after sending a message.
			time.Sleep(100 * time.Millisecond)
			return
		}

		// Send in endless loop (for usage with e.g. "tail -f").
		if *flagInteractive {
			for {
				scanner := bufio.NewScanner(os.Stdin)
				scanner.Scan()
				message = scanner.Text()
				for _, recipient := range recipients {
					_, err = client.Send(xmpp.Chat{Remote: recipient,
						Type: "chat", Text: message})
					if err != nil {
						// Try to nicely close connection,
						// even if there was an error sending.
						_ = client.Close()
						log.Fatal(err)
					}
				}
			}
		} else {
			for _, recipient := range recipients {
				if *flagHttpUpload != "" {
					_, err = client.Send(xmpp.Chat{Remote: recipient, Type: "chat",
						Ooburl: message, Text: message})
				} else {
					_, err = client.Send(xmpp.Chat{Remote: recipient, Type: "chat",
						Text: message})
				}
				if err != nil {
					// Try to nicely close connection,
					// even if there was an error sending.
					_ = client.Close()
					log.Fatal(err)
				}
			}
		}
	}

	// Wait for a short time as some messages are not delievered by the server
	// if the connection is closed immediately after sending a message.
	time.Sleep(100 * time.Millisecond)

	// Close XMPP connection
	err = client.Close()
	if err != nil {
		log.Fatal(err)
	}
}
