package manage

import (
	"context"
	"fmt"
	"strings"
	"time"

	execschedule "github.com/tidbcloud/AgentTeam/internal/schedule"
	"github.com/tidbcloud/AgentTeam/internal/store/schedule"
)

type scheduleRoute struct {
	InputID  string
	TargetID string
}

func (service *Service) scheduleStore() *schedule.Store {
	return schedule.New(service.db)
}

func (service *Service) resolveScheduleRoute(_ context.Context, _ string, _ string, triggerID string) (scheduleRoute, error) {
	trimmedID := strings.TrimSpace(triggerID)
	if trimmedID == "" {
		return scheduleRoute{}, badRequest("trigger id is required")
	}
	return scheduleRoute{InputID: trimmedID, TargetID: trimmedID}, nil
}

func (service *Service) loadScheduleDefinitionForOwner(ctx context.Context, workspaceID string, ownerMemberID string, definitionID string) (schedule.Definition, error) {
	definition, err := service.scheduleStore().LoadDefinition(ctx, workspaceID, definitionID)
	if err != nil {
		return schedule.Definition{}, err
	}
	if strings.TrimSpace(definition.OwnerMemberID) != strings.TrimSpace(ownerMemberID) && strings.TrimSpace(definition.PrincipalMemberID) != strings.TrimSpace(ownerMemberID) {
		return schedule.Definition{}, notFound("trigger not found")
	}
	return definition, nil
}

func (service *Service) viewFromScheduleDefinition(ctx context.Context, ownerUserID string, definition schedule.Definition, includeValidation bool) (TriggerView, error) {
	record := definitionRecordFromScheduleDefinition(ownerUserID, definition)
	return service.viewFromDefinition(ctx, ownerUserID, record, includeValidation)
}

func (service *Service) viewFromScheduleRoute(ctx context.Context, ownerUserID string, _ scheduleRoute, definition schedule.Definition, includeValidation bool) (TriggerView, error) {
	return service.viewFromScheduleDefinition(ctx, ownerUserID, definition, includeValidation)
}

func definitionRecordFromScheduleDefinition(ownerUserID string, definition schedule.Definition) definitionRecord {
	status := DefinitionStatus(strings.TrimSpace(definition.Status))
	nextOccurrenceAt := definition.NextOccurrenceAt
	if status != StatusActive {
		nextOccurrenceAt = nil
	}
	messageOptions := execschedule.MessageOptionsFromMetadata(definition.Metadata)
	targetKind := TargetKind(strings.TrimSpace(definition.TargetKind))
	return definitionRecord{
		ID:                    strings.TrimSpace(definition.ID),
		WorkspaceID:           strings.TrimSpace(definition.WorkspaceID),
		OwnerUserID:           strings.TrimSpace(ownerUserID),
		OwnerKind:             "human",
		OwnerMemberID:         strings.TrimSpace(definition.OwnerMemberID),
		OwnerMemberIDSnapshot: strings.TrimSpace(definition.OwnerMemberID),
		Name:                  strings.TrimSpace(definition.Name),
		Status:                status,
		SourceKind:            SourceKind(strings.TrimSpace(definition.SourceKind)),
		SourceType:            "time." + strings.TrimSpace(definition.SourceType),
		TimeMode:              TimeMode(strings.TrimSpace(definition.SourceType)),
		ScheduledAt:           definition.ScheduledAt,
		RelativeSeconds:       definition.RelativeSeconds,
		AnchorAt:              definition.AnchorAt,
		IntervalSeconds:       definition.IntervalSeconds,
		CronExpression:        strings.TrimSpace(definition.CronExpression),
		Timezone:              strings.TrimSpace(definition.Timezone),
		TargetKind:            targetKind,
		TargetRef:             strings.TrimSpace(definition.TargetRef),
		MessageTemplate:       strings.TrimSpace(definition.MessageTemplate),
		MentionHints:          messageOptions.MentionHints,
		AlsoStartThread:       messageOptions.AlsoStartThread && targetKind == TargetKindChannel,
		HumanNotificationMode: HumanNotificationMode(strings.TrimSpace(definition.NotificationMode)),
		NextOccurrenceAt:      nextOccurrenceAt,
		LastRunAt:             definition.LastRunAt,
		LastSuccessAt:         definition.LastSuccessAt,
		LastFailureCode:       strings.TrimSpace(definition.LastFailureCode),
		CreatedAt:             definition.CreatedAt,
		UpdatedAt:             definition.UpdatedAt,
	}
}

