package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net/http"
	"os"

	"gopkg.in/yaml.v2"
)

/**
 * scientia
 *
 * Copyright 2023 - 2024 Johannes Keßler
 *
 * https://www.bananas-playground.net/projekt/scientia/
 *
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the COMMON DEVELOPMENT AND DISTRIBUTION LICENSE
 *
 * You should have received a copy of the
 * COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0
 * along with this program.  If not, see http://www.sun.com/cddl/cddl.html
 */

const website = "https://www.bananas-playground.net/projekt/scientia/"
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"`
}

// post json struct
type PayloadJson struct {
	Asl  string `json:"asl"`
	Data string `json:"data"`
}

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

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

	// https://cli.urfave.org/v2/examples/arguments/
	// https://github.com/dnote/dnote
	//https://github.com/spf13/cobra/blob/main/site/content/user_guide.md

	// 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 payload
	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, " ; Errrormsg: ", 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/.scientia.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 + "/.scientia.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, "# scientia 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-scientia-endpoi.nt/api.php\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. Did you create one with -create-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")
	}

	payloadStruct := PayloadJson{
		Asl:  cfg.Endpoint.Secret,
		Data: payload,
	}

	jsonData, err := json.Marshal(payloadStruct)
	errorCheck(err, "Can not create json payload")

	req, err := http.NewRequest(http.MethodPost, cfg.Endpoint.Host, bytes.NewBuffer(jsonData))
	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", "application/json; charset=UTF-8")
	req.Header.Set("User-Agent", "scientiaAgent/1.0")

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

	responseBody, err := io.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, _ := io.ReadAll(os.Stdin)
			inputString = string(bytes)
		}
	}

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

	return inputString
}
