package agent

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

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	workflowplanstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/plan"
	workflowpayload "github.com/tidbcloud/AgentTeam/internal/workflow/payload"
	planlang "github.com/tidbcloud/AgentTeam/internal/workflow/plan"
	manage "github.com/tidbcloud/AgentTeam/internal/workflow/planmanage"
	"github.com/tidbcloud/AgentTeam/internal/workflow/publicerr"
)

const (
	agentWorkflowPlanMaxRequestBytes = int64(4 << 20)
	agentWorkflowPlanMaxJSONNesting  = 64
)

func (handler *agentWorkflowPlanHandler) agentView(
	ctx context.Context,
	principal storeagent.AuthenticatedAgent,
	workspaceID string,
	plan manage.Plan,
	draft manage.Draft,
) (dto.WorkflowPlanAgentViewResponse, error) {
	source, sourceRef, err := inlineWorkflowTextWithSnapshot("plan", workspaceID, plan.ID, "", "", "", "source", "", draft.Source, workflowpayload.Snapshot{
		Version: strconv.FormatInt(draft.Revision, 10), Digest: workflowpayload.ContentDigest([]byte(draft.Source)),
	})
	if err != nil {
		return dto.WorkflowPlanAgentViewResponse{}, err
	}
	response := dto.WorkflowPlanAgentViewResponse{
		PlanID: plan.ID, ScopeID: plan.ScopeID, PlanRevision: plan.Revision, State: string(plan.State),
		Source: source, WorkingRevision: draft.Revision,
	}
	if sourceRef != nil {
		response.SourceCursor = sourceRef.Cursor
	}
	selected := plan.DefaultVersion
	if selected == nil {
		selected = plan.CurrentVersion
	}
	if selected != nil {
		version, err := handler.service.GetVersion(ctx, workspaceID, plan.ID, selected.ID)
		if err != nil {
			return dto.WorkflowPlanAgentViewResponse{}, err
		}
		response.PlanVersion = &dto.WorkflowPlanVersionRefResponse{ID: version.ID}
	}
	if handler.teamWorkflows != nil {
		inspection, linked, err := handler.teamWorkflows.InspectPlan(ctx, principal, plan.ID)
		if err != nil {
			return dto.WorkflowPlanAgentViewResponse{}, err
		}
		if linked {
			response.TeamID = inspection.TeamID
			response.TeamWorkflowID = inspection.TeamWorkflowID
		}
	}
	return response, nil
}

func decodeAgentWorkflowPlanJSON[T any](ctx *gin.Context) (T, error) {
	return decodeWorkflowCLIJSON[T](ctx, agentWorkflowPlanMaxRequestBytes, agentWorkflowPlanMaxJSONNesting, agentWorkflowPlanJSONError)
}

func decodeAgentWorkflowPlanJSONBytes[T any](payload []byte) (T, error) {
	return decodeWorkflowCLIJSONBytes[T](payload, agentWorkflowPlanMaxRequestBytes, agentWorkflowPlanMaxJSONNesting, agentWorkflowPlanJSONError)
}

func agentWorkflowPlanJSONError(err error) error {
	var maxBytesError *http.MaxBytesError
	if errors.As(err, &maxBytesError) {
		return publicError(http.StatusRequestEntityTooLarge, "plan_request_too_large", "invalid request body")
	}
	return publicError(http.StatusBadRequest, "plan_invalid_json", "invalid request body")
}

func agentWorkflowPlanAction(raw json.RawMessage) (string, error) {
	object, err := decodeAgentWorkflowPlanJSONBytes[map[string]json.RawMessage](raw)
	if err != nil {
		return "", err
	}
	rawAction, ok := object["action"]
	if !ok {
		return "", publicError(http.StatusBadRequest, "plan_invalid_action", "action is required")
	}
	var action string
	if err := json.Unmarshal(rawAction, &action); err != nil || strings.TrimSpace(action) == "" || action != strings.TrimSpace(action) {
		return "", publicError(http.StatusBadRequest, "plan_invalid_action", "action is invalid")
	}
	return action, nil
}

func validateAgentWorkflowPlanContext(request api.AgentScopedRequest, hidden agentWorkflowPlanContext) error {
	return validateWorkflowCLIContext(request, workflowCLIContext(hidden), "Plan", "plan")
}

func verifyAgentWorkflowPlanIdempotency(request api.AgentScopedRequest, hidden agentWorkflowPlanContext, action string) (string, error) {
	return verifyWorkflowCLIIdempotency(request, workflowCLIContext(hidden), workflowCLIObjectPlan, action, "plan")
}