func (service *Service) scheduleMutationClock(ctx context.Context, record definitionRecord, fallback time.Time) (time.Time, error) {
	if record.TimeMode != TimeModeOneShotRelative || record.RelativeSeconds == nil || *record.RelativeSeconds <= 0 {
		return fallback.UTC(), nil
	}
	materialized := record
	if err := materializeRelativeScheduledAt(ctx, service.db, &materialized); err != nil {
		return time.Time{}, err
	}
	if materialized.ScheduledAt == nil {
		return fallback.UTC(), nil
	}
	return materialized.ScheduledAt.Add(-time.Duration(*record.RelativeSeconds) * time.Second).UTC(), nil
}

func (service *Service) createScheduleDefinitionForOwner(ctx context.Context, ownerUserID string, workspaceID string, input UpsertInput) (TriggerView, error) {
	return service.createScheduleDefinitionForOwnerWithVia(ctx, ownerUserID, workspaceID, input, "schedule_api")
}

func (service *Service) createScheduleDefinitionForOwnerWithVia(ctx context.Context, ownerUserID string, workspaceID string, input UpsertInput, createdVia string) (TriggerView, error) {
	now := service.now()
	normalized, err := service.normalizeMutation(ctx, ownerUserID, workspaceID, input, nil, now, false)
	if err != nil {
		return TriggerView{}, err
	}
	if normalized.validationErr != nil {
		return TriggerView{}, normalized.validationErr
	}
	owner, err := service.loadManageOwner(ctx, workspaceID, ownerUserID)
	if err != nil {
		return TriggerView{}, err
	}
	scheduleNow, err := service.scheduleMutationClock(ctx, normalized.record, now)
	if err != nil {
		return TriggerView{}, err
	}
	metadata, err := mergeScheduleMutationMetadata(nil, normalized.record, input.IdempotencyKey)
	if err != nil {
		return TriggerView{}, err
	}
	deliveryScopeID, permissionScopeID := normalized.record.TargetRef, normalized.record.TargetRef
	created, _, err := service.scheduleStore().CreateDefinition(ctx, schedule.DefinitionCreate{
		WorkspaceID:            strings.TrimSpace(workspaceID),
		OwnerMemberID:          owner.MemberID,
		PrincipalMemberID:      owner.MemberID,
		Name:                   normalized.record.Name,
		Status:                 schedule.DefinitionStatusActive,
		SourceKind:             schedule.SourceKindTime,
		SourceType:             string(normalized.record.TimeMode),
		ScheduledAt:            normalized.record.ScheduledAt,
		RelativeSeconds:        normalized.record.RelativeSeconds,
		AnchorAt:               normalized.record.AnchorAt,
		IntervalSeconds:        normalized.record.IntervalSeconds,
		CronExpression:         normalized.record.CronExpression,
		Timezone:               normalized.record.Timezone,
		TargetKind:             string(normalized.record.TargetKind),
		TargetRef:              normalized.record.TargetRef,
		DeliveryScopeID:        deliveryScopeID,
		PermissionCheckScopeID: permissionScopeID,
		MessageTemplate:        normalized.record.MessageTemplate,
		NotificationMode:       string(normalized.record.HumanNotificationMode),
		IdempotencyKey:         strings.TrimSpace(input.IdempotencyKey),
		CreatedByMemberID:      owner.MemberID,
		CreatedByActorType:     "human",
		CreatedByActorID:       strings.TrimSpace(ownerUserID),
		CreatedVia:             nonEmpty(strings.TrimSpace(createdVia), "schedule_api"),
		Metadata:               metadata,
		Now:                    scheduleNow,
	})
	if err != nil {
		return TriggerView{}, err
	}
	if matched, err := scheduleMutationMatchesExisting(created, normalized.record, input.IdempotencyKey); err != nil {
		return TriggerView{}, err
	} else if strings.TrimSpace(input.IdempotencyKey) != "" && !matched {
		return TriggerView{}, conflict("schedule mutation idempotency key already used for a different payload")
	}
	return scheduleMutationViewFromDefinition(ownerUserID, created, normalized.target, normalized.validation), nil
}

