// Copyright 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 (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/gob"
	"fmt"
	"log"
	"log/slog"
	"math/big"
	"net/url"
	"os"
	"regexp"
	"runtime"
	"strings"
	"time"

	"github.com/google/uuid"     // BSD-3-Clause
	"github.com/xmppo/go-xmpp"   // BSD-3-Clause
	"golang.org/x/crypto/scrypt" // BSD-3-Clause
)

type prettyLogger struct{}

func (pl prettyLogger) Write(bytes []byte) (int, error) {
	return fmt.Print(time.Now().Format("2006-01-02 15:04:05 ") + string(bytes))
}

func validUTF8(s string) string {
	// Remove invalid code points.
	s = strings.ToValidUTF8(s, "�")
	reg := regexp.MustCompile(`[\x{0000}-\x{0008}\x{000B}\x{000C}\x{000E}-\x{001F}]`)
	s = reg.ReplaceAllString(s, "�")

	return s
}

func validURI(s string) (*url.URL, error) {
	// Check if URI is valid
	uri, err := url.ParseRequestURI(s)
	if err != nil {
		return uri, fmt.Errorf("valid uri: %w", err)
	}
	return uri, nil
}

func readFile(path string) (*bytes.Buffer, error) {
	// #nosec G304 -- path comes from the local system, it's no untrusted input from the network
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("read file: %w", err)
	}
	defer func() {
		derr := file.Close()
		if derr != nil {
			slog.Warn("closing file failed", "error", derr)
		}
	}()
	buffer := new(bytes.Buffer)
	_, err = buffer.ReadFrom(file)
	if err != nil {
		return nil, fmt.Errorf("read file: %w", err)
	}
	return buffer, nil
}

func getFastData(jid string, password string, clientID string) (xmpp.Fast, error) {
	folder := fsFriendlyJid(jid)
	var fast xmpp.Fast
	fastPath, err := getDataPath(folder, true)
	if err != nil {
		return xmpp.Fast{}, fmt.Errorf("get fast data: failed to read fast cache folder: %w", err)
	}
	fastFileLoc := fastPath + "fast.bin"
	slog.Info("reading FAST data:", "file", fastFileLoc)
	buf, err := readFile(fastFileLoc)
	if err != nil {
		return xmpp.Fast{}, fmt.Errorf("get fast data: failed to read fast cache file: %w", err)
	}
	decBuf := bytes.NewBuffer(buf.Bytes())
	decoder := gob.NewDecoder(decBuf)
	err = decoder.Decode(&fast)
	if err != nil {
		return xmpp.Fast{}, fmt.Errorf("get fast data: failed to read fast cache file: %w", err)
	}
	salt := []byte(clientID)
	key, err := scrypt.Key([]byte(password), salt, 65536, 8, 1, 32)
	if err != nil {
		return xmpp.Fast{}, fmt.Errorf("get fast data: failed to create aes key: %w", err)
	}
	c, err := aes.NewCipher([]byte(key))
	if err != nil {
		return xmpp.Fast{}, fmt.Errorf("get fast data: failed to read fast cache file: %w", err)
	}
	gcm, err := cipher.NewGCM(c)
	if err != nil {
		return xmpp.Fast{}, fmt.Errorf("get fast data: failed to read fast cache file: %w", err)
	}
	nonceSize := gcm.NonceSize()
	cryptBuf := []byte(fast.Token)
	nonce, cryptBuf := cryptBuf[:nonceSize], cryptBuf[nonceSize:]
	tokenBuf, err := gcm.Open(nil, []byte(nonce), cryptBuf, nil)
	if err != nil {
		return xmpp.Fast{}, fmt.Errorf("get fast data: failed to read fast cache file: %w", err)
	}
	fast.Token = string(tokenBuf)
	return fast, nil
}

func deleteFastData(jid string) error {
	folder := fsFriendlyJid(jid)
	fastPath, err := getDataPath(folder, true)
	if err != nil {
		return fmt.Errorf("write fast data: failed to write fast cache file: %w", err)
	}
	fastFileLoc := fastPath + "fast.bin"
	slog.Info("deleting FAST data:", "file", fastFileLoc)
	return os.Remove(fastFileLoc)
}

