package channel

import (
	"context"
	"errors"
	"net/http"
	"strings"
	"sync"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/daemon"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	"github.com/tidbcloud/AgentTeam/internal/observability/logging"
	"github.com/tidbcloud/AgentTeam/internal/runtimecontrol"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
)

// ListReviewContexts handles GET /api/threads/:threadChannelId/review-contexts.
// Returns every review context bound to the reply thread (a thread may hold
// multiple), as the `contexts` shape the Thread review tab consumes.
func (h *ThreadHandler) ListReviewContexts(r api.WorkspaceScopedRequest) (api.Response, error) {
	if h.reviewStore == nil {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Internal server error")
	}
	threadChannelID := strings.TrimSpace(r.Gin.Param("threadChannelId"))
	if threadChannelID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "threadChannelId is required")
	}
	contexts, err := h.reviewStore.ListReviewContextsForThread(r.Context(), r.UserID(), r.WorkspaceID(), threadChannelID)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(gin.H{"contexts": converter.ToReviewContexts(contexts)}), nil
}

// ReviewSummary handles GET /api/threads/:threadChannelId/review/summary.
func (h *ThreadHandler) ReviewSummary(r api.WorkspaceScopedRequest) (api.Response, error) {
	rc, err := h.loadThreadReview(r)
	if err != nil {
		return api.Response{}, err
	}
	agentID, err := h.threadReviewAgent(rc)
	if err != nil {
		return api.Response{}, err
	}
	baseRef, headRef, comparisonMode := reviewDefaultsFromQuery(r, rc)
	summary, err := h.hub.GetAgentReviewSummary(threadRemoteCtx(r), rc.MachineID, agentID, rc.RootPath, baseRef, headRef, comparisonMode)
	if err != nil {
		return api.Response{}, reviewHubError(err)
	}
	converted := converter.ToAgentReviewSummary(summary)
	return api.OK(gin.H{"summary": converted}), nil
}

// ReviewSnapshot handles GET /api/threads/:threadChannelId/review/snapshot.
func (h *ThreadHandler) ReviewSnapshot(r api.WorkspaceScopedRequest) (api.Response, error) {
	rc, err := h.loadThreadReview(r)
	if err != nil {
		return api.Response{}, err
	}
	agentID, err := h.threadReviewAgent(rc)
	if err != nil {
		return api.Response{}, err
	}
	baseRef, headRef, comparisonMode := reviewDefaultsFromQuery(r, rc)
	ctx := threadRemoteCtx(r)
	snapshot, err := h.hub.GetAgentReviewSnapshot(ctx, rc.MachineID, agentID, rc.RootPath, baseRef, headRef, comparisonMode)
	if errors.Is(err, runtimecontrol.ErrDaemonReviewSnapshotUnavailable) {
		snapshot, err = h.legacyReviewSnapshot(ctx, rc, agentID, baseRef, headRef, comparisonMode)
	}
	if err != nil {
		return api.Response{}, reviewHubError(err)
	}
	return api.OK(gin.H{"snapshot": converter.ToAgentReviewSnapshot(snapshot)}), nil
}

// ReviewFingerprint handles GET /api/threads/:threadChannelId/review/fingerprint.
// Capability miss is surfaced as HTTP 501 with the upgrade message — the
// frontend then polls /review/snapshot at the slower cadence instead of
// treating fingerprint as available.
func (h *ThreadHandler) ReviewFingerprint(r api.WorkspaceScopedRequest) (api.Response, error) {
	rc, err := h.loadThreadReview(r)
	if err != nil {
		return api.Response{}, err
	}
	agentID, err := h.threadReviewAgent(rc)
	if err != nil {
		return api.Response{}, err
	}
	baseRef, headRef, comparisonMode := reviewDefaultsFromQuery(r, rc)
	ctx := threadRemoteCtx(r)
	fingerprint, err := h.hub.GetAgentReviewFingerprint(ctx, rc.MachineID, agentID, rc.RootPath, baseRef, headRef, comparisonMode)
	if err != nil {
		return api.Response{}, reviewHubError(err)
	}
	return api.OK(gin.H{"fingerprint": converter.ToAgentReviewFingerprint(fingerprint)}), nil
}

