package runmanage

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	workflowrunstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/run"
	"github.com/tidbcloud/AgentTeam/internal/workflow/runartifact"
)

// Repository is the narrow first-class Run read/control boundary. It mirrors
// the existing store without granting this human surface access to runtime
// leases, turn mutation, effect dispatch, or Run creation.
type ArtifactRepository interface {
	ReadArtifactPage(context.Context, runartifact.Ref, string, []string, string, int) (workflowrunstore.ArtifactPage, error)
}

type Repository interface {
	LoadRun(context.Context, string, string) (workflowrunstore.Run, error)
	ListRuns(context.Context, workflowrunstore.ListRunsParams) ([]workflowrunstore.Run, error)
	ListActivations(context.Context, workflowrunstore.ListChildrenParams) ([]workflowrunstore.Activation, error)
	ListEffects(context.Context, workflowrunstore.ListChildrenParams) ([]workflowrunstore.Effect, error)
	ListEvents(context.Context, workflowrunstore.ListEventsParams) ([]workflowrunstore.Event, error)
	LoadEvent(context.Context, string, string, int64) (workflowrunstore.Event, error)
	LoadEffectQueueContexts(context.Context, string, string) (map[string]workflowrunstore.EffectQueueContext, error)
	CancelRun(context.Context, workflowrunstore.CancelRunParams) (workflowrunstore.Run, *workflowrunstore.Event, error)
}

type Service struct {
	repository Repository
	clock      Clock
}

type Option func(*Service)

func WithClock(clock Clock) Option {
	return func(service *Service) {
		if clock != nil {
			service.clock = clock
		}
	}
}

func New(repository Repository, options ...Option) *Service {
	service := &Service{repository: repository, clock: systemClock{}}
	for _, option := range options {
		if option != nil {
			option(service)
		}
	}
	return service
}

func (service *Service) validate() error {
	if service == nil || service.repository == nil {
		return fmt.Errorf("Workflow Run repository is not configured")
	}
	if service.clock == nil {
		return fmt.Errorf("Workflow Run clock is not configured")
	}
	return nil
}

type runListLoader func(context.Context, workflowrunstore.ListRunsParams) ([]workflowrunstore.Run, error)

func (service *Service) ListRuns(ctx context.Context, input ListRunsInput) (RunPage, error) {
	return service.listRuns(ctx, input, service.repository.ListRuns)
}

func (service *Service) listRuns(ctx context.Context, input ListRunsInput, load runListLoader) (RunPage, error) {
	if err := service.validate(); err != nil {
		return RunPage{}, err
	}
	if load == nil {
		return RunPage{}, fmt.Errorf("Workflow Run list repository is not configured")
	}
	input, err := normalizeRunList(input)
	if err != nil {
		return RunPage{}, err
	}
	cursor, err := decodeCursor(input.Cursor, cursorKindRuns, input.WorkspaceID, input.PlanID, "", input.Status)
	if err != nil {
		return RunPage{}, err
	}
	values, err := load(ctx, workflowrunstore.ListRunsParams{
		WorkspaceID: input.WorkspaceID, PlanID: input.PlanID, Status: input.Status,
		BeforeStartedAt: cursor.BeforeStartedAt, BeforeID: cursor.AfterID, Limit: input.Limit + 1,
	})
	if err != nil {
		return RunPage{}, mapRepositoryError(err)
	}
	hasMore := len(values) > input.Limit
	if hasMore {
		values = values[:input.Limit]
	}
	for index, value := range values {
		if err := validateRun(value, input.WorkspaceID, ""); err != nil ||
			(input.PlanID != "" && value.PlanID != input.PlanID) ||
			(input.Status != "" && value.Status != input.Status) ||
			(index == 0 && cursor.AfterID != "" && !runComesAfterCursor(value, cursor)) ||
			(index > 0 && !runComesAfter(values[index-1], value)) {
			return RunPage{}, fmt.Errorf("list Workflow Runs: repository returned an invalid or out-of-scope resource")
		}
	}
	page := RunPage{Items: values}
	if page.Items == nil {
		page.Items = []workflowrunstore.Run{}
	}
	if hasMore {
		page.NextCursor, err = encodeCursor(pageCursor{
			Kind: cursorKindRuns, WorkspaceID: input.WorkspaceID, PlanID: input.PlanID, Status: input.Status,
			AfterID: values[len(values)-1].ID, BeforeStartedAt: values[len(values)-1].StartedAt,
		})
		if err != nil {
			return RunPage{}, err
		}
	}
	return page, nil
}

