// This deals with calls coming from Second Life or OpenSimulator.
// it's essentially a RESTful thingy
package main

import (
	_ "github.com/go-sql-driver/mysql"
	"crypto/md5"
	"database/sql"
	"encoding/hex"
	"fmt"
	"net/http"
	"strconv"
	"strings"
)

// GetMD5Hash takes a string which is to be encoded using MD5 and returns a string with the hex-encoded MD5 sum.
// Got this from https://gist.github.com/sergiotapia/8263278
func GetMD5Hash(text string) string {
    hasher := md5.New()
    hasher.Write([]byte(text))
    return hex.EncodeToString(hasher.Sum(nil))
}

// updateInventory updates the inventory of the object (object key will come in the headers).
func updateInventory(w http.ResponseWriter, r *http.Request) {
	// get all parameters in array
	err := r.ParseForm()
	checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Extracting parameters failed:", err)

	if r.Form.Get("signature") != "" && r.Header.Get("X-Secondlife-Object-Key") != "" {
		signature := GetMD5Hash(r.Header.Get("X-Secondlife-Object-Key") + r.Form.Get("timestamp") + ":" + LSLSignaturePIN)

		if signature != r.Form.Get("signature") {
			logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature does not match - hack attempt?")
			return
		}

		// open database connection and see if we can update the inventory for this object
		db, err := sql.Open(PDO_Prefix, GoBotDSN)
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Connect failed: %s\n", err)

		defer db.Close()

		stmt, err := db.Prepare("REPLACE INTO Inventory (`UUID`, `Name`, `Type`, `Permissions`, `LastUpdate`) VALUES (?,?,?,?,?)");
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace prepare failed:", err)

		defer stmt.Close()

		_, err = stmt.Exec(
			r.Header.Get("X-Secondlife-Object-Key"),
			r.Form.Get("name"),
			r.Form.Get("itemType"),
			r.Form.Get("permissions"),
			r.Form.Get("timestamp"),
		)
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace exec failed:", err)

		//_, err := res.RowsAffected()
		//checkErr(err)

		w.WriteHeader(http.StatusOK)
		w.Header().Set("Content-type", "text/plain; charset=utf-8")
		fmt.Fprintf(w, "%s successfully updated!", r.Header.Get("X-Secondlife-Object-Key"))
		return
	} else {
		logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature not found")
		return
	}

	/*
	fmt.Fprintf(w, "Root URL is: %s\n", updateInventory()) // send data to client side

    r.ParseForm()  // parse arguments, you have to call this by yourself
    Log.Debug(r.Form)  // print form information in server side
    Log.Debug("header connection: ", r.Header.Get("Connection"))
    Log.Debug("all headers:")
    for k, v := range r.Header {
        Log.Debug("key:", k)
        Log.Debug("val:", strings.Join(v, ""))
    }
    Log.Debug("path", r.URL.Path)
    Log.Debug("scheme", r.URL.Scheme)
    lLog.Debugr.Form["url-long"])
    for k, v := range r.Form {
        Log.Debug("key:", k)
        Log.Debug("val:", strings.Join(v, ""))
    }
	if r.Form["signature"] != nil {
	    fmt.Fprintf(w, "Signature is %s\n", r.Form.Get("signature"))
    }
    */
}

