package agent

import (
	"context"
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	workflowplan "github.com/tidbcloud/AgentTeam/internal/api/workflow/plan"
	serviceteamworkflow "github.com/tidbcloud/AgentTeam/internal/service/teamworkflow"
	workflowpayload "github.com/tidbcloud/AgentTeam/internal/workflow/payload"
	manage "github.com/tidbcloud/AgentTeam/internal/workflow/planmanage"
)

type agentWorkflowPlanContext = workflowCLIContext
type agentWorkflowPlanEnvelope = workflowCLIEnvelope

type agentWorkflowPlanHandler struct {
	service       workflowplan.Service
	teamWorkflows AgentTeamWorkflowPlanService
	agentEdit     WorkflowPlanAgentEditLifecycleGuard
}

type WorkflowPlanAgentEditLifecycleGuard interface {
	RequireAgentDraftUpdateAllowed(ctx context.Context, workspaceID, planID, agentID string, expectedRevision int64) error
}

func registerAgentWorkflowPlanRoutes(
	router gin.IRoutes,
	base string,
	auth AgentAuthenticator,
	service workflowplan.Service,
	teamWorkflows AgentTeamWorkflowPlanService,
	agentEdit WorkflowPlanAgentEditLifecycleGuard,
	recovery ...WorkflowRecoveryResetter,
) {
	if router == nil || auth == nil || service == nil || strings.TrimSpace(base) == "" {
		return
	}
	handler := &agentWorkflowPlanHandler{
		service: service, teamWorkflows: teamWorkflows, agentEdit: agentEdit,
	}
	router.POST(
		strings.TrimSuffix(base, "/")+"/workflow/plan",
		RequireAgentRuntimePrincipal(auth),
		api.HandleAgentScoped(
			handler.call,
			api.WithErrorClassifier(classifyAgentWorkflowPlanError),
			workflowRecoverySuccessOption(recovery...),
		),
	)
}