func runComesAfter(previous, current workflowrunstore.Run) bool {
	if previous.StartedAt == nil {
		return current.StartedAt == nil && current.ID < previous.ID
	}
	if current.StartedAt == nil {
		return true
	}
	if current.StartedAt.Equal(*previous.StartedAt) {
		return current.ID < previous.ID
	}
	return current.StartedAt.Before(*previous.StartedAt)
}

func runComesAfterCursor(current workflowrunstore.Run, cursor pageCursor) bool {
	previous := workflowrunstore.Run{ID: cursor.AfterID, StartedAt: cursor.BeforeStartedAt}
	return runComesAfter(previous, current)
}

func (service *Service) GetRun(ctx context.Context, workspaceID, runID string) (workflowrunstore.Run, error) {
	if err := service.validate(); err != nil {
		return workflowrunstore.Run{}, err
	}
	workspaceID, err := requiredID("workspaceId", workspaceID)
	if err != nil {
		return workflowrunstore.Run{}, err
	}
	runID, err = requiredID("runId", runID)
	if err != nil {
		return workflowrunstore.Run{}, err
	}
	value, err := service.repository.LoadRun(ctx, workspaceID, runID)
	if err != nil {
		return workflowrunstore.Run{}, mapRepositoryError(err)
	}
	if err := validateRun(value, workspaceID, runID); err != nil {
		return workflowrunstore.Run{}, fmt.Errorf("get Workflow Run: %w", err)
	}
	return value, nil
}

func (service *Service) ReadArtifactPage(ctx context.Context, input ArtifactReadInput) (ArtifactPage, error) {
	if err := service.validate(); err != nil {
		return ArtifactPage{}, err
	}
	workspaceID, err := requiredID("workspaceId", input.WorkspaceID)
	if err != nil {
		return ArtifactPage{}, err
	}
	runID, err := requiredID("runId", input.RunID)
	if err != nil {
		return ArtifactPage{}, err
	}
	if input.Reference.WorkspaceID != workspaceID || input.Reference.RunID != runID {
		return ArtifactPage{}, storeerr.ErrConflict
	}
	if _, err := service.GetRun(ctx, workspaceID, runID); err != nil {
		return ArtifactPage{}, err
	}
	repository, ok := service.repository.(ArtifactRepository)
	if !ok {
		return ArtifactPage{}, fmt.Errorf("Workflow Run artifact reads are not enabled")
	}
	page, err := repository.ReadArtifactPage(ctx, input.Reference, input.Section, input.Path, input.Cursor, input.Limit)
	if err != nil {
		return ArtifactPage{}, mapRepositoryError(err)
	}
	if page.Reference != input.Reference || page.Reference.WorkspaceID != workspaceID || page.Reference.RunID != runID {
		return ArtifactPage{}, fmt.Errorf("read Workflow Run artifact: repository returned an invalid or out-of-scope resource")
	}
	return ArtifactPage{Reference: page.Reference, Section: page.Section, Path: append([]string(nil), page.Path...), Bytes: append([]byte(nil), page.Bytes...), Offset: page.Offset, TotalBytes: page.TotalBytes, NextCursor: page.NextCursor}, nil
}