func requiredAgentWorkflowPlanID(raw string) (string, error) {
	value := strings.TrimSpace(raw)
	if value == "" {
		return "", publicError(http.StatusBadRequest, "plan_invalid_request", "planId is required")
	}
	return value, nil
}

func requiredAgentWorkflowPlanSource(source *string) (string, error) {
	if source == nil {
		return "", publicError(http.StatusBadRequest, "plan_invalid_request", "source is required")
	}
	return *source, nil
}

func requireAgentWorkflowPlanPositiveRevision(name string, value int64) error {
	if value < 1 {
		return publicError(http.StatusBadRequest, "plan_invalid_request", name+" must be a positive integer")
	}
	return nil
}

func requireAgentWorkflowPlanDraftChanged(expectedRevision int64, actualRevision int64) error {
	if actualRevision != expectedRevision {
		return nil
	}
	return publicError(
		http.StatusConflict,
		"plan_no_changes",
		"Plan draft update did not change source or layout",
	)
}

func agentWorkflowPlanLimit(limit *int) (int, error) {
	if limit == nil {
		return 0, nil
	}
	if *limit < 1 || *limit > 100 {
		return 0, publicError(http.StatusBadRequest, "plan_invalid_request", "limit must be between 1 and 100 when provided")
	}
	return *limit, nil
}

func agentWorkflowPlanResponse(value manage.Plan) dto.WorkflowPlanResponse {
	response := dto.WorkflowPlanResponse{
		ID: value.ID, ScopeID: value.ScopeID, Key: value.Key, DisplayName: value.DisplayName, DescriptionMD: value.DescriptionMD,
		State: string(value.State), Revision: value.Revision, CreatedBy: agentWorkflowPlanActorResponse(value.CreatedBy),
		CreatedAt: value.CreatedAt, UpdatedBy: agentWorkflowPlanActorResponse(value.UpdatedBy), UpdatedAt: value.UpdatedAt,
		ArchivedAt: value.ArchivedAt,
	}
	if value.CurrentVersion != nil {
		response.CurrentVersion = &dto.WorkflowPlanVersionRefResponse{ID: value.CurrentVersion.ID}
	}
	if value.DefaultVersion != nil {
		response.DefaultVersion = &dto.WorkflowPlanVersionRefResponse{ID: value.DefaultVersion.ID}
	}
	if value.ArchivedBy != nil {
		actor := agentWorkflowPlanActorResponse(*value.ArchivedBy)
		response.ArchivedBy = &actor
	}
	return response
}

func agentWorkflowPlanDraftResponse(value manage.Draft) dto.WorkflowPlanDraftResponse {
	return dto.WorkflowPlanDraftResponse{
		PlanID: value.PlanID, Source: value.Source, Layout: value.Layout, Revision: value.Revision,
		UpdatedBy: agentWorkflowPlanActorResponse(value.UpdatedBy), CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt,
	}
}

func agentWorkflowPlanResponseFromStore(value workflowplanstore.Plan) dto.WorkflowPlanResponse {
	plan := manage.Plan{
		ID: value.ID, WorkspaceID: value.WorkspaceID, ScopeID: value.ScopeID, Key: value.PlanKey,
		DisplayName: value.DisplayName, DescriptionMD: value.DescriptionMD,
		State: manage.PlanState(value.State), Revision: value.Revision,
		CreatedBy: agentWorkflowPlanActorFromStore(value.CreatedBy), CreatedAt: value.CreatedAt,
		UpdatedBy: agentWorkflowPlanActorFromStore(value.UpdatedBy), UpdatedAt: value.UpdatedAt,
		ArchivedAt: value.ArchivedAt,
	}
	if value.CurrentVersion != nil {
		plan.CurrentVersion = &manage.VersionRef{ID: value.CurrentVersion.ID}
	}
	if value.DefaultVersion != nil {
		plan.DefaultVersion = &manage.VersionRef{ID: value.DefaultVersion.ID}
	}
	if value.ArchivedBy != nil {
		actor := agentWorkflowPlanActorFromStore(*value.ArchivedBy)
		plan.ArchivedBy = &actor
	}
	return agentWorkflowPlanResponse(plan)
}

