package manage

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

const maxTargetRefBytes = 255

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

func (service *Service) UpdateForOwner(ctx context.Context, ownerUserID string, workspaceID string, triggerID string, input UpsertInput) (TriggerView, error) {
	route, err := service.resolveScheduleRoute(ctx, ownerUserID, workspaceID, triggerID)
	if err != nil {
		return TriggerView{}, err
	}
	return service.updateScheduleDefinitionForOwner(ctx, ownerUserID, workspaceID, route, input)
}

func (service *Service) PauseForOwner(ctx context.Context, ownerUserID string, workspaceID string, triggerID string) (TriggerView, error) {
	route, err := service.resolveScheduleRoute(ctx, ownerUserID, workspaceID, triggerID)
	if err != nil {
		return TriggerView{}, err
	}
	return service.pauseScheduleDefinitionForOwner(ctx, ownerUserID, workspaceID, route)
}

func (service *Service) ResumeForOwner(ctx context.Context, ownerUserID string, workspaceID string, triggerID string) (TriggerView, error) {
	route, err := service.resolveScheduleRoute(ctx, ownerUserID, workspaceID, triggerID)
	if err != nil {
		return TriggerView{}, err
	}
	return service.resumeScheduleDefinitionForOwner(ctx, ownerUserID, workspaceID, route)
}

func validateResumeStatus(record definitionRecord) error {
	if record.Status == StatusPaused {
		return nil
	}
	if record.Status == StatusBlocked {
		if strings.TrimSpace(record.LastFailureCode) == string(FailureCodeProviderConnectionDown) {
			return badRequest("trigger is blocked")
		}
		return nil
	}
	return badRequest("only paused or blocked triggers can be resumed")
}

func (service *Service) DeleteForOwner(ctx context.Context, ownerUserID string, workspaceID string, triggerID string) error {
	route, err := service.resolveScheduleRoute(ctx, ownerUserID, workspaceID, triggerID)
	if err != nil {
		return err
	}
	return service.deleteScheduleDefinitionForOwner(ctx, ownerUserID, workspaceID, route)
}

func buildStoredNextOccurrence(status DefinitionStatus, sourceKind SourceKind, timeMode TimeMode, scheduledAt *time.Time, previewNextAt *time.Time) *time.Time {
	if status != StatusActive {
		return nil
	}
	if sourceKind != SourceKindTime {
		return nil
	}
	if timeMode.IsOneShot() {
		return scheduledAt
	}
	return previewNextAt
}

func materializeRelativeScheduledAt(ctx context.Context, queryer coredb.QueryRower, record *definitionRecord) error {
	if record == nil || record.TimeMode != TimeModeOneShotRelative {
		return nil
	}
	if record.RelativeSeconds == nil || *record.RelativeSeconds <= 0 {
		return badRequest("source.relativeSeconds must be positive")
	}

	query := `SELECT clock_timestamp() + ($1 * interval '1 second')`
	if coredb.IsTiDB(queryer) {
		query = `SELECT CAST(TIMESTAMPADD(SECOND, $1, UTC_TIMESTAMP(6)) AS DATETIME(6))`
	}

	var scheduledAt time.Time
	if err := queryer.QueryRow(ctx, query, *record.RelativeSeconds).Scan(&scheduledAt); err != nil {
		return fmt.Errorf("materialize relative trigger scheduledAt: %w", err)
	}
	scheduledAt = scheduledAt.UTC()
	record.ScheduledAt = &scheduledAt
	return nil
}

func activePreviewRecord(record definitionRecord, nextOccurrenceAt *time.Time) definitionRecord {
	record.Status = StatusActive
	record.NextOccurrenceAt = buildStoredNextOccurrence(StatusActive, record.SourceKind, record.TimeMode, record.ScheduledAt, nextOccurrenceAt)
	return record
}

func previewNextOccurrence(record definitionRecord, now time.Time) (*time.Time, error) {
	if record.SourceKind == SourceKindEvent {
		return nil, nil
	}
	switch record.TimeMode {
	case TimeModeOneShotAbsolute, TimeModeOneShotRelative:
		if record.ScheduledAt == nil {
			return nil, badRequest("one-shot trigger is missing scheduledAt")
		}
		if !record.ScheduledAt.After(now) {
			return nil, badRequest("one-shot trigger has no future occurrence; update the schedule instead")
		}
		return nil, nil
	case TimeModeInterval, TimeModeCron:
		return nextOccurrenceAfter(record, now)
	default:
		return nil, badRequest("unsupported timeMode")
	}
}