func (service *Service) ListActivations(ctx context.Context, input ListChildrenInput) (ActivationPage, error) {
	if err := service.validate(); err != nil {
		return ActivationPage{}, err
	}
	input, err := normalizeChildList(input, validActivationStatus)
	if err != nil {
		return ActivationPage{}, err
	}
	if _, err := service.GetRun(ctx, input.WorkspaceID, input.RunID); err != nil {
		return ActivationPage{}, err
	}
	cursor, err := decodeCursor(input.Cursor, cursorKindActivations, input.WorkspaceID, "", input.RunID, input.Status)
	if err != nil {
		return ActivationPage{}, err
	}
	values, err := service.repository.ListActivations(ctx, workflowrunstore.ListChildrenParams{
		WorkspaceID: input.WorkspaceID, RunID: input.RunID, Status: input.Status,
		AfterID: cursor.AfterID, Limit: input.Limit + 1,
	})
	if err != nil {
		return ActivationPage{}, mapRepositoryError(err)
	}
	hasMore := len(values) > input.Limit
	if hasMore {
		values = values[:input.Limit]
	}
	for index, value := range values {
		if strings.TrimSpace(value.ID) == "" || value.WorkspaceID != input.WorkspaceID || value.RunID != input.RunID ||
			!validActivationStatus(value.Status) || (input.Status != "" && value.Status != input.Status) ||
			(index == 0 && cursor.AfterID != "" && value.ID <= cursor.AfterID) ||
			(index > 0 && value.ID <= values[index-1].ID) {
			return ActivationPage{}, fmt.Errorf("list Workflow Run activations: repository returned an invalid or out-of-scope resource")
		}
	}
	page := ActivationPage{Items: values}
	if page.Items == nil {
		page.Items = []workflowrunstore.Activation{}
	}
	if hasMore {
		page.NextCursor, err = encodeCursor(pageCursor{
			Kind: cursorKindActivations, WorkspaceID: input.WorkspaceID, RunID: input.RunID,
			Status: input.Status, AfterID: values[len(values)-1].ID,
		})
		if err != nil {
			return ActivationPage{}, err
		}
	}
	return page, nil
}

func (service *Service) ListEffects(ctx context.Context, input ListChildrenInput) (EffectPage, error) {
	if err := service.validate(); err != nil {
		return EffectPage{}, err
	}
	input, err := normalizeChildList(input, validEffectStatus)
	if err != nil {
		return EffectPage{}, err
	}
	if _, err := service.GetRun(ctx, input.WorkspaceID, input.RunID); err != nil {
		return EffectPage{}, err
	}
	cursor, err := decodeCursor(input.Cursor, cursorKindEffects, input.WorkspaceID, "", input.RunID, input.Status)
	if err != nil {
		return EffectPage{}, err
	}
	values, err := service.repository.ListEffects(ctx, workflowrunstore.ListChildrenParams{
		WorkspaceID: input.WorkspaceID, RunID: input.RunID, Status: input.Status,
		AfterID: cursor.AfterID, Limit: input.Limit + 1,
	})
	if err != nil {
		return EffectPage{}, mapRepositoryError(err)
	}
	hasMore := len(values) > input.Limit
	if hasMore {
		values = values[:input.Limit]
	}
	for index, value := range values {
		if strings.TrimSpace(value.ID) == "" || value.WorkspaceID != input.WorkspaceID || value.RunID != input.RunID ||
			!validEffectStatus(value.Status) || (input.Status != "" && value.Status != input.Status) ||
			(index == 0 && cursor.AfterID != "" && value.ID <= cursor.AfterID) ||
			(index > 0 && value.ID <= values[index-1].ID) {
			return EffectPage{}, fmt.Errorf("list Workflow Run effects: repository returned an invalid or out-of-scope resource")
		}
	}
	page := EffectPage{Items: values}
	if page.Items == nil {
		page.Items = []workflowrunstore.Effect{}
	}
	if hasMore {
		page.NextCursor, err = encodeCursor(pageCursor{
			Kind: cursorKindEffects, WorkspaceID: input.WorkspaceID, RunID: input.RunID, Status: input.Status, AfterID: values[len(values)-1].ID,
		})
		if err != nil {
			return EffectPage{}, err
		}
	}
	return page, nil
}