func (h *ThreadHandler) legacyReviewSnapshot(ctx context.Context, rc agent.ReviewContext, agentID, baseRef, headRef, comparisonMode string) (agent.AgentReviewSnapshot, error) {
	var (
		snapshot                          agent.AgentReviewSnapshot
		summaryErr, branchErr, workingErr error
		wg                                sync.WaitGroup
	)
	wg.Add(3)
	logging.Go(nil, func() {
		defer wg.Done()
		snapshot.Summary, summaryErr = h.hub.GetAgentReviewSummary(ctx, rc.MachineID, agentID, rc.RootPath, baseRef, headRef, comparisonMode)
	})
	logging.Go(nil, func() {
		defer wg.Done()
		snapshot.Files, branchErr = h.hub.ListAgentReviewFiles(ctx, rc.MachineID, agentID, rc.RootPath, "branch", baseRef, headRef, comparisonMode)
	})
	logging.Go(nil, func() {
		defer wg.Done()
		snapshot.WorkingFiles, workingErr = h.hub.ListAgentReviewFiles(ctx, rc.MachineID, agentID, rc.RootPath, "working", baseRef, headRef, comparisonMode)
	})
	wg.Wait()
	return snapshot, errors.Join(summaryErr, branchErr, workingErr)
}

// ReviewFiles handles GET /api/threads/:threadChannelId/review/files.
func (h *ThreadHandler) ReviewFiles(r api.WorkspaceScopedRequest) (api.Response, error) {
	rc, err := h.loadThreadReview(r)
	if err != nil {
		return api.Response{}, err
	}
	scope := strings.TrimSpace(r.Gin.Query("scope"))
	if scope == "" {
		scope = "branch"
	}
	if scope != "branch" && scope != "working" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "scope must be branch or working")
	}
	agentID, err := h.threadReviewAgent(rc)
	if err != nil {
		return api.Response{}, err
	}
	baseRef, headRef, comparisonMode := reviewDefaultsFromQuery(r, rc)
	files, err := h.hub.ListAgentReviewFiles(threadRemoteCtx(r), rc.MachineID, agentID, rc.RootPath, scope, baseRef, headRef, comparisonMode)
	if err != nil {
		return api.Response{}, reviewHubError(err)
	}
	return api.OK(gin.H{"files": converter.ToAgentReviewFiles(files)}), nil
}

// ReviewDiff handles GET /api/threads/:threadChannelId/review/diff.
func (h *ThreadHandler) ReviewDiff(r api.WorkspaceScopedRequest) (api.Response, error) {
	rc, err := h.loadThreadReview(r)
	if err != nil {
		return api.Response{}, err
	}
	scope := strings.TrimSpace(r.Gin.Query("scope"))
	if scope == "" {
		scope = "branch"
	}
	if scope != "branch" && scope != "working" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "scope must be branch or working")
	}
	// Path is a raw Git path: do not TrimSpace. See webagent.ReviewHandler.Diff
	// for the rationale — the URL query decoder gives the exact bytes and Git
	// paths may contain leading/trailing whitespace or newlines.
	path := r.Gin.Query("path")
	if path == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "path is required")
	}
	agentID, err := h.threadReviewAgent(rc)
	if err != nil {
		return api.Response{}, err
	}
	baseRef, headRef, comparisonMode := reviewDefaultsFromQuery(r, rc)
	diff, err := h.hub.GetAgentReviewDiff(threadRemoteCtx(r), rc.MachineID, agentID, rc.RootPath, scope, path, baseRef, headRef, comparisonMode)
	if err != nil {
		return api.Response{}, reviewHubError(err)
	}
	return api.OK(gin.H{"diff": converter.ToAgentReviewDiff(diff)}), nil
}

