// 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.")
	flagDirectTLS := getopt.BoolLong("tls", 't', "Use direct TLS.")
	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.")
	flagListen := getopt.BoolLong("listen", 'l', "Listen for messages and print them to stdout.")
	flagTimeout := getopt.IntLong("timeout", 0, 10, "Connection timeout in seconds.")
	flagTLSMinVersion := getopt.IntLong("tls-version", 0, 12,
		"Minimal TLS version. 10 (TSLv1.0), 11 (TLSv1.1), 12 (TLSv1.2) or 13 (TLSv1.3).")
	flagVersion := getopt.BoolLong("version", 0, "Show version information.")

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

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

	// If requested, show version and quit.
	if *flagVersion {
		fmt.Println("go-sendxmpp", "0.2.0")
		fmt.Println("License: BSD-2-clause")
		os.Exit(0)
	}

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

	// 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.")
		}
	}

	// Timeout
	timeout := time.Duration(*flagTimeout * 1000000000)

	// Use ALPN
	var tlsConfig tls.Config
	tlsConfig.ServerName = user[strings.Index(user, "@")+1:]
	tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client")
	tlsConfig.InsecureSkipVerify = *flagSkipVerify
	switch *flagTLSMinVersion {
	case 10:
		tlsConfig.MinVersion = tls.VersionTLS10
	case 11:
		tlsConfig.MinVersion = tls.VersionTLS11
	case 12:
		tlsConfig.MinVersion = tls.VersionTLS12
	case 13:
		tlsConfig.MinVersion = tls.VersionTLS13
	default:
		fmt.Println("Unknown TLS version.")
		os.Exit(0)
	}

	// Set XMPP connection options.
	options := xmpp.Options{
		Host:        server,
		User:        user,
		DialTimeout: timeout,
		Resource:    resource,
		Password:    password,
		// NoTLS doesn't mean that no TLS is used at all but that instead
		// of using an encrypted connection to the server (direct TLS)
		// an unencrypted connection is established. As StartTLS is
		// set when NoTLS is set go-sendxmpp won't use unencrypted
		// client-to-server connections.
		// See https://pkg.go.dev/github.com/mattn/go-xmpp#Options
		NoTLS:     !*flagDirectTLS,
		StartTLS:  !*flagDirectTLS,
		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, *flagDirectTLS)
	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'.
	// Also for listening mode.
	if !*flagInteractive && !*flagListen {
		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
	}

	if *flagListen {
		for {
			received, err := client.Recv()
			if err != nil {
				log.Println(err)
			}

			switch v := received.(type) {
			case xmpp.Chat:
				if v.Text == "" {
					continue
				}
				t := time.Now()
				bareFrom := strings.Split(v.Remote, "/")[0]
				// Print any messages if no recipients are specified
				if len(recipients) == 0 {
					fmt.Println(t.Format(time.RFC3339), bareFrom+":", v.Text)
				} else {
					for _, recipient := range recipients {
						if bareFrom == strings.ToLower(recipient) {
							fmt.Println(t.Format(time.RFC3339), bareFrom+":", v.Text)
						}
					}
				}
			default:
				continue
			}
		}
	}

	// 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)
	}
}