// ListEffectQueueContexts returns the callback queue/lease read-model for every
// effect of one Run so the UI can render "Waiting for @Agent", queue phase, and
// the current execution-lease owner.
func (service *Service) ListEffectQueueContexts(ctx context.Context, workspaceID, runID string) (map[string]workflowrunstore.EffectQueueContext, error) {
	if err := service.validate(); err != nil {
		return nil, err
	}
	contexts, err := service.repository.LoadEffectQueueContexts(ctx, workspaceID, runID)
	if err != nil {
		return nil, mapRepositoryError(err)
	}
	return contexts, nil
}

func (service *Service) GetEvent(ctx context.Context, workspaceID, runID string, sequence int64) (workflowrunstore.Event, error) {
	if err := service.validate(); err != nil {
		return workflowrunstore.Event{}, err
	}
	workspaceID, err := requiredID("workspaceId", workspaceID)
	if err != nil {
		return workflowrunstore.Event{}, err
	}
	runID, err = requiredID("runId", runID)
	if err != nil || sequence < 1 {
		if err != nil {
			return workflowrunstore.Event{}, err
		}
		return workflowrunstore.Event{}, invalidRequest("sequence must be positive")
	}
	if _, err := service.GetRun(ctx, workspaceID, runID); err != nil {
		return workflowrunstore.Event{}, err
	}
	value, err := service.repository.LoadEvent(ctx, workspaceID, runID, sequence)
	if err != nil {
		return workflowrunstore.Event{}, mapRepositoryError(err)
	}
	if value.WorkspaceID != workspaceID || value.RunID != runID || value.Seq != sequence {
		return workflowrunstore.Event{}, fmt.Errorf("get Workflow Run event: repository returned an out-of-scope resource")
	}
	return value, nil
}

func (service *Service) ListEvents(ctx context.Context, input ListEventsInput) (EventPage, error) {
	if err := service.validate(); err != nil {
		return EventPage{}, err
	}
	input, err := normalizeEventList(input)
	if err != nil {
		return EventPage{}, err
	}
	if _, err := service.GetRun(ctx, input.WorkspaceID, input.RunID); err != nil {
		return EventPage{}, err
	}
	cursor, err := decodeCursor(input.Cursor, cursorKindEvents, input.WorkspaceID, "", input.RunID, "")
	if err != nil {
		return EventPage{}, err
	}
	values, err := service.repository.ListEvents(ctx, workflowrunstore.ListEventsParams{
		WorkspaceID: input.WorkspaceID, RunID: input.RunID, AfterSeq: cursor.AfterSeq, Limit: input.Limit + 1,
	})
	if err != nil {
		return EventPage{}, mapRepositoryError(err)
	}
	hasMore := len(values) > input.Limit
	if hasMore {
		values = values[:input.Limit]
	}
	for index, value := range values {
		if strings.TrimSpace(value.ID) == "" || value.WorkspaceID != input.WorkspaceID || value.RunID != input.RunID ||
			value.Seq < 1 || strings.TrimSpace(value.Type) == "" ||
			(index == 0 && cursor.AfterSeq > 0 && value.Seq <= cursor.AfterSeq) ||
			(index > 0 && value.Seq <= values[index-1].Seq) {
			return EventPage{}, fmt.Errorf("list Workflow Run events: repository returned an invalid or out-of-scope resource")
		}
	}
	page := EventPage{Items: values}
	if page.Items == nil {
		page.Items = []workflowrunstore.Event{}
	}
	if hasMore || (input.ContinueCursor && len(values) > 0) {
		page.NextCursor, err = encodeCursor(pageCursor{
			Kind: cursorKindEvents, WorkspaceID: input.WorkspaceID, RunID: input.RunID,
			AfterSeq: values[len(values)-1].Seq,
		})
		if err != nil {
			return EventPage{}, err
		}
	}
	return page, nil
}

func (service *Service) Cancel(ctx context.Context, input CancelInput) (CancelResult, error) {
	return service.cancelUsing(ctx, input, service.repository.CancelRun)
}