func (service *Service) normalizeMutation(ctx context.Context, ownerUserID string, workspaceID string, input UpsertInput, existing *definitionRecord, committedAt time.Time, materializeRelative bool) (normalizedMutation, error) {
	name := strings.TrimSpace(input.Name)
	if name == "" {
		return normalizedMutation{}, badRequest("name is required")
	}

	sourceKind := SourceKind(strings.TrimSpace(string(input.Source.SourceKind)))
	if sourceKind == "" {
		return normalizedMutation{}, badRequest("source.sourceKind is required")
	}
	if sourceKind != SourceKindTime && sourceKind != SourceKindEvent {
		return normalizedMutation{}, badRequest("unsupported sourceKind")
	}

	record := definitionRecord{
		WorkspaceID: strings.TrimSpace(workspaceID),
		OwnerUserID: strings.TrimSpace(ownerUserID),
		Name:        name,
		SourceKind:  sourceKind,
	}
	var target TargetView
	messageTemplate := strings.TrimSpace(input.MessageTemplate)
	if messageTemplate == "" {
		return normalizedMutation{}, badRequest("messageTemplate is required")
	}
	var err error
	target, err = service.resolveTargetView(ctx, workspaceID, input.Target.Kind, input.Target.Ref, true)
	if err != nil {
		return normalizedMutation{}, err
	}
	record.TargetKind = target.Kind
	record.TargetRef = target.Ref
	record.MessageTemplate = messageTemplate
	record.MentionHints = normalizeScheduleMentionHints(input.MentionHints, messageTemplate)
	if existing != nil && !input.MentionHintsSet {
		record.MentionHints = normalizeScheduleMentionHints(existing.MentionHints, messageTemplate)
	}
	if input.AlsoStartThread && target.Kind != TargetKindChannel {
		return normalizedMutation{}, badRequest("alsoStartThread only supports channel targets")
	}
	record.AlsoStartThread = input.AlsoStartThread && target.Kind == TargetKindChannel
	proposedNextAt, estimatedAt, err := service.applySourceInput(&record, input.Source, existing, committedAt, materializeRelative)
	if err != nil {
		return normalizedMutation{}, err
	}
	humanNotificationMode, err := normalizeHumanNotificationMode(input.HumanNotificationMode, record, existing)
	if err != nil {
		return normalizedMutation{}, err
	}
	record.HumanNotificationMode = humanNotificationMode
	validation, validationErr := service.validateDefinitionForOwner(ctx, ownerUserID, activePreviewRecord(record, proposedNextAt))
	return normalizedMutation{
		record:               record,
		target:               target,
		validation:           validation,
		validationErr:        validationErr,
		estimatedScheduledAt: estimatedAt,
		proposedNextAt:       proposedNextAt,
	}, nil
}

func normalizeScheduleMentionHints(hints []message.MessageMentionHint, content string) []message.MessageMentionHint {
	hints = message.MentionHintsWithResolvedLinks(hints, message.ParseResolvedMentionLinks(content))
	if len(hints) == 0 {
		return nil
	}
	_, tokenSet := normalizedScheduleMentionTokens(content)
	if len(tokenSet) == 0 {
		return nil
	}
	normalized := make([]message.MessageMentionHint, 0, min(len(hints), message.MaxPersistedMentionHints))
	seen := make(map[string]struct{}, len(hints))
	for _, hint := range hints {
		mentionType := strings.ToLower(strings.TrimSpace(hint.Type))
		id := strings.TrimSpace(hint.ID)
		token := strings.TrimSpace(hint.Token)
		if mentionType == "" || id == "" || token == "" {
			continue
		}
		if !scheduleMentionTokenMatchesAny(strings.ToLower(token), tokenSet) {
			continue
		}
		key := mentionType + "\x00" + id + "\x00" + strings.ToLower(token)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		normalized = append(normalized, message.MessageMentionHint{
			Type:     mentionType,
			ID:       id,
			Token:    token,
			Metadata: hint.Metadata,
		})
		if len(normalized) >= message.MaxPersistedMentionHints {
			break
		}
	}
	return normalized
}

func normalizedScheduleMentionTokens(content string) ([]string, map[string]struct{}) {
	rawTokens := message.ParseDirectMentionTokens(content)
	tokens := make([]string, 0, len(rawTokens))
	tokenSet := make(map[string]struct{}, len(rawTokens))
	for _, token := range rawTokens {
		normalized := strings.ToLower(strings.TrimSpace(token))
		if normalized == "" {
			continue
		}
		if _, ok := tokenSet[normalized]; ok {
			continue
		}
		tokenSet[normalized] = struct{}{}
		tokens = append(tokens, normalized)
	}
	return tokens, tokenSet
}

