package api

import (
	"encoding/json"
	"errors"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"

	"github.com/alexei/tinyforge/internal/auth"
	"github.com/alexei/tinyforge/internal/store"
	"github.com/alexei/tinyforge/internal/workload/plugin"
	"github.com/alexei/tinyforge/internal/workload/preview"
)

// chainNode is the lightweight shape returned by /chain — we deliberately
// don't return full plugin.Workload values for ancestor/descendant rows
// because the secret fields don't belong in a chain-traversal response.
//
// IsPreview / PreviewBranch surface branch-preview children to the UI so it
// can render them in a dedicated "Preview environments" panel rather than as
// undistinguished stage children. They are computed against the chain's
// `self` workload via preview.IsPreviewChild — the canonical "this child is a
// branch preview" test that reverses the MaterializeForBranch naming formula.
// Both are zero-valued (false / "") for the parent and self nodes and for
// operator-created stage children.
type chainNode struct {
	ID            string `json:"id"`
	Name          string `json:"name"`
	SourceKind    string `json:"source_kind"`
	TriggerKind   string `json:"trigger_kind"`
	IsPreview     bool   `json:"is_preview"`
	PreviewBranch string `json:"preview_branch,omitempty"`
	CreatedAt     string `json:"created_at"`
	UpdatedAt     string `json:"updated_at"`
}

func chainNodeOf(w store.Workload) chainNode {
	return chainNode{
		ID:          w.ID,
		Name:        w.Name,
		SourceKind:  w.SourceKind,
		TriggerKind: w.TriggerKind,
		CreatedAt:   w.CreatedAt,
		UpdatedAt:   w.UpdatedAt,
	}
}

// previewBranchOf extracts the branch a preview child was materialized for
// from its source_config (the `branch` key MaterializeForBranch wrote).
// Returns "" on a missing/malformed config — the caller only calls this for
// rows preview.IsPreviewChild already confirmed, so a blank result just means
// the JSON couldn't be decoded.
func previewBranchOf(w store.Workload) string {
	var cfg struct {
		Branch string `json:"branch"`
	}
	if w.SourceConfig != "" {
		_ = json.Unmarshal([]byte(w.SourceConfig), &cfg)
	}
	return cfg.Branch
}

// childChainNode builds a chainNode for a child row, marking it as a branch
// preview (and attaching its branch) when it was materialized from `self`.
func childChainNode(self, child store.Workload) chainNode {
	node := chainNodeOf(child)
	if preview.IsPreviewChild(self, child) {
		node.IsPreview = true
		node.PreviewBranch = previewBranchOf(child)
	}
	return node
}

// getWorkloadChain handles GET /api/workloads/{id}/chain.
//
// Returns the workload's parent (or nil), itself, and its direct children
// — i.e. one hop in each direction along the parent_workload_id graph.
// Deeper traversal is left to the client: the chain is a tree the user
// builds incrementally, and a server-side recursive walk would surprise
// operators with O(N) loads on big graphs.
func (s *Server) getWorkloadChain(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")

	self, err := s.store.GetWorkloadByID(id)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "workload")
			return
		}
		respondError(w, http.StatusInternalServerError, "get workload")
		return
	}

	var parent *chainNode
	if self.ParentWorkloadID != "" {
		p, err := s.store.GetWorkloadByID(self.ParentWorkloadID)
		if err == nil {
			node := chainNodeOf(p)
			parent = &node
		} else if !errors.Is(err, store.ErrNotFound) {
			slog.Warn("chain: parent lookup failed", "workload", id, "parent", self.ParentWorkloadID, "error", err)
		}
	}

	childRows, err := s.store.ListChildrenByParent(self.ID)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "list children")
		return
	}
	children := make([]chainNode, 0, len(childRows))
	for _, c := range childRows {
		children = append(children, childChainNode(self, c))
	}

	respondJSON(w, http.StatusOK, map[string]any{
		"parent":   parent,
		"self":     chainNodeOf(self),
		"children": children,
	})
}

