// 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"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"strings"
	"time"

	"github.com/ProtonMail/gopenpgp/v2/crypto" // MIT License
	"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
	alias    string
}

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() {
	type recipientsType struct {
		Jid       string
		OxKeyRing *crypto.KeyRing
	}

	var (
		err                                              error
		message, user, server, password, resource, alias string
		oxPrivKey                                        *crypto.Key
		recipients                                       []recipientsType
	)

	// Define command line flags.
	flagHelp := getopt.BoolLong("help", 0, "Show help.")
	flagHTTPUpload := getopt.StringLong("http-upload", 'h', "", "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'. DEPRECATED: Use --alias instead.")
	flagAlias := getopt.StringLong("alias", 'a', "", "Set alias/nickname"+
		"for chatrooms.")
	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 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2) or 13 (TLSv1.3).")
	flagVersion := getopt.BoolLong("version", 0, "Show version information.")
	flagMUCPassword := getopt.StringLong("muc-password", 0, "", "Password for password protected MUCs.")
	flagOx := getopt.BoolLong("ox", 0, "Use \"OpenPGP for XMPP\" encryption (experimental).")
	flagOxGenPrivKeyRSA := getopt.BoolLong("ox-genprivkey-rsa", 0,
		"Generate a private OpenPGP key (RSA 4096 bit) for the given JID and publish the "+
			"corresponding public key.")
	flagOxGenPrivKeyX25519 := getopt.BoolLong("ox-genprivkey-x25519", 0,
		"Generate a private OpenPGP key (x25519) for the given JID and publish the "+
			"corresponding public key.")
	flagOxPassphrase := getopt.StringLong("ox-passphrase", 0, "",
		"Passphrase for locking and unlocking the private OpenPGP key.")
	flagOxImportPrivKey := getopt.StringLong("ox-import-privkey", 0, "",
		"Import an existing private OpenPGP key.")
	flagOxDeleteNodes := getopt.BoolLong("ox-delete-nodes", 0, "Delete existing OpenPGP nodes on the server.")

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

	switch {
	case *flagHelp:
		// If requested, show help and quit.
		getopt.Usage()
		os.Exit(0)
	case *flagVersion:
		// If requested, show version and quit.
		fmt.Println("go-sendxmpp", version)
		fmt.Println("License: BSD-2-clause")
		os.Exit(0)
		// Quit if Ox (OpenPGP for XMPP) is requested for unsupported operations like
		// groupchat, http-upload or listening.
	case *flagOx && *flagHTTPUpload != "":
		log.Fatal("No Ox support for http-upload available.")
	case *flagOx && *flagChatroom:
		log.Fatal("No Ox support for chat rooms available.")
	case *flagHTTPUpload != "" && *flagInteractive:
		log.Fatal("Interactive mode and http upload can't" +
			" be used at the same time.")
	case *flagHTTPUpload != "" && *flagMessageFile != "":
		log.Fatal("You can't send a message while using" +
			" http upload.")
	}

	// 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).
	recipientsList := getopt.Args()
	if (len(recipientsList) == 0 && !*flagRaw && !*flagListen && !*flagOxGenPrivKeyX25519 &&
		!*flagOxGenPrivKeyRSA && *flagOxImportPrivKey == "") && !*flagOxDeleteNodes ||
		(len(recipientsList) == 0 && *flagChatroom) {
		log.Fatal("No recipient specified.")
	}

	// 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
		alias = config.alias
		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
	}

	switch {
	// Use the resource as alias if no alias is specified otherwise but a resource is specified.
	// Backward compat, will be removed when resource is removed.
	case alias == "" && *flagAlias == "" && *flagResource != "":
		alias = *flagResource
		// Use the resource as alias if no alias is specified otherwise but a resource is specified.
		// Backward compat, will be removed when resource is removed.
	case alias == "" && *flagAlias == "" && resource != "":
		alias = resource
	// Use "go-sendxmpp" if no nick is specified via config or command line flag.
	case alias == "" && *flagAlias == "":
		alias = "go-sendxmpp"
		// Overwrite configured alias if a nick is specified via command line flag.
	case *flagAlias != "":
		alias = *flagAlias
	}

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

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

	// Skip reading message if '-i' or '--interactive' is set to work with e.g. 'tail -f'.
	// Also for listening mode and Ox key handling.
	if !*flagInteractive && !*flagListen && *flagHTTPUpload == "" &&
		!*flagOxDeleteNodes && *flagOxImportPrivKey == "" &&
		!*flagOxGenPrivKeyX25519 && !*flagOxGenPrivKeyRSA &&
		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 {
				log.Fatal(err)
			}
		}
	}

	// Remove invalid code points.
	message = validUTF8(message)
	// Exit if message is empty.
	if message == "" && !*flagInteractive && !*flagListen && !*flagOxGenPrivKeyRSA &&
		!*flagOxGenPrivKeyX25519 && *flagOxImportPrivKey == "" &&
		!*flagOxDeleteNodes && *flagHTTPUpload == "" {
		os.Exit(0)
	}

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

	iqc := make(chan xmpp.IQ, 100)
	msgc := make(chan xmpp.Chat, 100)
	go rcvStanzas(client, iqc, msgc)

	for _, r := range getopt.Args() {
		var re recipientsType
		re.Jid = r
		if *flagOx {
			re.OxKeyRing, err = oxGetPublicKeyRing(client, iqc, r)
			if err != nil {
				re.OxKeyRing = nil
				fmt.Println("ox: error fetching key for", r+":", err)
			}
		}
		recipients = append(recipients, re)
	}

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

	switch {
	case *flagOxGenPrivKeyX25519:
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			log.Fatal(err)
		}
		err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "x25519")
		if err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	case *flagOxGenPrivKeyRSA:
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			log.Fatal(err)
		}
		err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, "rsa")
		if err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	case *flagOxImportPrivKey != "":
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			log.Fatal(err)
		}
		err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey,
			client, iqc)
		if err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	case *flagOxDeleteNodes:
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			log.Fatal(err)
		}
		err = oxDeleteNodes(validatedOwnJid, client, iqc)
		if err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	case *flagOx:
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			log.Fatal(err)
		}
		oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase)
		if err != nil {
			log.Fatal(err)
		}
	}

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

	var msgType string
	msgType = "chat"
	if *flagChatroom {
		msgType = "groupchat"
		// Join the MUCs.
		for _, recipient := range recipients {
			if *flagMUCPassword != "" {
				dummyTime := time.Now()
				_, err = client.JoinProtectedMUC(recipient.Jid, alias,
					*flagMUCPassword, xmpp.NoHistory, 0, &dummyTime)
			} else {
				_, err = client.JoinMUCNoHistory(recipient.Jid, alias)
			}
			if err != nil {
				log.Fatal(err)
			}
		}
	}
	switch {
	case *flagRaw:
		if message == "" {
			break
		}
		// Send raw XML
		_, err = client.SendOrg(message + "\n")
		if err != nil {
			log.Fatal(err)
		}
	case *flagInteractive:
		// Send in endless loop (for usage with e.g. "tail -f").
		var reader = bufio.NewReader(os.Stdin)
		for {
			message, err = reader.ReadString('\n')
			if err != nil {
				log.Fatal("failed to read from stdin")
			}

			// Remove invalid code points.
			message = validUTF8(message)
			if message == "" {
				continue
			}
			for _, recipient := range recipients {
				switch {
				case *flagOx:
					if recipient.OxKeyRing == nil {
						continue
					}
					oxMessage, err := oxEncrypt(client, iqc, oxPrivKey,
						recipient.Jid, recipient.OxKeyRing, message)
					if err != nil {
						fmt.Println("Ox: couldn't encrypt to",
							recipient.Jid)
						continue
					}
					_, err = client.SendOrg(oxMessage + "\n")
					if err != nil {
						log.Fatal(err)
					}
				default:
					_, err = client.Send(xmpp.Chat{Remote: recipient.Jid,
						Type: msgType, Text: message})
					if err != nil {
						log.Fatal(err)
					}
				}
			}
		}
	case *flagListen:
		tz := time.Now().Location()
		for {
			v := <-msgc
			switch {
			case isOxMsg(v) && *flagOx:
				msg, t, err := oxDecrypt(v, client, iqc, user, oxPrivKey)
				if err != nil {
					log.Println(err)
					continue
				}
				if msg == "" {
					continue
				}
				var bareFrom string
				switch v.Type {
				case "chat":
					bareFrom = strings.Split(v.Remote, "/")[0]
				case "groupchat":
					bareFrom = v.Remote
				default:
					bareFrom = strings.Split(v.Remote, "/")[0]
				}
				// Print any messages if no recipients are specified
				if len(recipients) == 0 {
					fmt.Println(t.In(tz).Format(time.RFC3339), "[OX]",
						bareFrom+":", msg)
				} else {
					for _, recipient := range recipients {
						if strings.Split(v.Remote, "/")[0] ==
							strings.ToLower(recipient.Jid) {
							fmt.Println(t.In(tz).Format(time.RFC3339),
								"[OX]", bareFrom+":", msg)
						}
					}
				}
			default:
				var t time.Time
				if v.Text == "" {
					continue
				}
				if v.Stamp.IsZero() {
					t = time.Now()
				} else {
					t = v.Stamp
				}
				var bareFrom string
				switch v.Type {
				case "chat":
					bareFrom = strings.Split(v.Remote, "/")[0]
				case "groupchat":
					bareFrom = v.Remote
				default:
					bareFrom = strings.Split(v.Remote, "/")[0]
				}
				// Print any messages if no recipients are specified
				if len(recipients) == 0 {
					fmt.Println(t.In(tz).Format(time.RFC3339), bareFrom+":", v.Text)
				} else {
					for _, recipient := range recipients {
						if strings.Split(v.Remote, "/")[0] ==
							strings.ToLower(recipient.Jid) {
							fmt.Println(t.In(tz).Format(time.RFC3339),
								bareFrom+":", v.Text)
						}
					}
				}
			}
		}
	default:
		for _, recipient := range recipients {
			if message == "" {
				break
			}
			switch {
			case *flagHTTPUpload != "":
				_, err = client.Send(xmpp.Chat{Remote: recipient.Jid,
					Type: msgType, Ooburl: message, Text: message})
				if err != nil {
					fmt.Println("Couldn't send message to",
						recipient.Jid)
				}
			case *flagOx:
				if recipient.OxKeyRing == nil {
					continue
				}
				oxMessage, err := oxEncrypt(client, iqc, oxPrivKey,
					recipient.Jid, recipient.OxKeyRing, message)
				if err != nil {
					fmt.Println("Ox: couldn't encrypt to", recipient.Jid)
					continue
				}
				_, err = client.SendOrg(oxMessage + "\n")
				if err != nil {
					log.Fatal(err)
				}
			default:
				_, err = client.Send(xmpp.Chat{Remote: recipient.Jid,
					Type: msgType, Text: message})
				if err != nil {
					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)
}
