package api

import (
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"time"

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

	"github.com/alexei/tinyforge/internal/workload/plugin"
)

// getHookKindSchema returns the sample config shape for one registered
// plugin kind. Used by /apps/new and the edit form so the JSON editor's
// initial body is derived from the plugin itself rather than hardcoded
// in the frontend.
//
//	GET /api/hooks/kinds/{kind}/schema
func (s *Server) getHookKindSchema(w http.ResponseWriter, r *http.Request) {
	kind := chi.URLParam(r, "kind")
	sample, ok := plugin.SchemaSampleFor(kind)
	if !ok {
		respondNotFound(w, "plugin kind")
		return
	}
	respondJSON(w, http.StatusOK, map[string]any{
		"kind":   kind,
		"sample": sample,
	})
}

// listHookKinds reports every registered Source and Trigger so operators
// can verify the plugin registry is wired correctly without writing
// a workload.
//
//	GET /api/hooks/kinds
func (s *Server) listHookKinds(w http.ResponseWriter, r *http.Request) {
	respondJSON(w, http.StatusOK, map[string]any{
		"sources":  plugin.SourceKinds(),
		"triggers": plugin.TriggerKinds(),
	})
}

// dispatchGeneric accepts a pre-normalized InboundEvent payload and fans
// it out across registered triggers. The body shape mirrors
// plugin.InboundEvent — vendor-specific webhook parsing (Gitea / GitHub /
// generic registry) stays in the legacy /api/webhook/* handlers until
// Phase 5 of the refactor migrates them into trigger-specific ingress.
//
//	POST /api/hooks/generic
//	{
//	  "kind": "image-push",
//	  "image": { "registry": "...", "repo": "owner/app", "tag": "v1" }
//	}
//
// Until the store rewrite lands and workloads carry source_kind /
// trigger_kind, the workloads iteration here returns an empty list and
// the response reports zero matches. This still exercises the registry
// path so operators can curl it and confirm wiring.
func (s *Server) dispatchGeneric(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		respondError(w, http.StatusBadRequest, "read body: "+err.Error())
		return
	}
	var evt plugin.InboundEvent
	if err := json.Unmarshal(body, &evt); err != nil {
		respondError(w, http.StatusBadRequest, "invalid InboundEvent: "+err.Error())
		return
	}
	if evt.Kind == "" {
		respondError(w, http.StatusBadRequest, "kind is required")
		return
	}

	ctx := r.Context()
	triggers := plugin.AllTriggers()
	workloads := listPluginWorkloads(s)
	deps := s.deployer.PluginDeps()

	type matchReport struct {
		WorkloadID    string `json:"workload_id"`
		TriggerKind   string `json:"trigger_kind"`
		Reference     string `json:"reference"`
		Dispatched    bool   `json:"dispatched"`
		DispatchError string `json:"dispatch_error,omitempty"`
	}
	matches := []matchReport{}

	for _, wl := range workloads {
		trig, ok := triggers[wl.TriggerKind]
		if !ok {
			continue
		}
		intent, err := trig.Match(ctx, deps, wl, evt)
		if err != nil {
			slog.Warn("hooks: trigger match error",
				"trigger", wl.TriggerKind, "workload", wl.ID, "error", err)
			continue
		}
		if intent == nil {
			continue
		}
		if intent.TriggeredAt.IsZero() {
			intent.TriggeredAt = time.Now().UTC()
		}
		report := matchReport{
			WorkloadID:  wl.ID,
			TriggerKind: wl.TriggerKind,
			Reference:   intent.Reference,
		}
		if err := s.deployer.DispatchPlugin(ctx, wl, *intent); err != nil {
			// Wrapped error can carry registry-auth bytes / compose stdout
			// (i.e. user secrets baked into the YAML); keep it server-side
			// only and return a generic flag to the client.
			slog.Warn("hooks: dispatch failed",
				"workload", wl.ID, "trigger", wl.TriggerKind, "error", err)
			report.DispatchError = "dispatch failed; see server logs"
		} else {
			report.Dispatched = true
		}
		matches = append(matches, report)
	}

	respondJSON(w, http.StatusAccepted, map[string]any{
		"event_kind":         evt.Kind,
		"examined_triggers":  len(triggers),
		"examined_workloads": len(workloads),
		"matches":            matches,
	})
}

// listPluginWorkloads returns every workload row whose source_kind and
// trigger_kind are both set — these are the rows that opted into the new
// plugin pipeline. Legacy rows (kind/ref_id pointing at project/stack/site
// with empty source_kind) are skipped so the ingress only fires intents
// for workloads that have a registered Source + Trigger to dispatch them.
func listPluginWorkloads(s *Server) []plugin.Workload {
	rows, err := s.store.ListWorkloads("")
	if err != nil {
		slog.Warn("hooks: list workloads failed", "error", err)
		return nil
	}
	out := make([]plugin.Workload, 0, len(rows))
	for _, w := range rows {
		if w.SourceKind == "" || w.TriggerKind == "" {
			continue
		}
		out = append(out, toPluginWorkload(w))
	}
	return out
}
