package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/labstack/echo/v4"
	"github.com/rosti-cz/node-api/apps"
	"github.com/rosti-cz/node-api/common"
	"github.com/rosti-cz/node-api/glue"
)

func homeHandler(c echo.Context) error {
	return c.Render(http.StatusOK, "index.html", templateData{
		Token: config.Token,
	})
}

func listAppsHandler(c echo.Context) error {
	processor := glue.Processor{
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}

	applications, err := processor.List(false)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, applications)
}

// Returns one app
func getAppHandler(c echo.Context) error {
	name := c.Param("name")
	fast := c.Param("fast")

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	app, err := processor.Get(fast == "1")
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, app)
}

// Create a new app
// If you add register_only=1 into query string, it won't start or create any container, just adds record into the database.
func createAppHandler(c echo.Context) error {
	registerOnly := c.QueryParam("register_only") == "1"

	appTemplate := apps.App{}
	err := c.Bind(&appTemplate)
	if err != nil {
		return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
	}

	processor := glue.Processor{
		AppName:    appTemplate.Name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}

	if registerOnly {
		err = processor.Register(appTemplate)
		if err != nil && strings.Contains(err.Error(), "validation error") {
			return c.JSONPretty(http.StatusBadRequest, Message{Errors: []string{err.Error()}}, JSONIndent)
		} else if err != nil {
			return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
		}
	} else {
		err = processor.Create(appTemplate)
		if err != nil && strings.Contains(err.Error(), "validation error") {
			return c.JSONPretty(http.StatusBadRequest, Message{Errors: []string{err.Error()}}, JSONIndent)
		} else if err != nil {
			return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
		}
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Update existing app
func updateAppHandler(c echo.Context) error {
	name := c.Param("name")

	appTemplate := apps.App{}
	err := c.Bind(&appTemplate)
	if err != nil {
		return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
	}

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	err = processor.Update(appTemplate)
	if err != nil && strings.Contains(err.Error(), "validation error") {
		return c.JSONPretty(http.StatusBadRequest, Message{Errors: []string{err.Error()}}, JSONIndent)
	} else if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Stop existing app
func stopAppHandler(c echo.Context) error {
	name := c.Param("name")

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	err := processor.Stop()
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Start existing app
func startAppHandler(c echo.Context) error {
	name := c.Param("name")

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	err := processor.Start()
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Stop existing app
func restartAppHandler(c echo.Context) error {
	name := c.Param("name")

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	err := processor.Restart()
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Set password for the app user in the container
func setPasswordHandler(c echo.Context) error {
	name := c.Param("name")

	password := Password{}
	err := c.Bind(&password)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	err = processor.SetPassword(password.Password)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Clear password for the app user in the container
func clearPasswordHandler(c echo.Context) error {
	name := c.Param("name")

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	err := processor.ClearPassword()
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Copies body of the request into /srv/.ssh/authorized_keys
func setKeysHandler(c echo.Context) error {
	name := c.Param("name")

	body, err := io.ReadAll(c.Request().Body)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	err = processor.UpdateKeys(string(body) + "\n")
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

func setServicesHandler(c echo.Context) error {
	name := c.Param("name")

	tech := &Technology{}
	err := c.Bind(tech)
	if err != nil {
		return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
	}

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}

	err = processor.EnableTech(tech.Name, tech.Version)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Rebuilds existing app, it keeps the data but created the container again
func rebuildAppHandler(c echo.Context) error {
	name := c.Param("name")

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	err := processor.Rebuild()
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Adds new label
func addLabelHandler(c echo.Context) error {
	name := c.Param("name")
	label := apps.Label{}

	err := c.Bind(&label)
	if err != nil {
		return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
	}

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	err = processor.AddLabel(label.Value)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Removes existing label
func deleteLabelHandler(c echo.Context) error {
	name := c.Param("name")
	label := apps.Label{}

	err := c.Bind(&label)
	if err != nil {
		return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
	}

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	err = processor.RemoveLabel(label.Value)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, Message{Message: "ok"})
}

// Delete one app
// This is async function.
func deleteAppHandler(c echo.Context) error {
	name := c.Param("name")

	go func(name string) {
		processor := glue.Processor{
			AppName:    name,
			DB:         common.GetDBConnection(),
			DockerSock: config.DockerSocket,
			BindIPHTTP: config.AppsBindIPHTTP,
			BindIPSSH:  config.AppsBindIPSSH,
			AppsPath:   config.AppsPath,
		}
		err := processor.Delete()
		if err != nil {
			log.Printf("Deletion of application failed: %v", err)
		}
	}(name)

	return c.JSON(http.StatusOK, Message{Message: "deleted"})
}

// Orphans returns directories in /srv that doesn't match any hosted application
func getOrphansHander(c echo.Context) error {
	return c.JSON(http.StatusOK, []string{})
}

// Save metadata for the app
func saveMetadataHandler(c echo.Context) error {
	name := c.Param("name")

	processor := glue.Processor{
		AppName:           name,
		DB:                common.GetDBConnection(),
		SnapshotProcessor: &snapshotProcessor,
		DockerSock:        config.DockerSocket,
		BindIPHTTP:        config.AppsBindIPHTTP,
		BindIPSSH:         config.AppsBindIPSSH,
		AppsPath:          config.AppsPath,
	}

	body, err := io.ReadAll(c.Request().Body)
	if err != nil {
		return fmt.Errorf("error reading request body: %v", err)
	}

	err = processor.SaveMetadata(string(body))
	if err != nil {
		return fmt.Errorf("error while save metadata: %v", err.Error())
	}

	return nil
}

// Return info about the node including performance index
func getNodeInfoHandler(c echo.Context) error {
	processor := glue.Processor{
		DB:            common.GetDBConnection(),
		DockerSock:    config.DockerSocket,
		BindIPHTTP:    config.AppsBindIPHTTP,
		BindIPSSH:     config.AppsBindIPSSH,
		AppsPath:      config.AppsPath,
		NodeProcessor: &nodeProcessor,
	}
	node, err := processor.GetNode()
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, node)
}

// Returns list of running processes (inside supervisor)
func getAppProcessesHandler(c echo.Context) error {
	name := c.Param("name")

	processor := glue.Processor{
		AppName:    name,
		DB:         common.GetDBConnection(),
		DockerSock: config.DockerSocket,
		BindIPHTTP: config.AppsBindIPHTTP,
		BindIPSSH:  config.AppsBindIPSSH,
		AppsPath:   config.AppsPath,
	}
	processes, err := processor.Processes()
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, processes)
}

// Return metrics that are available for running node
func metricsHandler(c echo.Context) error {
	var metrics string

	// Node indexes
	processor := glue.Processor{
		DB:            common.GetDBConnection(),
		DockerSock:    config.DockerSocket,
		BindIPHTTP:    config.AppsBindIPHTTP,
		BindIPSSH:     config.AppsBindIPSSH,
		AppsPath:      config.AppsPath,
		NodeProcessor: &nodeProcessor,
	}
	node, err := processor.GetNode()
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	hostname, err := os.Hostname()
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	metrics += fmt.Sprintf("rosti_node_index{hostname=\"%s\"} %f\n", hostname, node.Index)
	metrics += fmt.Sprintf("rosti_node_disk_space_index{hostname=\"%s\"} %f\n", hostname, node.DiskSpaceIndex)
	metrics += fmt.Sprintf("rosti_node_load1_index{hostname=\"%s\"} %f\n", hostname, node.Load1Index)
	metrics += fmt.Sprintf("rosti_node_load5_index{hostname=\"%s\"} %f\n", hostname, node.Load5Index)
	metrics += fmt.Sprintf("rosti_node_load15_index{hostname=\"%s\"} %f\n", hostname, node.Load15Index)
	metrics += fmt.Sprintf("rosti_node_memory_index{hostname=\"%s\"} %f\n", hostname, node.MemoryIndex)
	metrics += fmt.Sprintf("rosti_node_sold_memory{hostname=\"%s\"} %d\n", hostname, node.SoldMemory)

	if elapsedMetric != -1 {
		metrics += fmt.Sprintf("rosti_node_stats_time_elapsed{hostname=\"%s\"} %f\n", hostname, float64(elapsedMetric)/1000000000)
	}

	apps, err := processor.List(true)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	for _, app := range apps {
		metrics += fmt.Sprintf("rosti_node_app_disk_usage_bytes{hostname=\"%s\", app=\"%s\"} %d\n", hostname, app.Name, app.DiskUsageBytes)
		metrics += fmt.Sprintf("rosti_node_app_disk_usage_inodes{hostname=\"%s\", app=\"%s\"} %d\n", hostname, app.Name, app.DiskUsageInodes)
	}

	return c.String(http.StatusOK, metrics)
}

func CreateSnapshotHandler(c echo.Context) error {
	name := c.Param("name")

	body := createSnapshotBody{}
	err := c.Bind(body)
	if err != nil {
		return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
	}

	processor := glue.Processor{
		AppName:           name,
		DB:                common.GetDBConnection(),
		SnapshotProcessor: &snapshotProcessor,
		DockerSock:        config.DockerSocket,
		BindIPHTTP:        config.AppsBindIPHTTP,
		BindIPSSH:         config.AppsBindIPSSH,
		AppsPath:          config.AppsPath,
	}

	err = processor.CreateSnapshot(body.Labels)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSONPretty(http.StatusOK, Message{Message: "ok"}, JSONIndent)
}

func RestoreFromSnapshotHandler(c echo.Context) error {
	name := c.Param("name")
	snapshot := c.Param("snapshot")

	processor := glue.Processor{
		AppName:           name,
		DB:                common.GetDBConnection(),
		SnapshotProcessor: &snapshotProcessor,
		DockerSock:        config.DockerSocket,
		BindIPHTTP:        config.AppsBindIPHTTP,
		BindIPSSH:         config.AppsBindIPSSH,
		AppsPath:          config.AppsPath,
	}

	err := processor.RestoreFromSnapshot(snapshot)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSONPretty(http.StatusOK, Message{Message: "ok"}, JSONIndent)
}

func ListSnapshotsHandler(c echo.Context) error {
	name := c.Param("name")

	processor := glue.Processor{
		AppName:           name,
		DB:                common.GetDBConnection(),
		SnapshotProcessor: &snapshotProcessor,
		DockerSock:        config.DockerSocket,
		BindIPHTTP:        config.AppsBindIPHTTP,
		BindIPSSH:         config.AppsBindIPSSH,
		AppsPath:          config.AppsPath,
	}

	snapshots, err := processor.ListSnapshots()
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, snapshots)
}

func ListAppsSnapshotsHandler(c echo.Context) error {
	name := c.Param("name")
	apps := []string{}

	err := c.Bind(&apps)
	if err != nil {
		return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
	}

	processor := glue.Processor{
		AppName:           name,
		DB:                common.GetDBConnection(),
		SnapshotProcessor: &snapshotProcessor,
		DockerSock:        config.DockerSocket,
		BindIPHTTP:        config.AppsBindIPHTTP,
		BindIPSSH:         config.AppsBindIPSSH,
		AppsPath:          config.AppsPath,
	}
	snapshots, err := processor.ListAppsSnapshots(apps)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, snapshots)
}

func ListSnapshotsByLabelHandler(c echo.Context) error {
	label := c.Param("label")

	processor := glue.Processor{
		AppName:           "",
		DB:                common.GetDBConnection(),
		SnapshotProcessor: &snapshotProcessor,
		DockerSock:        config.DockerSocket,
		BindIPHTTP:        config.AppsBindIPHTTP,
		BindIPSSH:         config.AppsBindIPSSH,
		AppsPath:          config.AppsPath,
	}
	snapshots, err := processor.ListSnapshotsByLabel(label)
	if err != nil {
		return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
	}

	return c.JSON(http.StatusOK, snapshots)
}