// ReviewDiffBatch handles POST /api/threads/:threadChannelId/review/diff/batch.
// It mirrors webagent.ReviewHandler.DiffBatch for the thread-bound review
// surface: collapse N parallel diff requests into one server round-trip and
// one desktop-side repo scan. See that handler for the raw-path and
// per-path-error rationale.
func (h *ThreadHandler) ReviewDiffBatch(r api.WorkspaceScopedJSON[dto.ReviewDiffBatchRequest]) (api.Response, error) {
	// contextId travels in the JSON body for the batch endpoint (POST
	// /review/diff/batch), not the query string. Resolve the review context
	// explicitly from the body so a thread that has multiple review contexts
	// bound to it picks the right one — loadThreadReview would otherwise
	// ignore the body and fall back to "first context wins".
	rc, err := h.loadThreadReviewWithContext(r.WorkspaceScopedRequest, strings.TrimSpace(r.Body.ContextID))
	if err != nil {
		return api.Response{}, err
	}
	scope, err := reviewBatchScope(r.Body.Scope)
	if err != nil {
		return api.Response{}, err
	}
	paths, err := reviewBatchPaths(r.Body.Paths)
	if err != nil {
		return api.Response{}, err
	}
	agentID, err := h.threadReviewAgent(rc)
	if err != nil {
		return api.Response{}, err
	}
	baseRef, headRef, comparisonMode := reviewBatchDefaultsFromBody(r.Body, rc)
	results, err := h.hub.GetAgentReviewDiffBatch(threadRemoteCtx(r.WorkspaceScopedRequest), rc.MachineID, agentID, rc.RootPath, scope, paths, baseRef, headRef, comparisonMode)
	if err != nil {
		return api.Response{}, reviewHubError(err)
	}
	return api.OK(buildReviewDiffBatchResponse(paths, results)), nil
}

func threadRemoteCtx(r api.WorkspaceScopedRequest) context.Context {
	return runtimecontrol.WithRemoteRequestContext(r.Context(), runtimecontrol.RemoteRequestContext{
		Origin: runtimecontrol.RemoteRequestOriginUserInteractive,
		Cause:  runtimecontrol.RemoteRequestCauseWebAPI,
	})
}

func reviewHubError(err error) error {
	if errors.Is(err, runtimecontrol.ErrMachineOffline) {
		return api.NewHTTPError(http.StatusServiceUnavailable, "Machine is offline")
	}
	if errors.Is(err, runtimecontrol.ErrDaemonWorkspaceTargetPathUpgradeRequired) {
		return api.NewHTTPError(http.StatusConflict, err.Error())
	}
	if errors.Is(err, runtimecontrol.ErrDaemonPermissionModeUpgradeRequired) {
		return api.NewHTTPError(http.StatusConflict, runtimecontrol.AgentPermissionModeDaemonUpgradeMessage)
	}
	if errors.Is(err, runtimecontrol.ErrDaemonReviewFingerprintUnavailable) {
		return api.NewHTTPError(http.StatusNotImplemented, runtimecontrol.ReviewFingerprintDaemonUpgradeMessage)
	}
	// Product limitation: repo tree contains a path the diff engine cannot
	// handle. Surface a stable "unsupported" response so the frontend can
	// distinguish it from transient 500s. The sentinel's message is
	// path-free; the underlying go-git error was discarded at the
	// workspace layer to avoid leaking the offending filename.
	if errors.Is(err, runtimecontrol.ErrReviewTargetUnsupported) {
		return api.NewHTTPError(http.StatusUnprocessableEntity, runtimecontrol.ReviewTargetUnsupportedMessage)
	}
	// Stable business errors from the remote-endpoint routing layer: the
	// daemon cannot accept this request via any transport right now. Map
	// busy/unavailable to 503 so clients can retry. This mapping MUST
	// mirror webagent.reviewRuntimeError so the Thread and Explore
	// surfaces classify identically.
	if stable, ok := reviewHubStableErrorClassification(err); ok {
		return api.NewHTTPError(http.StatusServiceUnavailable, stable)
	}
	return api.NewHTTPError(http.StatusInternalServerError, "Internal server error")
}

