package calendar

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

	calendarstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/calendar"
	"github.com/tidbcloud/AgentTeam/internal/workflow/binding"
	"github.com/tidbcloud/AgentTeam/internal/workflow/invocation"
)

const (
	MaxWindow                    = 31 * 24 * time.Hour
	MaxItems                     = 5000
	initialProjectedItemCapacity = 16
	calendarInstant              = time.Microsecond
)

type Repository interface {
	VisitSnapshot(context.Context, calendarstore.ListSnapshotParams, calendarstore.SnapshotVisitor) error
}

type Service struct{ repository Repository }

func New(repository Repository) *Service { return &Service{repository: repository} }

func (service *Service) List(ctx context.Context, input ListInput) (Result, error) {
	if service == nil || service.repository == nil {
		return Result{}, fmt.Errorf("Workflow calendar repository is not configured")
	}
	input.WorkspaceID = strings.TrimSpace(input.WorkspaceID)
	input.From = input.From.UTC().Truncate(calendarInstant)
	input.To = input.To.UTC().Truncate(calendarInstant)
	if input.WorkspaceID == "" {
		return Result{}, invalidWindow("workspaceId is required")
	}
	if input.From.IsZero() || input.To.IsZero() || !input.From.Before(input.To) {
		return Result{}, invalidWindow("from must be before to")
	}
	if input.To.Sub(input.From) > MaxWindow {
		return Result{}, invalidWindow("Workflow calendar window must not exceed 31 days")
	}

	var items []Item
	var acceptedOccurrences map[string]struct{}
	err := service.repository.VisitSnapshot(ctx, calendarstore.ListSnapshotParams{
		WorkspaceID: input.WorkspaceID, From: input.From, To: input.To,
		RunLimit: MaxItems + 1,
	}, calendarstore.SnapshotVisitor{
		Runs: func(runs []calendarstore.Run) error {
			if len(runs) > MaxItems {
				return windowTooDense("Workflow calendar window contains too many items; select a smaller time range")
			}
			itemCapacity := len(runs) + initialProjectedItemCapacity
			if itemCapacity > MaxItems {
				itemCapacity = MaxItems
			}
			items = make([]Item, 0, itemCapacity)
			acceptedOccurrences = make(map[string]struct{}, len(runs))
			for _, value := range runs {
				items = append(items, runItem(value))
				if value.SourceKind == invocation.SourceKindSchedule && strings.TrimSpace(value.OccurrenceKey) != "" {
					acceptedOccurrences[value.OccurrenceKey] = struct{}{}
				}
			}
			return nil
		},
		Source: func(source calendarstore.ScheduleSource) error {
			projected, err := projectSource(source, input.From, input.To, acceptedOccurrences, MaxItems-len(items))
			if err != nil {
				return err
			}
			items = append(items, projected...)
			if len(items) > MaxItems {
				return windowTooDense("Workflow calendar window contains too many items; select a smaller time range")
			}
			return nil
		},
	})
	if err != nil {
		return Result{}, err
	}

	sort.Slice(items, func(i, j int) bool {
		if items[i].OccursAt.Equal(items[j].OccursAt) {
			return items[i].stableID < items[j].stableID
		}
		return items[i].OccursAt.Before(items[j].OccursAt)
	})
	if items == nil {
		items = []Item{}
	}
	return Result{From: input.From, To: input.To, Items: items}, nil
}

func runItem(value calendarstore.Run) Item {
	displayName := strings.TrimSpace(value.PlanDisplayName)
	if displayName == "" {
		displayName = strings.TrimSpace(value.WorkflowName)
	}
	item := Item{
		Kind: ItemKindRun, OccursAt: value.OccursAt, stableID: value.InvocationID,
		Workflow: Workflow{
			PlanID: value.PlanID, PlanVersionID: value.PlanVersionID,
			Key: value.PlanKey, DisplayName: displayName,
		},
		Run: &Run{
			ID: value.RunID, InvocationID: value.InvocationID, Status: value.Status,
			QueuedAt: value.QueuedAt, StartedAt: value.StartedAt,
			CompletedAt: value.CompletedAt, CanceledAt: value.CanceledAt,
		},
	}
	if value.SourceKind == invocation.SourceKindSchedule {
		item.Schedule = &Schedule{
			BindingID: value.BindingID, BindingRevisionID: value.BindingRevisionID,
			BindingSourceID: value.BindingSourceID, BindingKey: value.BindingKey,
			BindingDisplayName: value.BindingDisplayName, OccurrenceKey: value.OccurrenceKey,
		}
	}
	return item
}

func projectSource(
	source calendarstore.ScheduleSource,
	from time.Time,
	to time.Time,
	accepted map[string]struct{},
	remaining int,
) ([]Item, error) {
	var scheduleSource binding.Source
	if err := json.Unmarshal(source.SourceJSON, &scheduleSource); err != nil {
		return nil, fmt.Errorf("decode active Workflow Schedule %s: %w", source.ID, err)
	}
	if scheduleSource.Type != binding.SourceTypeSchedule || scheduleSource.Schedule == nil {
		return nil, fmt.Errorf("active Workflow Schedule %s has invalid source configuration", source.ID)
	}
	schedule := *scheduleSource.Schedule
	candidate := source.NextOccurrenceAt.UTC().Truncate(calendarInstant)
	if candidate.Before(from) {
		switch schedule.Mode {
		case binding.ScheduleModeCron, binding.ScheduleModeInterval:
			next, err := binding.NextOccurrenceAfter(schedule, from.Add(-calendarInstant))
			if err != nil {
				return nil, fmt.Errorf("project active Workflow Schedule %s: %w", source.ID, err)
			}
			if next == nil {
				return nil, nil
			}
			candidate = next.UTC().Truncate(calendarInstant)
		case binding.ScheduleModeAbsolute, binding.ScheduleModeRelative:
			return nil, nil
		default:
			return nil, fmt.Errorf("active Workflow Schedule %s has unsupported mode %q", source.ID, schedule.Mode)
		}
	}

	items := make([]Item, 0)
	for candidate.Before(to) {
		occurrenceKey := invocation.ScheduleOccurrenceRef(source.ID, candidate)
		if _, exists := accepted[occurrenceKey]; !exists {
			if len(items) >= remaining {
				return nil, windowTooDense("Workflow calendar window contains too many items; select a smaller time range")
			}
			items = append(items, Item{
				Kind: ItemKindScheduled, OccursAt: candidate, stableID: occurrenceKey,
				Workflow: Workflow{
					PlanID: source.PlanID, PlanVersionID: source.PlanVersionID,
					Key: source.PlanKey, DisplayName: source.PlanDisplayName,
				},
				Schedule: &Schedule{
					BindingID: source.BindingID, BindingRevisionID: source.BindingRevisionID,
					BindingSourceID: source.ID, BindingKey: source.BindingKey,
					BindingDisplayName: source.BindingDisplayName, OccurrenceKey: occurrenceKey,
				},
			})
		}
		next, err := binding.NextOccurrenceAfter(schedule, candidate)
		if err != nil {
			return nil, fmt.Errorf("project active Workflow Schedule %s: %w", source.ID, err)
		}
		if next == nil {
			break
		}
		normalized := next.UTC().Truncate(calendarInstant)
		if !normalized.After(candidate) {
			return nil, fmt.Errorf("active Workflow Schedule %s did not advance its occurrence", source.ID)
		}
		candidate = normalized
	}
	return items, nil
}