func writeFastData(jid string, password string, clientID string, fast xmpp.Fast) error {
	var encBuf bytes.Buffer
	folder := fsFriendlyJid(jid)
	fastPath, err := getDataPath(folder, true)
	if err != nil {
		return fmt.Errorf("write fast data: failed to write fast cache file: %w", err)
	}
	fastFileLoc := fastPath + "fast.bin"
	slog.Info("writing FAST data:", "file", fastFileLoc)
	salt := []byte(clientID)
	key, err := scrypt.Key([]byte(password), salt, 65536, 8, 1, 32)
	if err != nil {
		return fmt.Errorf("write fast data: failed to create aes cipher: %w", err)
	}
	c, err := aes.NewCipher(key)
	if err != nil {
		return fmt.Errorf("write fast data: failed to create aes cipher: %w", err)
	}
	gcm, err := cipher.NewGCM(c)
	if err != nil {
		return fmt.Errorf("write fast data: failed to create aes cipher: %w", err)
	}
	nonce := make([]byte, gcm.NonceSize())
	_, err = rand.Read(nonce)
	if err != nil {
		return fmt.Errorf("write fast data: failed to create aes cipher: %w", err)
	}
	buf := gcm.Seal(nonce, nonce, []byte(fast.Token), nil)
	fast.Token = string(buf)
	encode := gob.NewEncoder(&encBuf)
	err = encode.Encode(fast)
	if err != nil {
		return fmt.Errorf("write fast data: failed to create fast token file: %w", err)
	}
	// #nosec G304 -- fast file loc comes from the local system, it's no untrusted input from the network
	file, err := os.Create(fastFileLoc)
	if err != nil {
		return fmt.Errorf("write fast data: failed to create fast token file: %w", err)
	}
	defer func() {
		derr := file.Close()
		if derr != nil {
			slog.Warn("closing file failed", "error", derr)
		}
	}()
	if runtime.GOOS != "windows" {
		_ = file.Chmod(os.FileMode(defaultFileRights))
	} else {
		_ = file.Chmod(os.FileMode(defaultFileRightsWin))
	}
	_, err = file.Write(encBuf.Bytes())
	if err != nil {
		return fmt.Errorf("write fast data: failed to write fast token file: %w", err)
	}
	return nil
}

func getClientID(jid string) (string, error) {
	var clientID string
	folder := fsFriendlyJid(jid)
	clientIDLoc, err := getClientIDLoc(folder)
	if err != nil {
		return strError, err
	}
	buf, err := readFile(clientIDLoc)
	if err != nil {
		clientID = uuid.NewString()
		// #nosec G304 -- client ID location comes from the local system,
		// it's no untrusted input from the network
		file, err := os.Create(clientIDLoc)
		if err != nil {
			return strEmpty, fmt.Errorf("get client id: failed to create clientid file: %w", err)
		}
		defer func() {
			derr := file.Close()
			if derr != nil {
				slog.Warn("closing file failed", "error", derr)
			}
		}()
		if runtime.GOOS != "windows" {
			_ = file.Chmod(os.FileMode(defaultFileRights))
		} else {
			_ = file.Chmod(os.FileMode(defaultFileRightsWin))
		}
		_, err = file.Write([]byte(clientID))
		if err != nil {
			return strEmpty, fmt.Errorf("get client id: failed to write client id file: %w", err)
		}
	} else {
		clientID = buf.String()
	}
	return clientID, nil
}

func getDataPath(folder string, create bool) (string, error) {
	var err error
	var homeDir, dataDir string
	switch {
	case os.Getenv("XDG_DATA_HOME") != "":
		dataDir = os.Getenv("XDG_DATA_HOME")
		slog.Info("using", "XDG_DATA_HOME", dataDir)
	case os.Getenv("XDG_HOME") != "":
		homeDir = os.Getenv("XDG_HOME")
		slog.Info("using", "XDG_HOME", homeDir)
		dataDir = homeDir + "/.local/share"
	case os.Getenv("HOME") != "":
		homeDir = os.Getenv("HOME")
		slog.Info("using", "HOME", homeDir)
		dataDir = homeDir + "/.local/share"
	default:
		homeDir, err = os.UserHomeDir()
		if err != nil {
			return strError, fmt.Errorf("get data path: failed to determine user dir: %w", err)
		}
		if homeDir == "" {
			return strError, fmt.Errorf("get data path: received empty string for home directory")
		}
		slog.Info("using", "HOME", homeDir)
		dataDir = homeDir + "/.local/share"
	}
	if folder != "" && !strings.HasSuffix(folder, "/") {
		folder = fmt.Sprintf("%s/", folder)
	}
	dataDir = fmt.Sprintf("%s/go-sendxmpp/%s", dataDir, folder)
	slog.Info("using data", "dir", dataDir)
	// #nosec G703 -- data dir comes from the local system, it's no untrusted input from the network
	_, err = os.Stat(dataDir)
	if create && os.IsNotExist(err) {
		// #nosec G703 -- data dir comes from the local system, it's no untrusted input from the network
		err = os.MkdirAll(dataDir, defaultDirRights)
		if err != nil {
			return strError, fmt.Errorf("get data path: could not create folder: %w", err)
		}
	}
	return dataDir, nil
}

func getClientIDLoc(folder string) (string, error) {
	dataDir, err := getDataPath(folder, true)
	if err != nil {
		return strError, fmt.Errorf("get client id location: %w", err)
	}
	dataFile := dataDir + "clientid"
	slog.Info("using client id", "location", dataDir)
	return dataFile, nil
}

func getRpad(messageLength int) string {
	rpadRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
	length := defaultRpadMultiple - messageLength%defaultRpadMultiple
	max := big.NewInt(int64(len(rpadRunes)))
	rpad := make([]rune, length)
	for i := range rpad {
		randInt, err := rand.Int(rand.Reader, max)
		if err != nil {
			log.Fatal(err)
		}
		rpad[i] = rpadRunes[randInt.Int64()]
	}
	return string(rpad)
}

func getID() string {
	id, err := uuid.NewV7()
	if err != nil {
		log.Fatal(err)
	}
	return id.String()
}

func getShortID() string {
	return uuid.NewString()[:6]
}

// Remove @ and dots
func fsFriendlyJid(jid string) string {
	jid = strings.ReplaceAll(jid, "@", "_at_")
	return strings.ReplaceAll(jid, ".", "_")
}
