package api

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/alertstore/pkg/agent"
	"github.com/tidbcloud/alertstore/pkg/alert"
	"github.com/tidbcloud/alertstore/pkg/textutil"
	"github.com/tidbcloud/alertstore/pkg/timeline"
)

// agentReportRequest is what an agent writes back. Every field but status is
// optional: a run may report that it started before it has anything to say, and
// a failed one has no verdict at all.
type agentReportRequest struct {
	Status     string          `json:"status" binding:"required" enums:"running,succeeded,failed"`
	Verdict    string          `json:"verdict" enums:"noise,self_recovered,mitigated,escalation,unknown"`
	Summary    string          `json:"summary"`
	SessionRef string          `json:"session_ref"`
	Detail     json.RawMessage `json:"detail"`
}

// maxAgentSummary bounds what reaches a human surface. The card and the
// timeline get the summary; everything else belongs in detail, behind
// session_ref, or in the agent's own transcript. An on-caller paged into a wall
// of generated prose stops reading the card at all, which costs more than the
// analysis was ever worth.
const maxAgentSummary = 2000

// createAgentRunRequest is what a caller sends to start a manual run.
type createAgentRunRequest struct {
	Agent string `json:"agent" binding:"required"`
}

// createAgentRunResponse includes the callback URL so the caller knows where
// to report results without constructing it themselves.
type createAgentRunResponse struct {
	*agent.Run
	CallbackURL string `json:"callback_url"`
}

// @Summary	Create a manual agent run
// @Id			createAgentRun
// @Tags		alerts
// @Accept		json
// @Produce	json
// @Param		id		path		string					true	"Alert ID"
// @Param		body	body		createAgentRunRequest	true	"Agent"
// @Success	201		{object}	createAgentRunResponse
// @Failure	404		{object}	map[string]string
// @Router		/alerts/{id}/agent-runs [post]
func (s *Server) createAgentRun(c *gin.Context) {
	ctx := c.Request.Context()
	alertID := c.Param("id")

	if _, err := s.alertStore.GetByID(ctx, alertID); err != nil {
		if errors.Is(err, alert.ErrNotFound) {
			c.JSON(http.StatusNotFound, errorMessage("alert not found"))
			return
		}
		c.JSON(http.StatusInternalServerError, errorBody(err))
		return
	}

	var req createAgentRunRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, errorBody(err))
		return
	}

	run, err := s.agentRuns.EnsureRun(ctx, alertID, req.Agent, agent.TriggerManual, "", nil, agent.DisclosureShown)
	if err != nil {
		c.JSON(http.StatusInternalServerError, errorBody(err))
		return
	}

	callbackURL := fmt.Sprintf("/api/v1/alertstore/alerts/%s/agent-runs/%d", alertID, run.ID)

	c.JSON(http.StatusCreated, createAgentRunResponse{
		Run:         run,
		CallbackURL: callbackURL,
	})
}

// @Summary	List an alert's agent runs
// @Id			listAgentRuns
// @Tags		alerts
// @Produce	json
// @Param		id	path		string	true	"Alert ID"
// @Success	200	{array}		agent.Run
// @Failure	404	{object}	map[string]string
// @Router		/alerts/{id}/agent-runs [get]
func (s *Server) listAgentRuns(c *gin.Context) {
	ctx := c.Request.Context()
	alertID := c.Param("id")

	a, err := s.alertStore.GetByID(ctx, alertID)
	if err != nil {
		if errors.Is(err, alert.ErrNotFound) {
			c.JSON(http.StatusNotFound, errorMessage("alert not found"))
			return
		}
		c.JSON(http.StatusInternalServerError, errorBody(err))
		return
	}

	runs, err := s.agentRuns.ListByAlertID(ctx, alertID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, errorBody(err))
		return
	}
	c.JSON(http.StatusOK, redactHeldVerdicts(runs, a.State))
}

// redactHeldVerdicts blanks the verdict of every held run while the alert is
// still unresolved. It is the read-side half of what shouldRecordNote does on
// the write side, and it exists for the same reason (see migration 000007): a
// held verdict on a card before the resolver has recorded their own anchors
// that human label to the agent's, and the agreement rate that certifies the
// agent then measures its persuasiveness rather than its accuracy. Once the
// resolution is recorded there is nothing left to contaminate, so the verdict
// is shown.
//
// Only the response is redacted. The stored row keeps the verdict either way —
// withheld from the human, never from the record — so the scorecard still
// reads it and this can never lose data.
func redactHeldVerdicts(runs []agent.Run, alertState string) []agent.Run {
	if alertState == alert.StateResolved {
		return runs
	}
	out := make([]agent.Run, len(runs))
	copy(out, runs)
	for i := range out {
		if out[i].Disclosure == agent.DisclosureHeld {
			out[i].Verdict = ""
		}
	}
	return out
}