func scheduleMentionTokenMatchesAny(token string, allowed map[string]struct{}) bool {
	token = strings.ToLower(strings.TrimSpace(token))
	if token == "" {
		return false
	}
	for allowedToken := range allowed {
		if token == allowedToken || strings.HasPrefix(allowedToken, token) {
			return true
		}
	}
	return false
}

func normalizeHumanNotificationMode(input HumanNotificationMode, _ definitionRecord, existing *definitionRecord) (HumanNotificationMode, error) {
	raw := strings.TrimSpace(strings.ToLower(string(input)))
	if raw == "" {
		mode := HumanNotificationModeNone
		if existing != nil {
			mode = normalizedHumanNotificationMode(existing.HumanNotificationMode)
		}
		return mode, nil
	}

	mode := HumanNotificationMode(raw)
	switch mode {
	case HumanNotificationModeNone:
		return HumanNotificationModeNone, nil
	case HumanNotificationMode("surface"):
		return "", badRequest("GitHub/Gitea provider watches must be managed through the app page or authorized provider app MCP")
	default:
		return "", badRequest("humanNotificationMode must be `none`")
	}
}

func normalizedHumanNotificationMode(_ HumanNotificationMode) HumanNotificationMode {
	return HumanNotificationModeNone
}

func (service *Service) applySourceInput(record *definitionRecord, source SourceInput, existing *definitionRecord, committedAt time.Time, materializeRelative bool) (*time.Time, *time.Time, error) {
	if record.SourceKind == SourceKindEvent {
		return nil, nil, badRequest("GitHub/Gitea provider watches must be managed through the app page or authorized provider app MCP")
	}

	timeMode := TimeMode(strings.TrimSpace(string(source.TimeMode)))
	if timeMode == "" {
		return nil, nil, badRequest("source.timeMode is required")
	}
	if existing != nil && existing.TimeMode != "" && existing.TimeMode.IsOneShot() != timeMode.IsOneShot() {
		return nil, nil, badRequest("cannot switch a trigger between one-shot and recurring")
	}
	record.TimeMode = timeMode
	record.ScheduledAt = nil
	record.RelativeSeconds = nil
	record.AnchorAt = nil
	record.IntervalSeconds = nil
	record.CronExpression = ""
	record.Timezone = ""
	record.EventSubscription = ""

	switch timeMode {
	case TimeModeOneShotAbsolute:
		if source.ScheduledAt == nil {
			return nil, nil, badRequest("source.scheduledAt is required")
		}
		scheduledAt := source.ScheduledAt.UTC()
		if !scheduledAt.After(committedAt) {
			return nil, nil, badRequest("source.scheduledAt must be in the future")
		}
		record.ScheduledAt = &scheduledAt
		return nil, nil, nil
	case TimeModeOneShotRelative:
		if source.RelativeSeconds == nil || *source.RelativeSeconds <= 0 {
			return nil, nil, badRequest("source.relativeSeconds must be positive")
		}
		record.RelativeSeconds = source.RelativeSeconds
		estimated := committedAt.UTC().Add(time.Duration(*source.RelativeSeconds) * time.Second)
		if materializeRelative {
			record.ScheduledAt = &estimated
		}
		return nil, &estimated, nil
	case TimeModeInterval:
		if source.AnchorAt == nil {
			return nil, nil, badRequest("source.anchorAt is required")
		}
		if source.IntervalSeconds == nil || *source.IntervalSeconds <= 0 {
			return nil, nil, badRequest("source.intervalSeconds must be positive")
		}
		anchorAt := source.AnchorAt.UTC()
		record.AnchorAt = &anchorAt
		record.IntervalSeconds = source.IntervalSeconds
		nextOccurrenceAt, err := nextOccurrenceAfter(*record, committedAt)
		if err != nil {
			return nil, nil, err
		}
		return nextOccurrenceAt, nil, nil
	case TimeModeCron:
		cronExpression := strings.TrimSpace(source.CronExpression)
		timezone := strings.TrimSpace(source.Timezone)
		if cronExpression == "" {
			return nil, nil, badRequest("source.cronExpression is required")
		}
		if timezone == "" {
			return nil, nil, badRequest("source.timezone is required")
		}
		record.CronExpression = cronExpression
		record.Timezone = timezone
		nextOccurrenceAt, err := nextOccurrenceAfter(*record, committedAt)
		if err != nil {
			return nil, nil, err
		}
		return nextOccurrenceAt, nil, nil
	default:
		return nil, nil, badRequest("unsupported source.timeMode")
	}
}