// promoteFromRequest is the body of /promote-from. ImageTag is optional —
// when blank the server falls back to whatever tag the source workload's
// most recent running container reports. The endpoint is intentionally
// non-destructive: it updates the SourceConfig.default_tag and queues a
// manual deploy. It does not change parent_workload_id.
type promoteFromRequest struct {
	ImageTag string `json:"image_tag"`
	Deploy   bool   `json:"deploy"`
}

// promoteFromWorkload handles POST /api/workloads/{id}/promote-from/{sourceID}.
//
// Copies the source workload's currently-running image tag into the
// target's SourceConfig.default_tag, optionally triggering an immediate
// deploy. The target's existing config blob is preserved aside from the
// promoted field. Both workloads must use the same source_kind (image)
// — promoting across kinds is undefined and rejected.
func (s *Server) promoteFromWorkload(w http.ResponseWriter, r *http.Request) {
	targetID := chi.URLParam(r, "id")
	sourceID := chi.URLParam(r, "sourceID")
	if targetID == sourceID {
		respondError(w, http.StatusBadRequest, "target and source must differ")
		return
	}

	target, err := s.store.GetWorkloadByID(targetID)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "workload")
			return
		}
		respondError(w, http.StatusInternalServerError, "get target workload")
		return
	}
	source, err := s.store.GetWorkloadByID(sourceID)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "source workload")
			return
		}
		respondError(w, http.StatusInternalServerError, "get source workload")
		return
	}
	if target.SourceKind != "image" || source.SourceKind != "image" {
		respondError(w, http.StatusBadRequest, "promote-from is only defined for image source workloads on both ends")
		return
	}

	var req promoteFromRequest
	if r.ContentLength > 0 {
		if !decodeJSONStrict(w, r, &req) {
			return
		}
	}

	// Resolve the tag: explicit override wins; otherwise pick the running
	// container's image_tag on the source workload.
	tag := strings.TrimSpace(req.ImageTag)
	if tag == "" {
		rows, err := s.store.ListContainersByWorkload(sourceID)
		if err != nil {
			respondError(w, http.StatusInternalServerError, "list source containers")
			return
		}
		for _, c := range rows {
			if c.State == "running" && c.ImageTag != "" {
				tag = c.ImageTag
				break
			}
		}
		if tag == "" {
			respondError(w, http.StatusBadRequest, "source workload has no running container; specify image_tag explicitly")
			return
		}
	}

	// Decode target source_config, patch default_tag, re-encode.
	cfg := map[string]any{}
	if target.SourceConfig != "" && target.SourceConfig != "{}" {
		if err := json.Unmarshal([]byte(target.SourceConfig), &cfg); err != nil {
			respondError(w, http.StatusInternalServerError, "decode target source_config")
			return
		}
	}
	cfg["default_tag"] = tag
	patched, err := json.Marshal(cfg)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "encode target source_config")
		return
	}
	target.SourceConfig = string(patched)
	if err := s.store.UpdateWorkload(target); err != nil {
		slog.Error("promote: update target", "target", targetID, "error", err)
		respondError(w, http.StatusInternalServerError, "update target workload")
		return
	}

	actor := "promote"
	if claims, ok := auth.ClaimsFromContext(r.Context()); ok && claims.Username != "" {
		actor = claims.Username
	}
	resp := map[string]any{
		"workload_id":   targetID,
		"source_id":     sourceID,
		"promoted_tag":  tag,
		"deploy_queued": false,
	}
	if req.Deploy {
		intent := plugin.DeploymentIntent{
			Reason:      "promote",
			Reference:   tag,
			Metadata:    map[string]string{"source_workload_id": sourceID},
			TriggeredAt: time.Now().UTC(),
			TriggeredBy: actor,
		}
		if err := s.deployer.DispatchPlugin(r.Context(), toPluginWorkload(target), intent); err != nil {
			slog.Warn("promote: dispatch failed", "target", targetID, "error", err)
			respondError(w, http.StatusInternalServerError, "dispatch failed; see server logs")
			return
		}
		resp["deploy_queued"] = true
	}
	respondJSON(w, http.StatusOK, resp)
}

