package roadmapsync

import (
	"context"
	"errors"
	"strings"

	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

// ErrNotReady is returned when the develop projection candidate is not ready,
// so a caller never silently projects a Plane Todo for an unmatched or unpushed
// Milestone.
var ErrNotReady = errors.New("develop projection candidate is not ready")

// ErrInvalidProjectionInput is returned when a required projection input
// (provider ref, todo state, or milestone content) is missing.
var ErrInvalidProjectionInput = errors.New("invalid plane todo projection input")

// preserveCommentPrefix opens the comment that preserves the original Plane
// body before the develop content replaces it. Kept as a stable prefix so the
// preserved request is recognizable in the Plane timeline.
const preserveCommentPrefix = "사용자 요청:"

// PlaneTodoProvider is the minimal set of provider facets the Todo projection
// drives, in execution order: preserve the original body as a comment, replace
// title/body, then move the work item to the Todo state. It is satisfied by the
// Plane adapter Client and by test fakes.
type PlaneTodoProvider interface {
	workitem.Commenter
	workitem.BodyProjector
	workitem.StatusProjector
}

// ProjectPlaneTodoInput carries everything ProjectPlaneTodo needs without
// reaching into provider DTOs: the develop match decision, the original Plane
// body to preserve when present, the Todo state id to move to, and the develop
// Milestone markdown to project. Ref is the provider work item the projection
// targets.
type ProjectPlaneTodoInput struct {
	Candidate    ProjectionCandidate
	Ref          workitem.Ref
	OriginalBody string
	TodoStateID  string
	// MilestoneMarkdown is the develop Milestone content used to build the new
	// Plane title/body via FormatPlaneProjection.
	MilestoneMarkdown string
	// ExternalSource/ExternalID tag the preservation comment so a re-run can be
	// recognized as nomadcode-authored rather than a user request.
	ExternalSource string
	ExternalID     string
}

// ProjectPlaneTodo runs the Plane Todo projection in the order the Milestone
// requires (doc lines 57/102): preserve the original body as a `사용자 요청:`
// comment, then replace the title/body with the develop Milestone content, then
// move the work item to the Todo state. A failure at any step stops the
// sequence so a later step never runs on an unpreserved or unupdated ticket.
//
// When the develop projection candidate is not ready it returns ErrNotReady
// without calling the provider, so a Plane Todo is never produced for an
// unmatched or unpushed Milestone.
func ProjectPlaneTodo(ctx context.Context, p PlaneTodoProvider, in ProjectPlaneTodoInput) error {
	if err := ValidateProjectionInput(p, in); err != nil {
		return err
	}

	// 1. Preserve the original Plane body as a comment before anything mutates it.
	if err := PreserveOriginalComment(ctx, p, in); err != nil {
		return err
	}

	// 2. Replace title/body with the develop Milestone projection.
	if err := UpdatePlaneBody(ctx, p, in); err != nil {
		return err
	}

	// 3. Move the work item to the Todo state.
	return MovePlaneTodo(ctx, p, in)
}

// PreserveOriginalComment is step 1 of the Plane Todo projection: it preserves
// the original Plane body as a `사용자 요청:` comment before any later step
// overwrites the title/body. It is split out of ProjectPlaneTodo so an
// orchestrator can record a per-step ledger mark right after the provider call
// succeeds and skip the step on a resume. Inputs are not re-validated here;
// ProjectPlaneTodo and the orchestrator validate before calling.
func PreserveOriginalComment(ctx context.Context, p workitem.Commenter, in ProjectPlaneTodoInput) error {
	body := preserveCommentPrefix
	if strings.TrimSpace(in.OriginalBody) != "" {
		body += " " + in.OriginalBody
	}
	return p.AppendComment(ctx, workitem.CommentInput{
		Ref:            in.Ref,
		Body:           body,
		Format:         workitem.CommentFormatHTML,
		ExternalSource: in.ExternalSource,
		ExternalID:     in.ExternalID,
	})
}

// UpdatePlaneBody is step 2 of the Plane Todo projection: it replaces the work
// item title/body with the develop Milestone projection. Split out for the same
// per-step ledger reason as PreserveOriginalComment.
func UpdatePlaneBody(ctx context.Context, p workitem.BodyProjector, in ProjectPlaneTodoInput) error {
	projection := FormatPlaneProjection(in.Candidate.Identity.RoadmapMilestonePath, in.MilestoneMarkdown)
	return p.ProjectBody(ctx, workitem.BodyProjection{
		Ref:             in.Ref,
		Title:           projection.Title,
		DescriptionHTML: projection.BodyHTML,
	})
}

// MovePlaneTodo is step 3 of the Plane Todo projection: it moves the work item
// to the Todo state. Split out for the same per-step ledger reason as
// PreserveOriginalComment.
func MovePlaneTodo(ctx context.Context, p workitem.StatusProjector, in ProjectPlaneTodoInput) error {
	return p.ProjectStatus(ctx, workitem.StatusProjection{
		Ref:           in.Ref,
		ProviderState: in.TodoStateID,
	})
}

// ValidateProjectionInput reports the same input guard ProjectPlaneTodo
// enforces, so an orchestrator that drives the steps individually rejects the
// same missing-input cases before calling any provider facet.
func ValidateProjectionInput(p PlaneTodoProvider, in ProjectPlaneTodoInput) error {
	if !in.Candidate.Ready {
		return ErrNotReady
	}
	if p == nil ||
		strings.TrimSpace(string(in.Ref.Provider)) == "" ||
		strings.TrimSpace(in.Ref.ID) == "" ||
		strings.TrimSpace(in.TodoStateID) == "" ||
		strings.TrimSpace(in.MilestoneMarkdown) == "" {
		return ErrInvalidProjectionInput
	}
	return nil
}
