package main

import (
	"errors"
	"fmt"
	"log"
	"math/rand"
	"os"
	"flag"
	"gopkg.in/yaml.v2"
	"net/http"
	"io/ioutil"
	"bytes"
	"mime/multipart"
	"encoding/json"
)

/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/gpl-3.0.
 *
 * 2019 - 2023 https://://www.bananas-playground.net/projekt/selfpaste
 */

/**
 * !WARNING!
 * This is a very simple, with limited experience written, go program.
 * Use at own risk and feel free to improve
 */

const website = "https://www.bananas-playground.net/projekt/selfpaste"
const version = "1.0"
// used for non-existing default config
const letters = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-_"

// command line parameters
var optsVerbose bool
var optsCreateConfig bool
var optsDebug bool

// config
var cfg Config
// config file struct
type Config struct {
	Endpoint struct {
		Host string `yaml:"host"`
		Secret string `yaml:"secret"`
    } `yaml:"endpoint"`
}

// response json struct
type Response struct {
	Message string `json:"message"`
	Status int `json:"status"`
}

/**
 * Main
 */
func main() {

	// parse commandline parameters
	flag.BoolVar(&optsVerbose, "verbose", false, "Produce verbose output")
	flag.BoolVar(&optsCreateConfig, "create-config-file", false, "Create default config file")
	flag.BoolVar(&optsDebug, "debug", false, "Print debug infos")
	flag.Parse()	
	if optsDebug {
		fmt.Println("verbose:", optsVerbose)
		fmt.Println("create-config-file:", optsCreateConfig)
		fmt.Println("debug:", optsDebug)
	}

	// load the config and populate Config
	loadConfig()

	// get the paylout
	payload := getInput()
	if optsDebug { log.Println(payload) }

	// do the upload and get the response
	responseString := uploadCall(payload)
	response := Response{}
    json.Unmarshal([]byte(responseString), &response)

    // print the result and link to the pasty
    fmt.Printf("Status: %d\n", response.Status)
    fmt.Printf("Message: %s\n", response.Message)
}



/**
 * Check and display error with additional message
 */
func errorCheck(e error, msg string) {
	if e != nil {
		log.Fatal(msg,e)
	}
}

/**
 * just a random string
 */
func randStringBytes(n int) string {
	b := make([]byte, n)
	for i := range b {
		b[i] = letters[rand.Intn(len(letters))]
	}
	return string(b)
}

/**
 * load or even create a default config
 * $HOME/.selfpaste.yaml
 */
func loadConfig() {
	homeDir, err := os.UserHomeDir()
	errorCheck(err, "No $HOME directory available?")
	if optsVerbose { log.Printf("Your $HOME: %s \n", homeDir) }

	var configFile = homeDir + "/.selfpaste.yaml"

	if _, err := os.Stat(configFile); errors.Is(err, os.ErrNotExist) {
		log.Printf("Config file not found: %s \n", configFile)

		if optsCreateConfig {
			log.Printf("Creating new default config file: %s \n", configFile)

			newConfig, err := os.Create(configFile)
			errorCheck(err, "Can not create config file!")
			defer newConfig.Close()


			_, err = fmt.Fprintf(newConfig, "# selfpaste go client config file.\n")
			errorCheck(err, "Can not write to new config file")
			fmt.Fprintf(newConfig, "# See %s for more details.\n", website)
			fmt.Fprintf(newConfig, "# Version: %s\n", version)
			fmt.Fprintf(newConfig, "endpoint:\n")
			fmt.Fprintf(newConfig, "  host: http://your-seflpaste-endpoi.nt\n")
			fmt.Fprintf(newConfig, "  secret: %s\n", randStringBytes(50))

			log.Fatalf("New default config file created: - %s - Edit and launch again!",configFile)
		}
	}

	existingConfigFile, err := os.Open(configFile)
	errorCheck(err, "Can not open config file")
	defer existingConfigFile.Close()
	if optsVerbose { log.Printf("Reading config file: %s \n", configFile) }

	var decoder = yaml.NewDecoder(existingConfigFile)
	err = decoder.Decode(&cfg)
	errorCheck(err, "Can not decode config file")

	if cfg.Endpoint.Host == "" || cfg.Endpoint.Secret == "" {
		log.Fatal("Empty config?")
	}

	if optsDebug {
		log.Println(cfg.Endpoint.Host)
		log.Println(cfg.Endpoint.Secret)
	}
}

/**
 * Do a http POST call to the defined endpoint
 * and upload the payload
 * Return response body as string
 */
func uploadCall(payload string) string {

	if optsVerbose { log.Println("Starting to upload data") }
	if optsDebug { log.Println(payload) }
	if len(payload) == 0 {
		log.Fatal("Nothing provided to upload")
	}

	// Buffer to store our request body as bytes
	var requestBody bytes.Buffer

	// Create a multipart writer
	multiPartWriter := multipart.NewWriter(&requestBody)

	// file field
	fileWriter, err := multiPartWriter.CreateFormFile("pasty", "pastyfile")
	errorCheck(err, "Can not create form file field")
	fileWriter.Write([]byte(payload))

	dlField, err := multiPartWriter.CreateFormField("dl")
	errorCheck(err, "Can not create form dl field")
	dlField.Write([]byte(cfg.Endpoint.Secret))

	multiPartWriter.Close()

	req, err := http.NewRequest(http.MethodPost, cfg.Endpoint.Host, &requestBody)
	errorCheck(err, "Can not create http request")
	// We need to set the content type from the writer, it includes necessary boundary as well
	req.Header.Set("Content-Type", multiPartWriter.FormDataContentType())
	req.Header.Set("User-Agent", "selfpasteAgent/1.0");

	// Do the request
	client := &http.Client{}
	response, err := client.Do(req)
	errorCheck(err, "POST request failed")

	responseBody, err := ioutil.ReadAll(response.Body)
	errorCheck(err, "Can not read response body")

	if optsVerbose { log.Println("Request done") }
	if optsDebug {
		log.Printf("Response status code: %d\n",response.StatusCode)
		log.Printf("Response headers: %#v\n", response.Header)
	 	log.Println(string(responseBody)) 
	 }

	 return string(responseBody)
}

/**
 * check if file is provided as commandline argument
 * or piped into
 * return the read data as string
 */
func getInput() string {
	if optsVerbose { log.Println("Getting input") }

	var inputString string

	if filename := flag.Arg(0); filename != "" {
		if optsVerbose { log.Println("Read from file argument") }

		bytes, err := os.ReadFile(filename)
		errorCheck(err, "Error opening file")
		inputString = string(bytes)
	} else {
		stat, _ := os.Stdin.Stat()
		if (stat.Mode() & os.ModeCharDevice) == 0 {
			if optsVerbose { log.Println("data is being piped") }

			bytes, _ := ioutil.ReadAll(os.Stdin)
			inputString = string(bytes)
		}
	}

	if len(inputString) == 0 {
		log.Fatal("Nothing provided to upload")
	}

	return inputString
}