//go:build !headless && !lite

package admin

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"path/filepath"
	"time"

	"breadbox/internal/agent"
	"breadbox/internal/appconfig"
	"breadbox/internal/service"
	"breadbox/internal/templates/components"
	"breadbox/internal/templates/components/pages"

	"github.com/alexedwards/scs/v2"
	"github.com/go-chi/chi/v5"
)

// AgentRunLivePayload is the JSON shape returned by
// GET /-/agents/runs/{shortId}/live — the live-update endpoint the
// run-detail page polls while a run is still in_progress.
//
// We send back HTML fragments rather than per-event diffs because (a)
// the templ rendering is the canonical source of truth for event
// shapes — duplicating it on the client would drift — and (b) the
// transcript is capped at 500 events, so a few KB of HTML on a 3 s
// cadence is cheaper than maintaining a JS-side renderer.
type AgentRunLivePayload struct {
	Status            string `json:"status"`
	SummaryStatusHTML string `json:"summaryStatusHTML"`
	TranscriptHTML    string `json:"transcriptHTML"`
	StatsHTML       string `json:"statsHTML"`
	EventCount      int    `json:"eventCount"`
	DurationMs      int64  `json:"durationMs,omitempty"`
}

// AgentRunLiveHandler serves the JSON snapshot the run-detail page
// polls. Same auth as the dashboard route (admin session via the /-/
// group); same transcript parsing as the full page handler so the
// in-place patch matches what the initial server-side render would
// have produced.
func AgentRunLiveHandler(svc *service.Service, sm *scs.SessionManager, tr *TemplateRenderer, dataDir string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		shortID := chi.URLParam(r, "shortId")

		run, err := svc.GetAgentRun(ctx, shortID)
		if err != nil {
			if errors.Is(err, service.ErrNotFound) {
				writeAdminError(w, http.StatusNotFound, "NOT_FOUND", "Run not found")
				return
			}
			writeAdminError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to load run")
			return
		}

		row := agentRunRowFromResponse(*run)

		// Enrich with agent identity so the stats panel renders the
		// same label set as the initial server render.
		if run.AgentDefinitionID != nil {
			if def, derr := svc.GetAgentDefinition(ctx, *run.AgentDefinitionID); derr == nil {
				row.AgentSlug = def.Slug
				row.AgentName = def.Name
			}
		}

		if reportMap, rerr := svc.ListReportSummariesForRunIDs(ctx, []string{run.ID}); rerr == nil {
			if reps, ok := reportMap[run.ID]; ok {
				row.Reports = make([]components.AgentRunReportRef, 0, len(reps))
				for _, rep := range reps {
					row.Reports = append(row.Reports, components.AgentRunReportRef{
						ShortID:  rep.ShortID,
						Title:    rep.Title,
						Priority: rep.Priority,
					})
				}
			}
		}

		// Reuse the page handler's parser so any classification fix is
		// applied uniformly across the initial render and the poll.
		path := ""
		if run.TranscriptPath != nil {
			path = *run.TranscriptPath
		}
		if path == "" && run.ID != "" {
			dir := appconfig.String(ctx, svc.Queries, appconfig.KeyAgentTranscriptDir, agent.DefaultTranscriptDir(dataDir))
			if dir != "" {
				path = filepath.Join(dir, run.ID+".ndjson")
			}
		}
		var events []pages.TranscriptEvent
		var truncated bool
		if path != "" {
			events, truncated, _ = parseTranscriptFile(path, agentRunTranscriptMaxEvents)
			// Match the initial server render — drop zero-valued result
			// envelopes so the live patch doesn't reintroduce them.
			events = pages.FilterTranscriptForDisplay(events)
		}

		props := pages.AgentRunDetailProps{
			Run:            row,
			Transcript:     events,
			Truncated:      truncated,
			TranscriptPath: path,
		}

		payload := AgentRunLivePayload{
			Status:     run.Status,
			EventCount: len(events),
		}

		// Render the transcript fragment.
		var buf bytes.Buffer
		if err := pages.AgentRunTranscriptFragment(props).Render(ctx, &buf); err != nil {
			writeAdminError(w, http.StatusInternalServerError, "RENDER_ERROR", "Failed to render transcript")
			return
		}
		payload.TranscriptHTML = buf.String()

		// Status markup for the summary STATUS card — the single run-status
		// indicator. in_progress renders a "running" spinner so it persists for
		// the whole run; terminal states render a soft daisy badge. Mirrors the
		// agentRunSummaryStatus templ so the patched HTML matches the initial
		// server render exactly.
		payload.SummaryStatusHTML = agentRunLiveSummaryStatusHTML(run.Status)

		if row.DurationMs > 0 {
			payload.DurationMs = row.DurationMs
		} else if !row.StartedAt.IsZero() && run.Status == "in_progress" {
			payload.DurationMs = time.Since(row.StartedAt).Milliseconds()
		}

		writeAdminJSON(w, http.StatusOK, payload)
	}
}

// writeAdminJSON is a tiny local helper — the admin package already has
// response helpers, but they live in response.go and we want to keep
// our endpoint self-contained while the surrounding admin error
// helpers are refactored.
func writeAdminJSON(w http.ResponseWriter, status int, body any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(body)
}

func writeAdminError(w http.ResponseWriter, status int, code, message string) {
	type errBody struct {
		Error struct {
			Code    string `json:"code"`
			Message string `json:"message"`
		} `json:"error"`
	}
	var b errBody
	b.Error.Code = code
	b.Error.Message = message
	writeAdminJSON(w, status, b)
}

// agentRunLiveSummaryStatusHTML returns the STATUS-card markup the live-update
// endpoint patches into the summary stats. It must stay byte-for-byte in step
// with the agentRunSummaryStatus templ so a live swap is visually seamless:
// terminal states get a soft daisy badge, in_progress a "running" spinner so
// the indicator persists for the whole run.
func agentRunLiveSummaryStatusHTML(status string) string {
	switch status {
	case "success":
		return `<span class="badge badge-soft badge-success badge-sm">success</span>`
	case "error":
		return `<span class="badge badge-soft badge-error badge-sm">error</span>`
	case "timeout":
		return `<span class="badge badge-soft badge-error badge-sm">timeout</span>`
	case "skipped":
		return `<span class="badge badge-ghost badge-sm">skipped</span>`
	case "cancelled":
		return `<span class="badge badge-soft badge-warning badge-sm">cancelled</span>`
	case "in_progress":
		return `<span class="inline-flex items-center gap-1.5 text-primary text-sm"><span class="loading loading-spinner loading-xs"></span>running</span>`
	default:
		return `<span class="text-base-content/40 text-sm">` + status + `</span>`
	}
}

// dummy reference to avoid "imported and not used" when the rest of the
// admin package vends a different ctx helper.
var _ = context.Background