// reviewHubStableErrorClassification mirrors
// webagent.remoteRequestStableErrorClassification so the Thread and
// Explore HTTP surfaces classify opaque stable-error strings identically.
// The mapping is intentionally duplicated rather than extracted to a
// shared package: the strings are tied to remoteRequestStableError in
// internal/api/daemon, and pulling them into a shared helper would force
// a wider import surface. Keep the two copies in sync.
func reviewHubStableErrorClassification(err error) (string, bool) {
	if err == nil {
		return "", false
	}
	switch err.Error() {
	case "desktop_busy":
		return "Desktop is busy", true
	case "desktop_unavailable":
		return "Desktop endpoint is unavailable", true
	default:
		return "", false
	}
}

// reviewBatchScope mirrors webagent.reviewBatchScope for the thread-bound
// review surface. See that helper for the no-TrimSpace rationale.
func reviewBatchScope(scope string) (string, error) {
	if scope == "" {
		return "branch", nil
	}
	if scope != "branch" && scope != "working" {
		return "", api.NewHTTPError(http.StatusBadRequest, "scope must be branch or working")
	}
	return scope, nil
}

// reviewBatchPaths mirrors webagent.reviewBatchPaths. Entries are raw Git
// paths and MUST NOT be TrimSpace'd.
func reviewBatchPaths(paths []string) ([]string, error) {
	if len(paths) == 0 {
		return nil, api.NewHTTPError(http.StatusBadRequest, "paths is required")
	}
	for _, path := range paths {
		if path == "" {
			return nil, api.NewHTTPError(http.StatusBadRequest, "paths must not contain empty strings")
		}
	}
	const maxBatchPaths = 64
	if len(paths) > maxBatchPaths {
		return nil, api.NewHTTPError(http.StatusBadRequest, "too many paths in a single batch")
	}
	return paths, nil
}

// reviewBatchDefaultsFromBody mirrors webagent.reviewBatchRefs. Refs fall
// back to the review context's persisted values when the body omits them.
func reviewBatchDefaultsFromBody(body dto.ReviewDiffBatchRequest, rc agent.ReviewContext) (string, string, string) {
	baseRef := strings.TrimSpace(body.BaseRef)
	if baseRef == "" {
		baseRef = rc.BaseRef
	}
	headRef := strings.TrimSpace(body.HeadRef)
	if headRef == "" {
		headRef = rc.HeadRef
	}
	comparisonMode := strings.TrimSpace(body.ComparisonMode)
	return baseRef, headRef, comparisonMode
}

// buildReviewDiffBatchResponse mirrors webagent.buildReviewDiffBatchResponse.
// Per-path errors are encoded inside the response.Errors map; the
// unsupported_target sentinel reaches us already encoded as a Diff payload
// (Status="unsupported_target") by the desktop, so it round-trips as a Diff
// entry. See the webagent helper for the full rationale.
func buildReviewDiffBatchResponse(paths []string, results map[string]daemon.AgentReviewDiffBatchResult) dto.ReviewDiffBatchResponse {
	response := dto.ReviewDiffBatchResponse{
		Diffs:  make(map[string]dto.AgentReviewDiff, len(paths)),
		Errors: make(map[string]string, len(paths)),
	}
	for _, path := range paths {
		result, ok := results[path]
		if !ok {
			response.Errors[path] = "Diff was not returned for this path"
			continue
		}
		if result.Error != nil {
			response.Errors[path] = "Failed to load diff"
			continue
		}
		response.Diffs[path] = converter.ToAgentReviewDiff(result.Diff)
	}
	if len(response.Errors) == 0 {
		response.Errors = nil
	}
	return response
}
