package main

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
	"unsafe"

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/pelletier/go-toml"
)

// this project started by copying:
// https://github.com/jfarleyx/chrome-native-messaging-golang

// constants for Logger
var (
	// Trace logs general information messages.
	Trace *log.Logger
	// Error logs error messages.
	Error *log.Logger
)

// nativeEndian used to detect native byte order
var nativeEndian binary.ByteOrder

// bufferSize used to set size of IO buffer - adjust to accommodate message payloads
var bufferSize = 8192 * 8

// IncomingMessage represents a message sent to the native host.
type IncomingMessage struct {
	Multipart       bool   `json:"multipart"`
	MultipartID     string `json:"multipartid"`
	MultipartNext   bool   `json:"multipartnext"`
	Hugocontent     string `json:"hugocontent"`
	Hugofrontmatter string `json:"hugofrontmatter"`
	Filepath        string `json:"filepath"`
	Gitpath         string `json:"gitpath"`
	Publishpath     string `json:"publishpath"`
	Publish         bool   `json:"publish"`
	Offline         bool   `json:"offline"`
	Protocol        string `json:"protocol"`
}

// OutgoingMessage respresents a response to an incoming message query.
type OutgoingMessage struct {
	Action   string `json:"action"`
	ActionID string `json:"actionid"`
}

// Init initializes logger and determines native byte order.
func Init(traceHandle io.Writer, errorHandle io.Writer) {
	Trace = log.New(traceHandle, "TRACE: ", log.Ldate|log.Ltime|log.Lshortfile)
	Error = log.New(errorHandle, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)

	// determine native byte order so that we can read message size correctly
	var one int16 = 1
	b := (*byte)(unsafe.Pointer(&one))
	if *b == 0 {
		nativeEndian = binary.BigEndian
	} else {
		nativeEndian = binary.LittleEndian
	}
}