// updateSensor updates the Obstacles database with an additional object found by the sensors.
func updateSensor(w http.ResponseWriter, r *http.Request) {
	if r.Header.Get("X-Secondlife-Object-Key") != "" {
		err := r.ParseForm()
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Extracting parameters failed:", err)

		// open database connection and see if we can update the inventory for this object
		db, err := sql.Open(PDO_Prefix, GoBotDSN)
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Connect failed:", err)

		defer db.Close()

		stmt, err := db.Prepare("REPLACE INTO Obstacles (`UUID`, `Name`, `BotKey`, `BotName`, `Type`, `Origin`, `Position`, `Rotation`, `Velocity`, `Phantom`, `Prims`, `BBHi`, `BBLo`, `LastUpdate`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace prepare failed:", err)

		defer stmt.Close()

		_, err = stmt.Exec(
			r.Form.Get("key"),
			r.Form.Get("name"),
			r.Header.Get("X-Secondlife-Object-Key"),
			r.Header.Get("X-Secondlife-Object-Name"),
			r.Form.Get("type"),
			r.Form.Get("origin"),
			strings.Trim(r.Form.Get("pos"), "<>()"),
			strings.Trim(r.Form.Get("rot"), "<>()"),
			strings.Trim(r.Form.Get("vel"), "<>()"),
			r.Form.Get("phantom"),
			r.Form.Get("prims"),
			strings.Trim(r.Form.Get("bbhi"), "<>()"),
			strings.Trim(r.Form.Get("bblo"), "<>()"),
			r.Form.Get("timestamp"),
		)
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace exec failed:", err)

		//_, err := res.RowsAffected()
		//checkErr(err)

		w.WriteHeader(http.StatusOK)
		w.Header().Set("Content-type", "text/plain; charset=utf-8")

		reply := r.Header.Get("X-Secondlife-Owner-Name") + " sent us:\n" +
			"Key: " + r.Form.Get("key") + " Name: " + r.Form.Get("name")+ "\n" +
			"Position: " + r.Form.Get("pos") + " Rotation: " + r.Form.Get("rot") + "\n" +
			"Type: " + r.Form.Get("type") + "\n" + "Origin: " + r.Form.Get("origin") + "\n" +
			"Velocity: " + r.Form.Get("vel") +
			" Phantom: " + r.Form.Get("phantom") +
			" Prims: " + r.Form.Get("prims") + "\n" +
			"BB high: " + r.Form.Get("bbhi") +
			" BB low: " + r.Form.Get("bblo") + "\n" +
			"Timestamp: " + r.Form.Get("timestamp")

		fmt.Fprint(w, reply)
		return
	} else {
		logErrHTTP(w, http.StatusForbidden, funcName() + ": Not called from within the virtual world.")
		return
	}
}

// registerPosition saves a HTTP URL for a single object, making it persistent.
// POST parameters:
//  permURL: a permanent URL from llHTTPServer
//  signature: to make spoofing harder
//  timestamp: in-world timestamp retrieved with llGetTimestamp()
func registerPosition(w http.ResponseWriter, r *http.Request) {
	// get all parameters in array
	err := r.ParseForm()
	checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Extracting parameters failed:", err)
	// Log.Debug("Received: ", r) // we know this works well (20170725)

	if r.Header.Get("X-Secondlife-Object-Key") == "" {
		// Log.Debugf("Got '%s'\n", r.Header["X-Secondlife-Object-Key"])
		logErrHTTP(w, http.StatusForbidden, funcName() + ": Not called from within the virtual world.")
		return
	}

	if r.Form.Get("signature") != "" {
		// if we don't have the permURL to store, registering this object is pointless
		if r.Form["permURL"] == nil {
			logErrHTTP(w, http.StatusForbidden, funcName() + ": No PermURL specified")
			return
		}

		signature := GetMD5Hash(r.Header.Get("X-Secondlife-Object-Key") + r.Form.Get("timestamp") + ":" +  LSLSignaturePIN)
		/* Log.Debugf("%s: Calculating signature and comparing with what we got: Object Key: '%s' Timestamp: '%s' PIN: '%s' LSL signature: '%s' Our signature: %s\n",
			funcName(),
			r.Header.Get("X-Secondlife-Object-Key"),
			r.Form.Get("timestamp"),
			LSLSignaturePIN,
			r.Form.Get("signature"),
			signature)	*/
		if signature != r.Form.Get("signature") {
			logErrHTTP(w, http.StatusServiceUnavailable, funcName() + ": Signature does not match - hack attempt?")
			return
		}

		// open database connection and see if we can update the inventory for this object
		db, err := sql.Open(PDO_Prefix, GoBotDSN)
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Connect failed:", err)

		defer db.Close()

		stmt, err := db.Prepare("REPLACE INTO Positions (`UUID`, `Name`, `PermURL`, `Location`, `Position`, `Rotation`, `Velocity`, `OwnerKey`, `OwnerName`, `ObjectType`, `ObjectClass`, `RateEnergy`, `RateMoney`, `RateHappiness`, `LastUpdate`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace prepare failed: %s\n", err)

		defer stmt.Close()

		_, err = stmt.Exec(
			r.Header.Get("X-Secondlife-Object-Key"),
			r.Header.Get("X-Secondlife-Object-Name"),
			r.Form.Get("permURL"),
			r.Header.Get("X-Secondlife-Region"),
			strings.Trim(r.Header.Get("X-Secondlife-Local-Position"), "<>()"),
			strings.Trim(r.Header.Get("X-Secondlife-Local-Rotation"), "<>()"),
			strings.Trim(r.Header.Get("X-Secondlife-Local-Velocity"), "<>()"),
			r.Header.Get("X-Secondlife-Owner-Key"),
			r.Header.Get("X-Secondlife-Owner-Name"),
			r.Form.Get("objecttype"),
			r.Form.Get("objectclass"),
			r.Form.Get("rateenergy"),
			r.Form.Get("ratemoney"),
			r.Form.Get("ratehappiness"),
			r.Form.Get("timestamp"),
		)
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace exec failed:", err)

		//_, err := res.RowsAffected()
		//checkErr(err)

		w.WriteHeader(http.StatusOK)
		w.Header().Set("Content-type", "text/plain; charset=utf-8")
		fmt.Fprintf(w, "'%s' successfully updated!", r.Header.Get("X-Secondlife-Object-Name"))
		// Log.Debugf("These are the headers I got: %v\nAnd these are the parameters %v\n", r.Header, r.Form)
		return
	} else {
		logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature not found")
		return
	}
}