func (service *Service) updateScheduleDefinitionForOwner(ctx context.Context, ownerUserID string, workspaceID string, route scheduleRoute, input UpsertInput) (TriggerView, error) {
	owner, err := service.loadManageOwner(ctx, workspaceID, ownerUserID)
	if err != nil {
		return TriggerView{}, err
	}
	existing, err := service.loadScheduleDefinitionForOwner(ctx, workspaceID, owner.MemberID, route.TargetID)
	if err != nil {
		return TriggerView{}, err
	}
	if existing.Status == schedule.DefinitionStatusCompleted {
		return TriggerView{}, badRequest("trigger_completed")
	}
	existingRecord := definitionRecordFromScheduleDefinition(ownerUserID, existing)
	now := service.now()
	normalized, err := service.normalizeMutation(ctx, ownerUserID, workspaceID, input, &existingRecord, now, false)
	if err != nil {
		return TriggerView{}, err
	}
	if normalized.validationErr != nil {
		return TriggerView{}, normalized.validationErr
	}
	if matched, err := scheduleMutationMatchesExisting(existing, normalized.record, input.IdempotencyKey); err != nil {
		return TriggerView{}, err
	} else if matched {
		return service.viewFromScheduleRoute(ctx, ownerUserID, route, existing, true)
	}
	if input.ExpectedUpdatedAt != nil && !existing.UpdatedAt.UTC().Equal(input.ExpectedUpdatedAt.UTC()) {
		return TriggerView{}, conflict("schedule changed since the provided expectedUpdatedAt")
	}
	scheduleNow, err := service.scheduleMutationClock(ctx, normalized.record, now)
	if err != nil {
		return TriggerView{}, err
	}
	metadata, err := mergeScheduleMutationMetadata(existing.Metadata, normalized.record, input.IdempotencyKey)
	if err != nil {
		return TriggerView{}, err
	}
	deliveryScopeID, permissionScopeID := normalized.record.TargetRef, normalized.record.TargetRef
	updated, err := service.scheduleStore().UpdateDefinition(ctx, schedule.DefinitionUpdate{
		WorkspaceID:            strings.TrimSpace(workspaceID),
		ID:                     existing.ID,
		Name:                   normalized.record.Name,
		Status:                 existing.Status,
		SourceKind:             schedule.SourceKindTime,
		SourceType:             string(normalized.record.TimeMode),
		ScheduledAt:            normalized.record.ScheduledAt,
		RelativeSeconds:        normalized.record.RelativeSeconds,
		AnchorAt:               normalized.record.AnchorAt,
		IntervalSeconds:        normalized.record.IntervalSeconds,
		CronExpression:         normalized.record.CronExpression,
		Timezone:               normalized.record.Timezone,
		TargetKind:             string(normalized.record.TargetKind),
		TargetRef:              normalized.record.TargetRef,
		DeliveryScopeID:        deliveryScopeID,
		PermissionCheckScopeID: permissionScopeID,
		MessageTemplate:        normalized.record.MessageTemplate,
		NotificationMode:       string(normalized.record.HumanNotificationMode),
		Metadata:               metadata,
		Now:                    scheduleNow,
	})
	if err != nil {
		return TriggerView{}, err
	}
	if existing.Status == schedule.DefinitionStatusBlocked {
		if err := service.activateScheduleDefinition(ctx, workspaceID, updated.ID, updated.NextOccurrenceAt, now); err != nil {
			return TriggerView{}, err
		}
		updated, err = service.scheduleStore().LoadDefinition(ctx, workspaceID, updated.ID)
		if err != nil {
			return TriggerView{}, err
		}
	}
	return scheduleMutationViewFromDefinition(ownerUserID, updated, normalized.target, normalized.validation), nil
}

func scheduleMutationViewFromDefinition(ownerUserID string, definition schedule.Definition, target TargetView, validation ValidationView) ScheduleView {
	record := definitionRecordFromScheduleDefinition(ownerUserID, definition)
	return triggerViewFromRecord(record, target, validation)
}

func (service *Service) pauseScheduleDefinitionForOwner(ctx context.Context, ownerUserID string, workspaceID string, route scheduleRoute) (TriggerView, error) {
	return service.pauseScheduleDefinitionForOwnerWithReason(ctx, ownerUserID, workspaceID, route, "schedule_api")
}

func (service *Service) pauseScheduleDefinitionForOwnerWithReason(ctx context.Context, ownerUserID string, workspaceID string, route scheduleRoute, reason string) (TriggerView, error) {
	owner, err := service.loadManageOwner(ctx, workspaceID, ownerUserID)
	if err != nil {
		return TriggerView{}, err
	}
	definition, err := service.loadScheduleDefinitionForOwner(ctx, workspaceID, owner.MemberID, route.TargetID)
	if err != nil {
		return TriggerView{}, err
	}
	if definition.Status == schedule.DefinitionStatusCompleted {
		return TriggerView{}, badRequest("trigger_completed")
	}
	if definition.Status != schedule.DefinitionStatusActive {
		return TriggerView{}, badRequest("only active triggers can be paused")
	}
	updated, err := service.scheduleStore().SetDefinitionStatus(ctx, schedule.DefinitionStatusUpdate{WorkspaceID: workspaceID, ID: definition.ID, Status: schedule.DefinitionStatusPaused, Reason: nonEmpty(strings.TrimSpace(reason), "schedule_api"), Now: service.now()})
	if err != nil {
		return TriggerView{}, err
	}
	return service.viewFromScheduleRoute(ctx, ownerUserID, route, updated, true)
}