func (handler *agentWorkflowPlanHandler) call(request api.AgentScopedRequest) (api.Response, error) {
	envelope, err := decodeAgentWorkflowPlanJSON[agentWorkflowPlanEnvelope](request.Gin)
	if err != nil {
		return api.Response{}, err
	}
	if err := validateAgentWorkflowPlanContext(request, envelope.Context); err != nil {
		return api.Response{}, err
	}
	action, err := agentWorkflowPlanAction(envelope.Input)
	if err != nil {
		return api.Response{}, err
	}

	workspaceID := strings.TrimSpace(request.Principal.WorkspaceID)
	actor := manage.Actor{
		Type: manage.ActorTypeAgent, ID: strings.TrimSpace(request.Principal.AgentID), MemberID: strings.TrimSpace(request.Principal.MemberID),
	}

	switch action {
	case "create":
		input, err := decodeAgentWorkflowPlanJSONBytes[dto.AgentWorkflowPlanCreateInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		source, err := requiredAgentWorkflowPlanSource(input.Source)
		if err != nil {
			return api.Response{}, err
		}
		idempotencyKey, err := verifyAgentWorkflowPlanIdempotency(request, envelope.Context, action)
		if err != nil {
			return api.Response{}, err
		}
		teamID := strings.TrimSpace(input.TeamID)
		if teamID != "" {
			if handler.teamWorkflows == nil {
				return api.Response{}, publicError(http.StatusServiceUnavailable, "team_workflow_unavailable", "Team Workflow authoring is unavailable")
			}
			created, err := handler.teamWorkflows.Create(request.Context(), request.Principal, serviceteamworkflow.CreateInput{
				TeamID: teamID, SourceMessageID: envelope.Context.CommandID,
				PlanKey: input.Key, DisplayName: input.DisplayName, DescriptionMD: input.DescriptionMD,
				Source: source, Layout: []byte(`{}`), IdempotencyKey: idempotencyKey,
			})
			if err != nil {
				return api.Response{}, err
			}
			plan := agentWorkflowPlanResponseFromStore(created.Plan)
			draft := agentWorkflowPlanDraftResponseFromStore(created.Draft)
			return api.Response{
				Status: http.StatusCreated,
				Body:   dto.WorkflowPlanCreateResponse{Plan: plan, Draft: draft},
				Headers: http.Header{
					"ETag":              []string{agentWorkflowPlanETag(created.Plan.ID, created.Plan.Revision)},
					"X-Plan-Draft-ETag": []string{agentWorkflowPlanDraftETag(created.Draft.PlanID, created.Draft.Revision)},
				},
			}, nil
		}
		plan, draft, err := handler.service.Create(request.Context(), manage.CreateInput{
			WorkspaceID: workspaceID, Key: input.Key, DisplayName: input.DisplayName, DescriptionMD: input.DescriptionMD,
			Source: source, Layout: []byte(`{}`), Actor: actor, IdempotencyKey: idempotencyKey,
		})
		if err != nil {
			return api.Response{}, err
		}
		return api.Response{
			Status: http.StatusCreated,
			Body: dto.WorkflowPlanCreateResponse{
				Plan: agentWorkflowPlanResponse(plan), Draft: agentWorkflowPlanDraftResponse(draft),
			},
			Headers: http.Header{
				"ETag":              []string{agentWorkflowPlanETag(plan.ID, plan.Revision)},
				"X-Plan-Draft-ETag": []string{agentWorkflowPlanDraftETag(draft.PlanID, draft.Revision)},
			},
		}, nil
	case "list":
		input, err := decodeAgentWorkflowPlanJSONBytes[dto.AgentWorkflowPlanListInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		limit, err := agentWorkflowPlanLimit(input.Limit)
		if err != nil {
			return api.Response{}, err
		}
		teamID := strings.TrimSpace(input.TeamID)
		if teamID != "" {
			if input.IncludeArchived {
				return api.Response{}, publicError(http.StatusBadRequest, "plan_invalid_request", "Team Workflow list contains approved usable Plans only")
			}
			if handler.teamWorkflows == nil {
				return api.Response{}, publicError(http.StatusServiceUnavailable, "team_workflow_unavailable", "Team Workflow discovery is unavailable")
			}
			page, err := handler.teamWorkflows.ListAvailable(request.Context(), request.Principal, teamID, limit, strings.TrimSpace(input.Cursor))
			if err != nil {
				return api.Response{}, err
			}
			items := make([]dto.WorkflowPlanResponse, 0, len(page.Items))
			for _, item := range page.Items {
				items = append(items, agentWorkflowPlanResponseFromStore(item))
			}
			return api.OK(dto.WorkflowPlanListResponse{Items: items, NextCursor: page.NextCursor}), nil
		}
		page, err := handler.service.List(request.Context(), manage.ListInput{
			WorkspaceID: workspaceID, IncludeArchived: input.IncludeArchived, Limit: limit, Cursor: strings.TrimSpace(input.Cursor),
		})
		if err != nil {
			return api.Response{}, err
		}
		items := make([]dto.WorkflowPlanResponse, 0, len(page.Items))
		for _, item := range page.Items {
			items = append(items, agentWorkflowPlanResponse(item))
		}
		return api.OK(dto.WorkflowPlanListResponse{Items: items, NextCursor: page.NextCursor}), nil
	case "show":
		input, err := decodeAgentWorkflowPlanJSONBytes[dto.AgentWorkflowPlanShowInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		planID, err := requiredAgentWorkflowPlanID(input.PlanID)
		if err != nil {
			return api.Response{}, err
		}
		plan, err := handler.service.Get(request.Context(), workspaceID, planID)
		if err != nil {
			return api.Response{}, err
		}
		draft, err := handler.service.GetDraft(request.Context(), workspaceID, planID)
		if err != nil {
			return api.Response{}, err
		}
		section := strings.TrimSpace(input.Section)
		if section == "" || section == "compact" {
			view, err := handler.agentView(request.Context(), request.Principal, workspaceID, plan, draft)
			if err != nil {
				return api.Response{}, err
			}
			if section == "compact" {
				view.Source = ""
				if len(draft.Source) > 0 {
					ref, refErr := workflowPayloadRefForTextWithSnapshot("plan", workspaceID, planID, "", "", "", "source", "", len(draft.Source), workflowpayload.Snapshot{
						Version: strconv.FormatInt(draft.Revision, 10), Digest: workflowpayload.ContentDigest([]byte(draft.Source)),
					})
					if refErr != nil {
						return api.Response{}, refErr
					}
					view.SourceCursor = ref.Cursor
				}
			}
			return api.OK(view), nil
		}
		if section != "source" {
			return api.Response{}, publicError(http.StatusBadRequest, "plan_invalid_request", "section must be compact or source")
		}
		if strings.TrimSpace(input.Path) != "" {
			return api.Response{}, publicError(http.StatusBadRequest, "plan_invalid_request", "source does not accept path")
		}
		limit, err := agentWorkflowPayloadLimit(input.Limit)
		if err != nil {
			return api.Response{}, publicError(http.StatusBadRequest, "plan_invalid_request", err.Error())
		}
		page, err := workflowPayloadPageForTextWithSnapshot("plan", workspaceID, planID, "", "", "", "source", "", strings.TrimSpace(input.Cursor), limit, draft.Source, workflowpayload.Snapshot{
			Version: strconv.FormatInt(draft.Revision, 10), Digest: workflowpayload.ContentDigest([]byte(draft.Source)),
		})
		if err != nil {
			return api.Response{}, agentWorkflowPayloadError("plan", err)
		}
		view, err := handler.agentView(request.Context(), request.Principal, workspaceID, plan, draft)
		if err != nil {
			return api.Response{}, err
		}
		view.Source = ""
		view.SourceCursor = ""
		view.Payload = &dto.WorkflowPayloadPageResponse{
			Section: page.Section, Encoding: page.Encoding, Value: page.Value, NextCursor: page.NextCursor,
		}
		return api.OK(view), nil

	case "version":
		input, err := decodeAgentWorkflowPlanJSONBytes[dto.AgentWorkflowPlanVersionGetInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		planID, err := requiredAgentWorkflowPlanID(input.PlanID)
		if err != nil {
			return api.Response{}, err
		}
		versionID := strings.TrimSpace(input.VersionID)
		if versionID == "" {
			return api.Response{}, publicError(http.StatusBadRequest, "plan_invalid_request", "versionId is required")
		}
		version, err := handler.service.GetVersion(request.Context(), workspaceID, planID, versionID)
		if err != nil {
			return api.Response{}, err
		}
		section := strings.TrimSpace(input.Section)
		if section == "" || section == "compact" {
			return api.OK(agentWorkflowPlanVersionResponse(version)), nil
		}
		if section != "source" && section != "layout" {
			return api.Response{}, publicError(http.StatusBadRequest, "plan_invalid_request", "section must be compact, source, or layout")
		}
		limit, err := agentWorkflowPayloadLimit(input.Limit)
		if err != nil {
			return api.Response{}, publicError(http.StatusBadRequest, "plan_invalid_request", err.Error())
		}
		var page workflowPayloadPage
		if section == "source" {
			if strings.TrimSpace(input.Path) != "" {
				return api.Response{}, publicError(http.StatusBadRequest, "plan_invalid_request", "source does not accept path")
			}
			page, err = workflowPayloadPageForTextWithSnapshot("plan", workspaceID, planID, versionID, "", "", section, "", strings.TrimSpace(input.Cursor), limit, version.Source, workflowpayload.Snapshot{
				Version: version.ID, Digest: workflowpayload.ContentDigest([]byte(version.Source)),
			})
		} else {
			page, err = workflowPayloadPageForJSONWithSnapshot("plan", workspaceID, planID, versionID, "", "", section, input.Path, strings.TrimSpace(input.Cursor), limit, version.Layout, workflowpayload.Snapshot{
				Version: version.ID, Digest: workflowpayload.ContentDigest(version.Layout),
			})
		}
		if err != nil {
			return api.Response{}, agentWorkflowPayloadError("plan", err)
		}
		versionResponse := agentWorkflowPlanVersionResponse(version)
		versionResponse.Source = ""
		versionResponse.SourceCursor = ""
		versionResponse.Payload = &dto.WorkflowPayloadPageResponse{
			Section: page.Section, Encoding: page.Encoding, Value: page.Value, NextCursor: page.NextCursor,
		}
		return api.OK(versionResponse), nil
	case "validate":
		input, err := decodeAgentWorkflowPlanJSONBytes[dto.AgentWorkflowPlanValidateInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		source, err := requiredAgentWorkflowPlanSource(input.Source)
		if err != nil {
			return api.Response{}, err
		}
		return api.OK(agentWorkflowPlanValidationResponse(handler.service.Validate(request.Context(), source))), nil
	case "update":
		input, err := decodeAgentWorkflowPlanJSONBytes[dto.AgentWorkflowPlanReviseInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		planID, err := requiredAgentWorkflowPlanID(input.PlanID)
		if err != nil {
			return api.Response{}, err
		}
		if err := requireAgentWorkflowPlanPositiveRevision("expectedRevision", input.ExpectedRevision); err != nil {
			return api.Response{}, err
		}
		source, err := requiredAgentWorkflowPlanSource(input.Source)
		if err != nil {
			return api.Response{}, err
		}
		// Team-linked Plans no longer enter the generic Approval subject. The
		// authenticated Team leader publishes the exact compiled update in one
		// transaction with the draft and relation CAS fences. Resolve the
		// durable Team link when older callers omit teamId.
		teamID := strings.TrimSpace(input.TeamID)
		if teamID == "" && handler.teamWorkflows != nil {
			if manager, ok := handler.teamWorkflows.(interface {
				ResolvePlanManagement(context.Context, string, string) (string, string, bool, bool, error)
			}); ok {
				resolvedTeamID, _, deleted, linked, err := manager.ResolvePlanManagement(request.Context(), workspaceID, planID)
				if err != nil {
					return api.Response{}, err
				}
				if linked {
					if deleted {
						return api.Response{}, publicError(http.StatusConflict, "team_workflow_deleted", "Team Workflow is deleted")
					}
					teamID = strings.TrimSpace(resolvedTeamID)
				}
			}
		}
		if teamID != "" && handler.teamWorkflows != nil {
			expectedPlanRevision := input.ExpectedPlanRevision
			if expectedPlanRevision <= 0 {
				currentPlan, err := handler.service.Get(request.Context(), workspaceID, planID)
				if err != nil {
					return api.Response{}, err
				}
				expectedPlanRevision = currentPlan.Revision
			}
			draftRevision := input.DraftRevision
			if draftRevision <= 0 {
				draftRevision = input.ExpectedRevision
			}
			if _, err := verifyAgentWorkflowPlanIdempotency(request, envelope.Context, action); err != nil {
				return api.Response{}, err
			}
			updated, err := handler.teamWorkflows.Update(request.Context(), request.Principal, serviceteamworkflow.UpdateInput{
				TeamID: teamID, PlanID: planID, ExpectedPlanRevision: expectedPlanRevision,
				DraftRevision: draftRevision, Source: source, Layout: input.Layout,
				MakeDefault: input.MakeDefault, ClientRequestID: envelope.Context.CommandID,
			})
			if err != nil {
				return api.Response{}, err
			}
			return agentWorkflowPlanPublishResponseFromStore(http.StatusOK, updated.Plan, updated.Version), nil
		}
		if err := handler.requireAgentEditDraftUpdateAllowed(
			request.Context(), workspaceID, planID, actor.ID, input.ExpectedRevision,
		); err != nil {
			return api.Response{}, err
		}
		draft, err := handler.service.SaveDraft(request.Context(), manage.SaveDraftInput{
			WorkspaceID: workspaceID, PlanID: planID, ExpectedRevision: input.ExpectedRevision,
			Source: source, Layout: input.Layout, Actor: actor,
		})
		if err != nil {
			return api.Response{}, err
		}
		if err := requireAgentWorkflowPlanDraftChanged(input.ExpectedRevision, draft.Revision); err != nil {
			return api.Response{}, err
		}
		return api.Response{
			Status: http.StatusOK, Body: agentWorkflowPlanDraftResponse(draft),
			Headers: http.Header{"ETag": []string{agentWorkflowPlanDraftETag(draft.PlanID, draft.Revision)}},
		}, nil
	case "publish":
		input, err := decodeAgentWorkflowPlanJSONBytes[dto.AgentWorkflowPlanPublishInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		planID, err := requiredAgentWorkflowPlanID(input.PlanID)
		if err != nil {
			return api.Response{}, err
		}
		if err := requireAgentWorkflowPlanPositiveRevision("expectedRevision", input.ExpectedRevision); err != nil {
			return api.Response{}, err
		}
		if err := handler.requireDirectPublishAllowed(request.Context(), workspaceID, planID); err != nil {
			return api.Response{}, err
		}
		draft, err := handler.service.GetDraft(request.Context(), workspaceID, planID)
		if err != nil {
			return api.Response{}, err
		}
		if draft.Revision != input.ExpectedRevision {
			return api.Response{}, publicError(http.StatusConflict, "plan_revision_conflict", "working revision changed")
		}
		planState, err := handler.service.Get(request.Context(), workspaceID, planID)
		if err != nil {
			return api.Response{}, err
		}
		idempotencyKey, err := verifyAgentWorkflowPlanIdempotency(request, envelope.Context, action)
		if err != nil {
			return api.Response{}, err
		}
		plan, version, err := handler.service.Publish(request.Context(), manage.PublishInput{
			WorkspaceID: workspaceID, PlanID: planID, ExpectedPlanRevision: planState.Revision,
			DraftRevision: draft.Revision, MakeDefault: input.MakeDefault, Actor: actor, IdempotencyKey: idempotencyKey,
		})
		if err != nil {
			return api.Response{}, err
		}
		return agentWorkflowPlanPublishResponse(http.StatusCreated, plan, version), nil
	case "archive":
		input, err := decodeAgentWorkflowPlanJSONBytes[dto.AgentWorkflowPlanLifecycleInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		planID, err := requiredAgentWorkflowPlanID(input.PlanID)
		if err != nil {
			return api.Response{}, err
		}
		if err := requireAgentWorkflowPlanPositiveRevision("expectedRevision", input.ExpectedRevision); err != nil {
			return api.Response{}, err
		}
		if handler.teamWorkflows != nil {
			teamPlan, linked, err := handler.teamWorkflows.ArchivePlan(
				request.Context(), request.Principal, planID, input.ExpectedRevision,
			)
			if err != nil {
				return api.Response{}, err
			}
			if linked {
				return api.OK(agentWorkflowPlanResponseFromStore(teamPlan)), nil
			}
		}
		if err := handler.requireDirectPublishAllowed(request.Context(), workspaceID, planID); err != nil {
			return api.Response{}, err
		}
		lifecycle := manage.LifecycleInput{
			WorkspaceID: workspaceID, PlanID: planID, ExpectedRevision: input.ExpectedRevision, Actor: actor,
		}
		plan, err := handler.service.Archive(request.Context(), lifecycle)
		if err != nil {
			return api.Response{}, err
		}
		return agentWorkflowPlanOK(plan), nil
	default:
		return api.Response{}, publicError(http.StatusBadRequest, "plan_invalid_action", "action is not supported")
	}
}

func (handler *agentWorkflowPlanHandler) requireAgentEditDraftUpdateAllowed(
	ctx context.Context,
	workspaceID string,
	planID string,
	agentID string,
	expectedRevision int64,
) error {
	if handler.agentEdit == nil {
		return publicError(
			http.StatusServiceUnavailable,
			"workflow_agent_edit_unavailable",
			"Workflow Agent edit lifecycle guard is unavailable",
		)
	}
	return handler.agentEdit.RequireAgentDraftUpdateAllowed(
		ctx, workspaceID, planID, agentID, expectedRevision,
	)
}

func (handler *agentWorkflowPlanHandler) requireDirectPublishAllowed(ctx context.Context, workspaceID, planID string) error {
	if handler.teamWorkflows == nil {
		return nil
	}
	return handler.teamWorkflows.RequireUnlinkedPlan(ctx, workspaceID, planID)
}