// registerAgent saves a HTTP URL for a single agent, making it persistent.
// POST parameters:
//  permURL: a permanent URL from llHTTPServer
//  signature: to make spoofing harder
//  timestamp: in-world timestamp retrieved with llGetTimestamp()
//  request: currently only delete (to remove entry from database when the bot dies)
func registerAgent(w http.ResponseWriter, r *http.Request) {
	// get all parameters in array
	err := r.ParseForm()
	checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Extracting parameters failed:", err)

	if r.Header.Get("X-Secondlife-Object-Key") == "" {
		// Log.Debugf("Got '%s'\n", r.Header["X-Secondlife-Object-Key"])
		logErrHTTP(w, http.StatusForbidden, funcName() + ": Only in-world requests allowed.")
		return
	}

	if r.Form.Get("signature") == "" {
		logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature not found")
		return
	}

	signature := GetMD5Hash(r.Header.Get("X-Secondlife-Object-Key") + r.Form.Get("timestamp") + ":" + LSLSignaturePIN)

	if signature != r.Form.Get("signature") {
		logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature does not match - hack attempt?")
		return
	}

	// open database connection and see if we can update the inventory for this object
	db, err := sql.Open(PDO_Prefix, GoBotDSN)
	checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Connect failed:", err)

	defer db.Close()

	if r.Form.Get("permURL") != "" { // bot registration
		stmt, err := db.Prepare("REPLACE INTO Agents (`UUID`, `Name`, `OwnerKey`, `OwnerName`, `PermURL`, `Location`, `Position`, `Rotation`, `Velocity`, `Energy`, `Money`, `Happiness`, `Class`, `SubType`, `LastUpdate`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, "Replace prepare failed: %s\n", err)

		defer stmt.Close()

		_, err = stmt.Exec(
			r.Header.Get("X-Secondlife-Object-Key"),
			r.Header.Get("X-Secondlife-Object-Name"),
			r.Header.Get("X-Secondlife-Owner-Key"),
			r.Header.Get("X-Secondlife-Owner-Name"),
			r.Form.Get("permURL"),
			r.Header.Get("X-Secondlife-Region"),
			strings.Trim(r.Header.Get("X-Secondlife-Local-Position"), "<>()"),
			strings.Trim(r.Header.Get("X-Secondlife-Local-Rotation"), "<>()"),
			strings.Trim(r.Header.Get("X-Secondlife-Local-Velocity"), "<>()"),
			r.Form.Get("energy"),
			r.Form.Get("money"),
			r.Form.Get("happiness"),
			r.Form.Get("class"),
			r.Form.Get("subtype"),
			r.Form.Get("timestamp"),
		)
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace exec failed:", err)

		w.WriteHeader(http.StatusOK)
		w.Header().Set("Content-type", "text/plain; charset=utf-8")
		replyText := "'" + r.Header.Get("X-Secondlife-Object-Name") +
				"' successfully updated object for NPC '" +
				r.Header.Get("X-Secondlife-Owner-Name") + "' (" +
				r.Header.Get("X-Secondlife-Owner-Key") + "), energy=" +
				r.Form.Get("energy") + ", money=" +
				r.Form.Get("money") + ", happiness=" +
				r.Form.Get("happiness") + ", class=" +
				r.Form.Get("class") + ", subtype=" +
				r.Form.Get("subtype") + "."

		fmt.Fprint(w, replyText)
		// log.Printf(replyText) // debug
	} else if r.Form.Get("request") == "delete" { // other requests, currently only deletion
		stmt, err := db.Prepare("DELETE FROM Agents WHERE UUID=?")
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Delete agent prepare failed:", err)

		defer stmt.Close()

		_, err = stmt.Exec(r.Form.Get("npc"))
		checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Delete agent exec failed:", err)

		w.WriteHeader(http.StatusOK)
		w.Header().Set("Content-type", "text/plain; charset=utf-8")
		fmt.Fprintf(w, "'%s' successfully deleted.", r.Form.Get("npc"))
		return
	}
}

// configureCube Support scripts for remote startup configuration for the cubes.
//	This basically gives the lists of options (e.g. energy, happiness; classes of NPCs, etc.) so
//	that we don't need to hardcode them
func configureCube(w http.ResponseWriter, r *http.Request) {
	logErrHTTP(w, http.StatusNotImplemented, funcName() + ": configureCube not implemented")
	// return
}

// processCube is called when an agent sits on the cube; it will update the agent's money/energy/happiness.
//  Once everything is updated, the agent will get kicked out of the cube after a certain time has elapsed. This happens in-world.
//  We might also animate the avatar depending on its class, subclass, etc.
func processCube(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Extracting parameters failed:", err)

	if r.Header.Get("X-Secondlife-Object-Key") == "" {
		logErrHTTP(w, http.StatusForbidden, funcName() + ": Only in-world requests allowed.")
		return
	}

	if r.Form.Get("signature") == "" {
		logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature not found")
		return
	}

	signature := GetMD5Hash(r.Header.Get("X-Secondlife-Object-Key") + r.Form.Get("timestamp") + ":" + LSLSignaturePIN)

	if signature != r.Form.Get("signature") {
		logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature does not match - hack attempt?")
		return
	}

	if r.Form.Get("avatar") == NullUUID { // happens when standing up, we return without giving errors
		w.WriteHeader(http.StatusOK)
		w.Header().Set("Content-type", "text/plain; charset=utf-8")
		fmt.Fprintf(w, "Got an avatar/NPC standing up")
		sendMessageToBrowser("status", "success", "Got an avatar/NPC standing up", "")
		return
	}

	// all checks fine, now let's get the agent data from the database:
	// allegedly, we get avatar=[UUID] — all the rest ought to be on the database (20170807)
	db, err := sql.Open(PDO_Prefix, GoBotDSN)
	checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Connect failed:", err)

	defer db.Close()

	Log.Debug("processCube called, avatar UUID is", r.Form.Get("avatar"), "cube UUID is", r.Header.Get("X-Secondlife-Object-Key"))

	var agent AgentType
	err = db.QueryRow("SELECT * FROM Agents WHERE OwnerKey=?", r.Form.Get("avatar")).Scan(
		&agent.UUID,
		&agent.Name,
		&agent.OwnerName,
		&agent.OwnerKey,
		&agent.Location,
		&agent.Position,
		&agent.Rotation,
		&agent.Velocity,
		&agent.Energy,
		&agent.Money,
		&agent.Happiness,
		&agent.Class,
		&agent.SubType,
		&agent.PermURL,
		&agent.LastUpdate,
		&agent.BestPath,
		&agent.SecondBestPath,
		&agent.CurrentTarget,
	)
	if err != nil || !agent.UUID.Valid {
		// this can happen when an avatar sits on the object, or a NPC that never got a chance to register, or even because of bugs
		//  or cubes which did not have llSitTarget() initialised (20170811)
		checkErrHTTP(w, http.StatusNotFound, funcName() + " Agent UUID not found in database or invalid:", err)
		return
	}
	// get information on this cube
	var cube PositionType
	err = db.QueryRow("SELECT * FROM Positions WHERE UUID=?", r.Header.Get("X-Secondlife-Object-Key")).Scan(
		&cube.PermURL,
		&cube.UUID,
		&cube.Name,
		&cube.OwnerName,
		&cube.Location,
		&cube.Position,
		&cube.Rotation,
		&cube.Velocity,
		&cube.LastUpdate,
		&cube.OwnerKey,
		&cube.ObjectType,
		&cube.ObjectClass,
		&cube.RateEnergy,
		&cube.RateMoney,
		&cube.RateHappiness,
		)
	if err != nil || !cube.UUID.Valid {
		checkErrPanicHTTP(w, http.StatusNotFound, funcName() + " This cube's UUID was not found in database or is invalid! We should reset it in-world. Error was:", err)
	}
	// we update the avatar's money, happiness etc. by the rate of the object
	energyAgent, err := strconv.ParseFloat(*agent.Energy.Ptr(), 64) // get this as a float or else all hell will break loose!
	checkErr(err)
	energyCube, err := strconv.ParseFloat(*cube.RateEnergy.Ptr(), 64)
	checkErr(err)
	energyAgent += energyCube
	// in theory, this should now go to the engine, to see if it's enough or not; we'll see what happens (20170808)
	// We now call the agent and tell him about the new values:
	rsBody, err := callURL(*agent.PermURL.Ptr(), fmt.Sprintf("command=setEnergy&float=%f", energyAgent))
    if (err != nil) {
	    sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in-world with new energy settings; in-world reply was: '%s'", *agent.Name.Ptr(), rsBody), "")
	} else {
		Log.Debug("Result from updating energy of ", *agent.Name.Ptr(), ":", rsBody)
	}
	// now do it for money!
	moneyAgent, err := strconv.ParseFloat(*agent.Money.Ptr(), 64)
	checkErr(err)
	moneyCube, err := strconv.ParseFloat(*cube.RateMoney.Ptr(), 64)
	checkErr(err)
	moneyAgent += moneyCube
	rsBody, err = callURL(*agent.PermURL.Ptr(), fmt.Sprintf("command=setMoney&float=%f", moneyAgent))
    if (err != nil) {
	    sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in-world with new money settings; in-world reply was: '%s'", *agent.Name.Ptr(), rsBody), "")
	} else {
		Log.Debug("Result from updating money of ", *agent.Name.Ptr(), ":", rsBody)
	}
	// last but not least, make the agent more happy!
	happinessAgent, err := strconv.ParseFloat(*agent.Happiness.Ptr(), 64)
	checkErr(err)
	happinessCube, err := strconv.ParseFloat(*cube.RateHappiness.Ptr(), 64)
	checkErr(err)
	happinessAgent += happinessCube
	rsBody, err = callURL(*agent.PermURL.Ptr(), fmt.Sprintf("command=setHappiness&float=%f", happinessAgent))
    if (err != nil) {
	    sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in-world with new happiness settings; in-world reply was: '%s'", *agent.Name.Ptr(), rsBody), "")
	} else {
		Log.Debug("Result from updating happiness of ", *agent.Name.Ptr(), ":", rsBody)
	}
	// stand up the bot, he's done his work
	rsBody, err = callURL(*agent.PermURL.Ptr(), "command=osNpcStand")
    if (err != nil) {
	    sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be made to stand-up; in-world reply was: '%s'", *agent.Name.Ptr(), rsBody), "")
	} else {
		Log.Debug("Result from making", *agent.Name.Ptr(), "stand up:", rsBody)
	}

	// the next step is to update the database with the new values
	stmt, err := db.Prepare("UPDATE Agents SET `Energy`=?, `Money`=?, `Happiness`=? WHERE UUID=?")
	checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Update prepare failed:", err)
    if (err != nil) {
	    sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in database with new energy/money/happiness settings; database reply was: '%v'", *agent.Name.Ptr(), err), "")
	}

	defer stmt.Close()

	_, err = stmt.Exec(energyAgent, moneyAgent, happinessAgent, *agent.UUID.Ptr())
	checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace exec failed:", err)
    if (err != nil) {
	    sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in database with new energy/money/happiness settings; database reply was: '%v'", *agent.Name.Ptr(), err), "")
	}

	Log.Debug("Agent", *agent.Name.Ptr(), "updated database with new energy:", energyAgent, "money:", moneyAgent, "happiness:", happinessAgent)

	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-type", "text/plain; charset=utf-8")
	fmt.Fprintf(w, "'%s' successfully updated.", *agent.Name.Ptr())
	sendMessageToBrowser("status", "success", fmt.Sprintf("'%s' successfully updated.", *agent.Name.Ptr()), "")
	// return
}