func main() {
	file, err := os.OpenFile(filepath.Join(os.TempDir(), "sandpoints-chromeext-log.txt"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		Init(os.Stdout, os.Stderr)
		Error.Printf("Unable to create and/or open log file. Will log to Stdout and Stderr. Error: %v", err)
	} else {
		Init(file, file)
		// ensure we close the log file when we're done
		defer file.Close()
	}

	Trace.Printf("Chrome native messaging host started. Native byte order: %v.", nativeEndian)
	read()
	Trace.Print("Chrome native messaging host exited.")
}

// read Creates a new buffered I/O reader and reads messages from Stdin.
func read() {
	v := bufio.NewReader(os.Stdin)
	// adjust buffer size to accommodate your json payload size limits; default is 4096
	s := bufio.NewReaderSize(v, bufferSize)
	Trace.Printf("IO buffer reader created with buffer size of %v.", s.Size())

	lengthBytes := make([]byte, 4)
	lengthNum := int(0)

	// we're going to indefinitely read the first 4 bytes in buffer, which gives us the message length.
	// if stdIn is closed we'll exit the loop and shut down host
	for b, err := s.Read(lengthBytes); b > 0 && err == nil; b, err = s.Read(lengthBytes) {
		// convert message length bytes to integer value
		lengthNum = readMessageLength(lengthBytes)
		Trace.Printf("Message size in bytes: %v", lengthNum)

		// If message length exceeds size of buffer, the message will be truncated.
		// This will likely cause an error when we attempt to unmarshal message to JSON.
		if lengthNum > bufferSize {
			Error.Printf("Message size of %d exceeds buffer size of %d. Message will be truncated and is unlikely to unmarshal to JSON.", lengthNum, bufferSize)
		}

		// read the content of the message from buffer
		content := make([]byte, lengthNum)
		n, err := s.Read(content)
		if err != nil && err != io.EOF {
			Error.Fatal(err)
		}
		Trace.Printf("Read %d bytes out of announced %d.", n, lengthNum)
		// message has been read, now parse and process
		parseMessage(content)
	}

	Trace.Print("Stdin closed.")
}

// readMessageLength reads and returns the message length value in native byte order.
func readMessageLength(msg []byte) int {
	var length uint32
	buf := bytes.NewBuffer(msg)
	err := binary.Read(buf, nativeEndian, &length)
	if err != nil {
		Error.Printf("Unable to read bytes representing message length: %v", err)
	}
	return int(length)
}

// write incoming message to file
func writeMessageToFile(fpath string, content string) {
	f, err := os.Create(fpath)
	if err != nil {
		Error.Printf("Write to file failed: %v", err)
	}
	defer f.Close()
	f.WriteString(content)
}

// commit edit to git repo
func commitChangeToGit(iMsg IncomingMessage) {
	g, err := git.PlainOpen(iMsg.Gitpath)
	if err != nil {
		Error.Printf("ERROR: Go-git Plain Open: %s", err)
	}
	w, err := g.Worktree()
	if err != nil {
		Error.Printf("ERROR: Go-git Worktree: %s", err)
	}
	// filePath := filepath.Base(iMsg.Filepath)
	// editFile := filepath.Join(iMsg.Gitpath, filePath)
	relPathFile := filepath.Join(strings.ReplaceAll(iMsg.Filepath, iMsg.Gitpath+"/", ""))
	// err = ioutil.WriteFile(iMsg.Filepath, []byte(iMsg.Hugopage), 0644)
	// check(err)
	var frontmatter map[string]interface{}
	err = json.Unmarshal([]byte(iMsg.Hugofrontmatter), &frontmatter)
	if err != nil {
		Error.Printf("ERROR: Unmarshaling frontmatter JSON failed: %s", err)
	}
	Trace.Printf("Frontmatter JSON: %s", &frontmatter)
	tml, err := toml.TreeFromMap(frontmatter)
	if err != nil {
		Error.Printf("ERROR: Frontmatter JSON to TML: %s", err)
	}
	tomlFrontmatter := fmt.Sprintf("+++\n%s+++\n", tml)
	Trace.Printf("TOML Frontmatter: \n%s", tml)
	writeMessageToFile(iMsg.Filepath, fmt.Sprintf("%s%s", tomlFrontmatter, iMsg.Hugocontent))
	_, err = w.Add(relPathFile)
	if err != nil {
		Error.Printf("ERROR: Go-git Add: %s", err)
	}
	tn := time.Now()
	w.Commit(fmt.Sprintf("sandpoints-ext - %s - editing via browser...", tn), &git.CommitOptions{
		Author: &object.Signature{
			Name:  "Sandpoints Edit Page",
			Email: "editpage@sandpoints.org",
			When:  tn,
		},
	})
}

// trigger git hook
func triggerGitHook(gitpath string, publish bool, offline bool, protocol string) {
	cmd := exec.Command(filepath.Join(gitpath, ".git", "hooks", "post-commit"))
	stdin, err := cmd.StdinPipe()
	if err != nil {
		Error.Printf("StdinPipe: %s", err)
	}
	pub := "nope"
	if publish {
		pub = "publish"
	}
	offl := "nope"
	if offline {
		offl = "offline"
	}

	go func() {
		defer stdin.Close()
		io.WriteString(stdin, fmt.Sprintf("sandpoints-ext %s %s %s", pub, offl, protocol))
	}()
	out, err := cmd.CombinedOutput()
	if err != nil {
		Error.Printf("ERROR Combined output: \n%s", err)
	}
	Trace.Printf("Git Hook Combined Output: \n%s", out)
}

func commitAndRender(iMsg IncomingMessage) {
	commitChangeToGit(iMsg)
	triggerGitHook(iMsg.Gitpath, iMsg.Publish, iMsg.Offline, iMsg.Protocol)
	if iMsg.Protocol == "file" {
		send(OutgoingMessage{Action: "redirect", ActionID: iMsg.Publishpath})
	}
}

// parseMessage parses incoming message
func parseMessage(msg []byte) {
	iMsg := decodeMessage(msg)
	if iMsg.Multipart {
		tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("sandpoints_hugocontent_%s", iMsg.MultipartID))
		if iMsg.MultipartNext {
			file, err := os.OpenFile(tmpFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
			if err != nil {
				Error.Printf("Error: %v", err)
			} else {
				defer file.Close()
				_, err := file.WriteString(iMsg.Hugocontent)
				if err != nil {
					Error.Printf("file.WriteString err: %s", err)
				}
				Trace.Printf("Wrote multipart to: %s\n%s\n", iMsg.MultipartID, iMsg.Hugocontent[:300])
				send(OutgoingMessage{Action: "nextMessage", ActionID: iMsg.MultipartID})
			}
		} else {
			hugoContent, err := os.ReadFile(tmpFile)
			if err != nil {
				Error.Printf("Error reading file: %v", err)
			}
			iMsg.Hugocontent = string(hugoContent) + iMsg.Hugocontent
			err = os.RemoveAll(tmpFile)
			if err != nil {
				Error.Printf("Error: %v", err)
			}
			commitAndRender(iMsg)
		}
	} else {
		commitAndRender(iMsg)
	}
}

// decodeMessage unmarshals incoming json request and returns query value.
func decodeMessage(msg []byte) IncomingMessage {
	var iMsg IncomingMessage
	// msg = bytes.TrimRight(msg, "\x00")
	err := json.Unmarshal(msg, &iMsg)
	if err != nil {
		Error.Printf("Unable to unmarshal json to struct: %v", err)
	}
	Trace.Printf("iMsg.Multipart: %t, iMsg.MultipartID: %s, iMsg.MultipartNext: %t", iMsg.Multipart, iMsg.MultipartID, iMsg.MultipartNext)
	return iMsg
}

// sends an OutgoingMessage to os.Stdout.
func send(msg OutgoingMessage) {
	byteMsg := dataToBytes(msg)
	writeMessageLength(byteMsg)

	var msgBuf bytes.Buffer
	_, err := msgBuf.Write(byteMsg)
	if err != nil {
		Error.Printf("Unable to write message length to message buffer: %v", err)
	}

	_, err = msgBuf.WriteTo(os.Stdout)
	if err != nil {
		Error.Printf("Unable to write message buffer to Stdout: %v", err)
	}
}

// dataToBytes marshals OutgoingMessage struct to slice of bytes
func dataToBytes(msg OutgoingMessage) []byte {
	byteMsg, err := json.Marshal(msg)
	if err != nil {
		Error.Printf("Unable to marshal OutgoingMessage struct to slice of bytes: %v", err)
	}
	return byteMsg
}

// writeMessageLength determines length of message and writes it to os.Stdout.
func writeMessageLength(msg []byte) {
	err := binary.Write(os.Stdout, nativeEndian, uint32(len(msg)))
	if err != nil {
		Error.Printf("Unable to write message length to Stdout: %v", err)
	}
}
