package api

import (
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/auth"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/services"
	"github.com/gin-gonic/gin"
)

// getServerWorkloads returns the stored snapshot.
//
// A server that has never reported answers an empty list rather than 404: the
// agent may simply not have got there yet, and 404 reads as "no such server".
func getServerWorkloads(c *gin.Context) {
	instanceID := auth.InstanceID(c)
	id := c.Param("id")

	if _, err := services.GetServer(instanceID, id); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
		return
	}

	sw, err := services.GetWorkloads(instanceID, id)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	if sw == nil {
		c.JSON(http.StatusOK, models.ServerWorkloads{
			ServerID:  id,
			Workloads: []models.Workload{},
		})
		return
	}
	if sw.Workloads == nil {
		sw.Workloads = []models.Workload{}
	}
	c.JSON(http.StatusOK, sw)
}

// refreshServerWorkloads nudges the agent to report now. It returns no data:
// the client refetches the stored document once the agent has written it.
func refreshServerWorkloads(c *gin.Context) {
	instanceID := auth.InstanceID(c)
	id := c.Param("id")

	s, err := services.GetServer(instanceID, id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
		return
	}

	if err := services.DispatchRefreshWorkloads(s.ServerID); err != nil {
		// Not queued: a command whose owner died must fail loudly, so the
		// client can show the stored snapshot as stale rather than pretend.
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusAccepted, gin.H{"message": "refresh requested"})
}

func controlWorkload(c *gin.Context) {
	instanceID := auth.InstanceID(c)
	id := c.Param("id")

	wid, ok := workloadIDParam(c)
	if !ok {
		return
	}

	var body struct {
		Action string `json:"action"`
		Kind   string `json:"kind"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
		return
	}
	switch body.Action {
	case models.WorkloadStart, models.WorkloadStop, models.WorkloadRestart:
	default:
		c.JSON(http.StatusBadRequest, gin.H{"error": "action must be start, stop or restart"})
		return
	}
	if body.Kind != models.WorkloadContainer && body.Kind != models.WorkloadUnit {
		c.JSON(http.StatusBadRequest, gin.H{"error": "kind must be container or unit"})
		return
	}

	s, err := services.GetServer(instanceID, id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
		return
	}

	err = services.DispatchControlWorkload(s.ServerID, body.Kind, wid, body.Action)
	if err != nil {
		switch {
		case errors.Is(err, services.ErrAgentNotConnected):
			c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()})
		case services.IsWorkloadProtected(err):
			// Nothing failed — the agent refused, which is the design. 409, not
			// 500, and the reason is carried through.
			c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
		default:
			c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
		}
		return
	}

	services.LogEvent(instanceID, "workload."+body.Action, actorFromCtx(c), s.ServerID, "",
		fmt.Sprintf("%s %s %s on %s", body.Action, body.Kind, wid, s.Hostname))
	c.JSON(http.StatusOK, gin.H{"message": body.Action + " ok"})
}

func getWorkloadLogs(c *gin.Context) {
	instanceID := auth.InstanceID(c)
	id := c.Param("id")

	wid, ok := workloadIDParam(c)
	if !ok {
		return
	}

	kind := c.DefaultQuery("kind", models.WorkloadContainer)
	if kind != models.WorkloadContainer && kind != models.WorkloadUnit {
		c.JSON(http.StatusBadRequest, gin.H{"error": "kind must be container or unit"})
		return
	}

	// Clamped rather than refused: a client asking for more than the cap gets
	// the cap, which is what it would have got anyway.
	tail, _ := strconv.Atoi(c.Query("tail"))
	if tail <= 0 || tail > services.MaxWorkloadLogLines {
		tail = services.MaxWorkloadLogLines
	}

	s, err := services.GetServer(instanceID, id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
		return
	}

	text, truncated, err := services.DispatchWorkloadLogs(s.ServerID, kind, wid, tail)
	if err != nil {
		if errors.Is(err, services.ErrAgentNotConnected) {
			c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
		return
	}

	// Audited because container output is arbitrary and cannot be masked: a
	// startup banner or a stack trace may carry credentials nobody declared.
	services.LogEvent(instanceID, "workload.logs_read", actorFromCtx(c), s.ServerID, "",
		fmt.Sprintf("read %s logs for %s on %s", kind, wid, s.Hostname))

	c.JSON(http.StatusOK, gin.H{"text": text, "truncated": truncated})
}

// listWorkloads answers the fleet-wide question, which is the reason the
// snapshot is stored rather than fetched on demand and discarded.
func listWorkloads(c *gin.Context) {
	hits, err := services.SearchWorkloads(auth.InstanceID(c),
		c.Query("image"), c.Query("stack"), c.Query("state"))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, hits)
}

// workloadIDParam decodes :wid. Unit names carry dots and '@', so the client
// encodes it and this is where it comes back.
func workloadIDParam(c *gin.Context) (string, bool) {
	raw := c.Param("wid")
	decoded, err := url.PathUnescape(raw)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid workload id"})
		return "", false
	}
	decoded = strings.TrimSpace(decoded)
	if decoded == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "workload id is required"})
		return "", false
	}
	return decoded, true
}