type cancelRepository func(context.Context, workflowrunstore.CancelRunParams) (workflowrunstore.Run, *workflowrunstore.Event, error)

func (service *Service) cancelUsing(ctx context.Context, input CancelInput, cancel cancelRepository) (CancelResult, error) {
	if err := service.validate(); err != nil {
		return CancelResult{}, err
	}
	input, err := normalizeCancel(input)
	if err != nil {
		return CancelResult{}, err
	}
	reason, err := json.Marshal(struct {
		Reason string `json:"reason"`
		Actor  Actor  `json:"actor"`
	}{Reason: input.Reason, Actor: input.Actor})
	if err != nil {
		return CancelResult{}, fmt.Errorf("encode Workflow Run cancellation provenance: %w", err)
	}
	value, event, err := cancel(ctx, workflowrunstore.CancelRunParams{
		WorkspaceID: input.WorkspaceID, RunID: input.RunID, ReasonJSON: reason,
		Now: service.clock.Now().UTC(),
	})
	if err != nil {
		if errors.Is(err, storeerr.ErrConflict) {
			current, loadErr := service.repository.LoadRun(ctx, input.WorkspaceID, input.RunID)
			if loadErr == nil && validateRun(current, input.WorkspaceID, input.RunID) == nil {
				switch current.Status {
				case workflowrunstore.RunStatusCanceled:
					return CancelResult{Run: current, Replayed: true}, nil
				case workflowrunstore.RunStatusSucceeded, workflowrunstore.RunStatusFailed:
					return CancelResult{}, terminalConflict(current.Status)
				}
			}
		}
		return CancelResult{}, mapRepositoryError(err)
	}
	if err := validateRun(value, input.WorkspaceID, input.RunID); err != nil || value.Status != workflowrunstore.RunStatusCanceled {
		return CancelResult{}, fmt.Errorf("cancel Workflow Run: repository returned an invalid or non-canceled resource")
	}
	if event != nil && (event.WorkspaceID != input.WorkspaceID || event.RunID != input.RunID ||
		event.Type != "run_canceled" || event.Seq < 1 || strings.TrimSpace(event.ID) == "") {
		return CancelResult{}, fmt.Errorf("cancel Workflow Run: repository returned an invalid cancellation event")
	}
	return CancelResult{Run: value, Event: event, Replayed: event == nil}, nil
}

func validateRun(value workflowrunstore.Run, workspaceID, runID string) error {
	if strings.TrimSpace(value.ID) == "" || value.WorkspaceID != workspaceID ||
		(runID != "" && value.ID != runID) || !validRunStatus(value.Status) ||
		strings.TrimSpace(value.InvocationID) == "" || strings.TrimSpace(value.PlanID) == "" ||
		strings.TrimSpace(value.PlanVersionID) == "" || value.Revision < 1 || value.QueuedAt.IsZero() {
		return fmt.Errorf("repository returned an invalid Workflow Run")
	}
	return nil
}

func mapRepositoryError(err error) error {
	if err == nil {
		return nil
	}
	var requestError RequestError
	if errors.As(err, &requestError) {
		return err
	}
	switch {
	case errors.Is(err, storeerr.ErrNotFound):
		return RequestError{Kind: storeerr.ErrNotFound, Code: ErrorCodeNotFound, Message: "Workflow Run not found", Cause: err}
	case errors.Is(err, storeerr.ErrBadRequest):
		return RequestError{Kind: storeerr.ErrBadRequest, Code: ErrorCodeInvalidRequest, Message: "Workflow Run request is invalid", Cause: err}
	case errors.Is(err, storeerr.ErrForbidden), errors.Is(err, storeerr.ErrUnauthorized):
		return RequestError{Kind: storeerr.ErrForbidden, Code: ErrorCodeForbidden, Message: "Workflow Run is forbidden", Cause: err}
	case errors.Is(err, storeerr.ErrConflict):
		return RequestError{Kind: storeerr.ErrConflict, Code: ErrorCodeConflict, Message: "Workflow Run mutation conflicts with the current state", Cause: err}
	default:
		return err
	}
}