func agentWorkflowPlanDraftResponseFromStore(value workflowplanstore.PlanDraft) dto.WorkflowPlanDraftResponse {
	return agentWorkflowPlanDraftResponse(manage.Draft{
		PlanID: value.PlanID, WorkspaceID: value.WorkspaceID, Source: value.Source,
		Layout: append(json.RawMessage(nil), value.Layout...), Revision: value.Revision,
		UpdatedBy: agentWorkflowPlanActorFromStore(value.UpdatedBy),
		CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt,
	})
}

func agentWorkflowPlanActorFromStore(value workflowplanstore.Actor) manage.Actor {
	return manage.Actor{Type: value.Type, ID: value.ID, MemberID: value.MemberID}
}

type agentWorkflowPlanVersionDTO struct {
	ID              string                           `json:"id"`
	PlanID          string                           `json:"planId"`
	DraftRevision   *int64                           `json:"draftRevision,omitempty"`
	SourceVersionID *string                          `json:"sourceVersionId,omitempty"`
	Source          string                           `json:"source,omitempty"`
	SourceCursor    string                           `json:"sourceCursor,omitempty"`
	Layout          json.RawMessage                  `json:"layout,omitempty"`
	LayoutRef       *workflowPayloadRef              `json:"layoutRef,omitempty"`
	CompilerVersion string                           `json:"compilerVersion"`
	Digest          string                           `json:"digest"`
	PublishedBy     dto.WorkflowPlanActorResponse    `json:"publishedBy"`
	PublishedAt     time.Time                        `json:"publishedAt"`
	Payload         *dto.WorkflowPayloadPageResponse `json:"payload,omitempty"`
}

type agentWorkflowPlanValidationDTO struct {
	Diagnostics []dto.WorkflowPlanDiagnosticResponse `json:"diagnostics"`
	Digest      string                               `json:"digest,omitempty"`
}

type agentWorkflowPlanPublishDTO struct {
	Plan    dto.WorkflowPlanResponse    `json:"plan"`
	Version agentWorkflowPlanVersionDTO `json:"version"`
}

func agentWorkflowPlanVersionResponse(value manage.Version) agentWorkflowPlanVersionDTO {
	source, sourceRef, _ := inlineWorkflowTextWithSnapshot("plan", value.WorkspaceID, value.PlanID, value.ID, "", "", "source", "", value.Source, workflowpayload.Snapshot{
		Version: value.ID, Digest: workflowpayload.ContentDigest([]byte(value.Source)),
	})
	layout, layoutRef, _ := inlineWorkflowJSONWithSnapshot("plan", value.WorkspaceID, value.PlanID, value.ID, "", "", "layout", "", value.Layout, workflowpayload.Snapshot{
		Version: value.ID, Digest: workflowpayload.ContentDigest(value.Layout),
	})
	response := agentWorkflowPlanVersionDTO{
		ID: value.ID, PlanID: value.PlanID, DraftRevision: value.DraftRevision,
		SourceVersionID: value.SourceVersionID, Source: source,
		Layout: layout, LayoutRef: layoutRef, CompilerVersion: value.CompilerVersion, Digest: value.Digest,
		PublishedBy: agentWorkflowPlanActorResponse(value.PublishedBy), PublishedAt: value.PublishedAt,
	}
	if sourceRef != nil {
		response.SourceCursor = sourceRef.Cursor
	}
	return response
}

func agentWorkflowPlanValidationResponse(value manage.Validation) agentWorkflowPlanValidationDTO {
	diagnostics := make([]dto.WorkflowPlanDiagnosticResponse, 0, len(value.Diagnostics))
	for _, diagnostic := range value.Diagnostics {
		diagnostics = append(diagnostics, agentWorkflowPlanDiagnosticResponse(diagnostic))
	}
	return agentWorkflowPlanValidationDTO{Diagnostics: diagnostics, Digest: value.Digest}
}

func agentWorkflowPlanDiagnosticResponse(value planlang.Diagnostic) dto.WorkflowPlanDiagnosticResponse {
	return dto.WorkflowPlanDiagnosticResponse{
		Severity: string(value.Severity), Code: value.Code, Path: value.Path, Message: value.Message,
		Line: value.Line, Column: value.Column, NodeID: value.NodeID, EdgeID: value.EdgeID,
	}
}

func agentWorkflowPlanActorResponse(value manage.Actor) dto.WorkflowPlanActorResponse {
	return dto.WorkflowPlanActorResponse{Type: value.Type, ID: value.ID, MemberID: value.MemberID}
}