//	@Summary	Report an agent run's progress or result
//	@Id			reportAgentRun
//	@Tags		alerts
//	@Accept		json
//	@Produce	json
//	@Param		id		path		string				true	"Alert ID"
//	@Param		runId	path		int					true	"Agent run ID"
//	@Param		body	body		agentReportRequest	true	"Report"
//	@Success	200		{object}	agent.Run
//	@Failure	404		{object}	map[string]string
//	@Router		/alerts/{id}/agent-runs/{runId} [patch]
//
// The one write an agent is allowed. It cannot ack, resolve, assign or mute:
// what an agent concludes lands beside the human record, never inside it.
//
// The endpoint is authenticated by the devops gateway (Kong devops-auth), the
// same way every other alertstore endpoint is.
func (s *Server) reportAgentRun(c *gin.Context) {
	ctx := c.Request.Context()
	alertID := c.Param("id")

	runID, err := strconv.ParseInt(c.Param("runId"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, errorMessage("invalid run id"))
		return
	}

	run, err := s.agentRuns.GetByID(ctx, alertID, runID)
	if err != nil {
		if errors.Is(err, agent.ErrRunNotFound) {
			c.JSON(http.StatusNotFound, errorMessage("agent run not found"))
			return
		}
		c.JSON(http.StatusInternalServerError, errorBody(err))
		return
	}
	// Whether this report is the one that ends the run decides if it reaches the
	// timeline, so it has to be read before the update rather than inferred from
	// the result — a retried success is terminal both before and after.
	wasTerminal := run.Terminal()

	var req agentReportRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, errorBody(err))
		return
	}
	if !agent.ValidStatus(req.Status) {
		c.JSON(http.StatusBadRequest, errorMessage("status must be running, succeeded or failed"))
		return
	}
	// A verdict that matches nothing would drop out of every agreement query
	// silently rather than showing up as a bad row, so it is refused here.
	if req.Verdict != "" && !agent.ValidVerdict(req.Verdict) {
		c.JSON(http.StatusBadRequest, errorMessage("unknown verdict"))
		return
	}
	if len(req.Detail) > 0 && !json.Valid(req.Detail) {
		c.JSON(http.StatusBadRequest, errorMessage("detail must be valid JSON"))
		return
	}
	summary := textutil.Truncate(strings.TrimSpace(req.Summary), maxAgentSummary)

	updated, err := s.agentRuns.Update(ctx, alertID, runID, agent.Report{
		Status:     req.Status,
		Verdict:    req.Verdict,
		Summary:    summary,
		SessionRef: strings.TrimSpace(req.SessionRef),
		Detail:     req.Detail,
	})
	if err != nil {
		if errors.Is(err, agent.ErrRunNotFound) {
			c.JSON(http.StatusNotFound, errorMessage("agent run not found"))
			return
		}
		c.JSON(http.StatusInternalServerError, errorBody(err))
		return
	}

	if shouldRecordNote(updated, wasTerminal) {
		s.recordAgentNote(c, updated)
		// Reply the verdict into the thread too. Enqueued, not sent here: a
		// flaky IM call must not fail the agent's PATCH.
		if err := s.dispatcher.OnAgentVerdict(c.Request.Context(), updated.AlertID, updated.ID, updated.Verdict, updated.Summary); err != nil {
			s.logger.Error("enqueue agent verdict reply", "err", err, "alert_id", updated.AlertID, "run_id", updated.ID)
		}
	}
	c.JSON(http.StatusOK, updated)
}

// shouldRecordNote decides whether this report is the one that puts the agent's
// finding on the alert's timeline. Four conditions, each load-bearing:
//
//   - the run just became terminal, so every later report about a run that has
//     already spoken adds nothing;
//   - it succeeded, because a failed run's story is its status, not a finding;
//   - it has a summary — which is read from the merged row, not from the report
//     that triggered this, since an agent may write its findings while still
//     running and close with a bare `succeeded`;
//   - and it is not held. That exclusion is the whole point of the held sample:
//     the timeline is a human surface, so a summary written there before the
//     resolver has recorded their own verdict anchors it exactly as a card
//     would. The run row keeps the verdict either way — withheld from the
//     human, never from the record.
func shouldRecordNote(run *agent.Run, wasTerminal bool) bool {
	return !wasTerminal &&
		run.Status == agent.StatusSucceeded &&
		run.Summary != "" &&
		run.Disclosure == agent.DisclosureShown
}

// recordAgentNote puts a finished agent's summary on the alert's timeline.
//
// The run ID is the dedup ref, so a success reported twice because the first
// response was lost leaves one note rather than two. Together with the terminal
// check in shouldRecordNote that makes this exactly-once: keyed against
// redelivery, gated against every later report.
func (s *Server) recordAgentNote(c *gin.Context, run *agent.Run) {
	detail := map[string]interface{}{"run_id": run.ID, "verdict": run.Verdict}
	if run.SessionRef != "" {
		detail["session_ref"] = run.SessionRef
	}
	refID := fmt.Sprintf("agent-run-%d", run.ID)
	if _, err := s.timelineStore.Insert(c.Request.Context(), run.AlertID, timeline.KindAgentNote,
		"agent:"+run.Agent, "agent", run.Summary, refID, detail); err != nil {
		// The run is recorded; only its projection onto the timeline failed.
		s.logger.Error("record agent note", "err", err, "alert_id", run.AlertID, "run_id", run.ID)
	}
}