func (service *Service) resumeScheduleDefinitionForOwner(ctx context.Context, ownerUserID string, workspaceID string, route scheduleRoute) (TriggerView, error) {
	owner, err := service.loadManageOwner(ctx, workspaceID, ownerUserID)
	if err != nil {
		return TriggerView{}, err
	}
	definition, err := service.loadScheduleDefinitionForOwner(ctx, workspaceID, owner.MemberID, route.TargetID)
	if err != nil {
		return TriggerView{}, err
	}
	record := definitionRecordFromScheduleDefinition(ownerUserID, definition)
	if record.Status == StatusCompleted {
		return TriggerView{}, badRequest("trigger_completed")
	}
	if err := validateResumeStatus(record); err != nil {
		return TriggerView{}, err
	}
	nextOccurrenceAt, err := previewNextOccurrence(record, service.now())
	if err != nil {
		return TriggerView{}, err
	}
	if _, validationErr := service.validateDefinitionForOwner(ctx, ownerUserID, activePreviewRecord(record, nextOccurrenceAt)); validationErr != nil {
		return TriggerView{}, validationErr
	}
	storedNext := buildStoredNextOccurrence(StatusActive, record.SourceKind, record.TimeMode, record.ScheduledAt, nextOccurrenceAt)
	if err := service.activateScheduleDefinition(ctx, workspaceID, definition.ID, storedNext, service.now()); err != nil {
		return TriggerView{}, err
	}
	updated, err := service.scheduleStore().LoadDefinition(ctx, workspaceID, definition.ID)
	if err != nil {
		return TriggerView{}, err
	}
	return service.viewFromScheduleRoute(ctx, ownerUserID, route, updated, true)
}

func (service *Service) activateScheduleDefinition(ctx context.Context, workspaceID string, definitionID string, nextOccurrenceAt *time.Time, now time.Time) error {
	result, err := service.db.Exec(ctx, `
UPDATE schedule_definitions
SET status = $3,
    next_occurrence_at = $4,
    last_failure_code = '',
    last_failure_detail = '',
    paused_at = NULL,
    paused_reason = '',
    updated_at = $5
WHERE workspace_id = $1
  AND id = $2
  AND deleted_at IS NULL
`, strings.TrimSpace(workspaceID), strings.TrimSpace(definitionID), schedule.DefinitionStatusActive, nextOccurrenceAt, now.UTC())
	if err != nil {
		return fmt.Errorf("activate schedule definition: %w", err)
	}
	if result.RowsAffected() == 0 {
		return notFound("trigger not found")
	}
	return nil
}

func (service *Service) deleteScheduleDefinitionForOwner(ctx context.Context, ownerUserID string, workspaceID string, route scheduleRoute) error {
	owner, err := service.loadManageOwner(ctx, workspaceID, ownerUserID)
	if err != nil {
		return err
	}
	if _, err := service.loadScheduleDefinitionForOwner(ctx, workspaceID, owner.MemberID, route.TargetID); err != nil {
		return err
	}
	_, err = service.scheduleStore().DeleteDefinition(ctx, workspaceID, route.TargetID, service.now())
	return err
}

func (service *Service) listScheduleDefinitionsForOwner(ctx context.Context, ownerUserID string, workspaceID string, limit int) ([]TriggerView, error) {
	owner, err := service.loadManageOwner(ctx, workspaceID, ownerUserID)
	if err != nil {
		return nil, err
	}
	definitions, err := service.listScheduleDefinitionsForTriggerList(ctx, workspaceID, owner.MemberID, limit)
	if err != nil {
		return nil, err
	}
	views := make([]TriggerView, 0, len(definitions))
	for _, definition := range definitions {
		if strings.TrimSpace(definition.SourceKind) != schedule.SourceKindTime {
			continue
		}
		view, err := service.viewFromScheduleDefinition(ctx, ownerUserID, definition, false)
		if err != nil {
			return nil, err
		}
		views = append(views, view)
	}
	return views, nil
}