func agentWorkflowPlanETag(planID string, revision int64) string {
	return fmt.Sprintf(`"workflow-plan:%s:%d"`, strings.TrimSpace(planID), revision)
}

func agentWorkflowPlanDraftETag(planID string, revision int64) string {
	return fmt.Sprintf(`"workflow-plan-draft:%s:%d"`, strings.TrimSpace(planID), revision)
}

func agentWorkflowPlanOK(plan manage.Plan) api.Response {
	return api.Response{
		Status:  http.StatusOK,
		Body:    agentWorkflowPlanResponse(plan),
		Headers: http.Header{"ETag": []string{agentWorkflowPlanETag(plan.ID, plan.Revision)}},
	}
}

func agentWorkflowPlanPublishResponse(status int, plan manage.Plan, version manage.Version) api.Response {
	return api.Response{
		Status: status,
		Body: agentWorkflowPlanPublishDTO{
			Plan: agentWorkflowPlanResponse(plan), Version: agentWorkflowPlanVersionResponse(version),
		},
		Headers: http.Header{"ETag": []string{agentWorkflowPlanETag(plan.ID, plan.Revision)}},
	}
}

func agentWorkflowPlanPublishResponseFromStore(status int, plan workflowplanstore.Plan, version workflowplanstore.PlanVersion) api.Response {
	return api.Response{
		Status: status,
		Body: agentWorkflowPlanPublishDTO{
			Plan: agentWorkflowPlanResponseFromStore(plan), Version: agentWorkflowPlanVersionResponseFromStore(version),
		},
		Headers: http.Header{"ETag": []string{agentWorkflowPlanETag(plan.ID, plan.Revision)}},
	}
}

func agentWorkflowPlanVersionResponseFromStore(value workflowplanstore.PlanVersion) agentWorkflowPlanVersionDTO {
	return agentWorkflowPlanVersionDTO{
		ID: value.ID, PlanID: value.PlanID, DraftRevision: value.DraftRevision,
		SourceVersionID: value.SourceVersionID, Source: value.Source, Layout: value.Layout,
		CompilerVersion: value.CompilerVersion, Digest: value.Digest,
		PublishedBy: agentWorkflowPlanActorResponse(agentWorkflowPlanActorFromStore(value.PublishedBy)), PublishedAt: value.PublishedAt,
	}
}

func classifyAgentWorkflowPlanError(err error) (api.ErrorResponse, bool) {
	// The Plan wake/edit lifecycle errors are canonical API-layer domain
	// failures with a stable code, bounded problem, and stage. Route them
	// through the shared public classifier so every canonical contract field
	// (stage, path, hint, retryability, diagnostics) survives the
	// classifier→ErrorResponse→writer path instead of an ad-hoc envelope.
	if errors.Is(err, manage.ErrAgentEditRevisionMismatch) {
		return workflowErrorResponse(classifyPlanWakeEditError("workflow_agent_edit_revision_mismatch", "Workflow Agent edit revision does not match the request baseline"), nil), true
	}
	if errors.Is(err, manage.ErrAgentWakeActive) {
		return workflowErrorResponse(classifyPlanWakeEditError("workflow_agent_wake_active", "Workflow Agent wake is active"), nil), true
	}
	classified, ok := publicerr.Classify(err, publicerr.Options{
		Stage:       publicerr.StagePlan,
		DefaultCode: "plan_request_failed",
		Unredacted:  true,
		StatusForCode: func(code string, status int) int {
			if code == string(manage.ErrorCodeRevisionConflict) || code == string(manage.ErrorCodeDraftConflict) {
				return http.StatusPreconditionFailed
			}
			return status
		},
	})
	if !ok {
		return api.ErrorResponse{}, false
	}
	return workflowErrorResponse(classified, nil), true
}

// classifyPlanWakeEditError projects a Plan Agent wake/edit lifecycle error
// through the canonical public Workflow classifier with a stable code, the
// owning Plan stage, a bounded problem, and no raw cause. The wrapped
// storeerr.ErrConflict kind yields the 409 status; every canonical contract
// field (stage/path/hint/retryable/diagnostics) is carried on the response.
func classifyPlanWakeEditError(code string, problem string) publicerr.Classified {
	classified, _ := publicerr.Classify(
		publicerr.WorkflowError{Kind: storeerr.ErrConflict, Code: code, Problem: problem},
		publicerr.Options{Stage: publicerr.StagePlan, DefaultCode: "plan_request_failed"},
	)
	return classified
}