func (service *Service) listScheduleDefinitionsForTriggerList(ctx context.Context, workspaceID string, ownerMemberID string, limit int) ([]schedule.Definition, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	ownerMemberID = strings.TrimSpace(ownerMemberID)
	if workspaceID == "" || ownerMemberID == "" {
		return nil, badRequest("workspace and owner member are required")
	}
	rows, err := service.db.Query(ctx, `
SELECT schedule_definitions.id
FROM schedule_definitions
WHERE schedule_definitions.workspace_id = $1
  AND schedule_definitions.owner_member_id = $2
  AND schedule_definitions.source_kind = 'time'
  AND schedule_definitions.deleted_at IS NULL
ORDER BY schedule_definitions.created_at DESC, schedule_definitions.id DESC
LIMIT $3
`, workspaceID, ownerMemberID, limit)
	if err != nil {
		return nil, fmt.Errorf("list schedule definitions for trigger list: %w", err)
	}
	defer rows.Close()
	definitionIDs := make([]string, 0, limit)
	for rows.Next() {
		var definitionID string
		if err := rows.Scan(&definitionID); err != nil {
			return nil, fmt.Errorf("scan trigger list schedule definition id: %w", err)
		}
		if definitionID = strings.TrimSpace(definitionID); definitionID != "" {
			definitionIDs = append(definitionIDs, definitionID)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate trigger list schedule definition ids: %w", err)
	}

	scheduleStore := service.scheduleStore()
	definitions := make([]schedule.Definition, 0, len(definitionIDs))
	for _, definitionID := range definitionIDs {
		definition, err := scheduleStore.LoadDefinition(ctx, workspaceID, definitionID)
		if err != nil {
			return nil, err
		}
		definitions = append(definitions, definition)
	}
	return definitions, nil
}

func (service *Service) listAutomationRunsForSchedulePage(ctx context.Context, ownerUserID string, workspaceID string, route scheduleRoute, options RunListOptions) (RunPage, error) {
	owner, err := service.loadManageOwner(ctx, workspaceID, ownerUserID)
	if err != nil {
		return RunPage{}, err
	}
	definition, err := service.loadScheduleDefinitionForOwner(ctx, workspaceID, owner.MemberID, route.TargetID)
	if err != nil {
		return RunPage{}, err
	}
	page, err := service.scheduleStore().ListAutomationRunPage(ctx, schedule.AutomationRunListQuery{
		WorkspaceID: workspaceID, AutomationKind: schedule.AutomationKindSchedule, AutomationID: definition.ID,
		Limit: options.Limit, CursorAttemptedAt: options.CursorAttempted, CursorID: options.CursorID,
	})
	if err != nil {
		return RunPage{}, err
	}
	views := make([]RunView, 0, len(page.Runs))
	for _, run := range page.Runs {
		target, err := service.resolveTargetView(ctx, run.WorkspaceID, TargetKind(run.TargetKind), run.TargetRef, false)
		if err != nil {
			return RunPage{}, err
		}
		// The public attemptedAt field doubles as the effective pagination key:
		// unattempted rows use created_at so API cursors match the store ORDER BY.
		attemptedAt := run.CreatedAt
		if run.AttemptedAt != nil {
			attemptedAt = *run.AttemptedAt
		}
		views = append(views, RunView{
			ID:                     run.ID,
			ScheduleID:             definition.ID,
			TriggerID:              nonEmpty(route.InputID, definition.ID),
			WorkspaceID:            run.WorkspaceID,
			IdempotencyKey:         run.IdempotencyKey,
			SourceOccurrenceRef:    run.SourceOccurrenceRef,
			SourceKind:             SourceKind(run.SourceKind),
			TimeMode:               TimeMode(run.SourceType),
			Status:                 RunStatus(run.Status),
			ScheduledFor:           run.ScheduledFor,
			OccurredAt:             run.OccurredAt,
			AttemptedAt:            attemptedAt,
			CompletedAt:            run.CompletedAt,
			MessageID:              run.MessageID,
			EffectKind:             run.EffectKind,
			EffectRef:              run.EffectRef,
			ErrorCode:              run.ErrorCode,
			ErrorDetail:            run.ErrorDetail,
			RenderedContentPreview: run.RenderedContentPreview,
			AttemptCount:           run.AttemptCount,
			Target:                 target,
			ScheduleName:           run.AutomationNameSnapshot,
			TriggerName:            run.AutomationNameSnapshot,
		})
	}
	return RunPage{Runs: views, HasMore: page.HasMore}, nil
}

func (service *Service) listAutomationRunsForSchedule(ctx context.Context, ownerUserID string, workspaceID string, route scheduleRoute, limit int) ([]RunView, error) {
	page, err := service.listAutomationRunsForSchedulePage(ctx, ownerUserID, workspaceID, route, RunListOptions{Limit: limit})
	if err != nil {
		return nil, err
	}
	return page.Runs, nil
}

func nonEmpty(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}
