package reposync

import (
	"context"
	"fmt"
)

const (
	PlannerModeBootstrap    = "bulk_bootstrap"
	PlannerModeCurrentState = "current_state_diff"
	PlannerModeCheckpoint   = "checkpoint_diff"
)

// InventorySource returns a complete current inventory and a stable digest.
// Implementations may page or stream internally, but missing pages must return
// an error rather than an incomplete inventory that could imply deletion.
type InventorySource interface {
	Inventory(ctx context.Context) ([]InventoryObject, string, error)
}

// BatchSink applies one compatible physical batch and verifies each logical
// object independently. GitHub implementations may use bounded parallel API
// calls when the provider has no native bulk endpoint.
type BatchSink interface {
	ApplyBatch(ctx context.Context, batch PhysicalBatch) error
	Verify(ctx context.Context, item PlannedItem) error
}

type EngineOptions struct {
	DryRun      bool
	BatchLimits BatchLimits
}

type EngineResult struct {
	PlannerMode           string
	SourceInventoryDigest string
	TargetInventoryDigest string
	Items                 []PlannedItem
	Batches               []PhysicalBatch
}

// Execute inventories the target first so an empty repository can take the
// importer-compatible bootstrap path. Both paths share the same mapper output,
// logical RunItems, physical batcher, writer, and per-object verification.
func Execute(ctx context.Context, source, target InventorySource, sink BatchSink, links []LinkView, options EngineOptions) (EngineResult, error) {
	var targetObjects []InventoryObject
	var targetDigest string
	targetEmpty := false
	if checker, ok := target.(EmptyChecker); ok {
		var err error
		targetEmpty, err = checker.IsEmpty(ctx)
		if err != nil {
			return EngineResult{}, fmt.Errorf("check target emptiness: %w", err)
		}
	}
	if targetEmpty {
		targetDigest = EmptyInventoryDigest()
	} else {
		var err error
		targetObjects, targetDigest, err = target.Inventory(ctx)
		if err != nil {
			return EngineResult{}, fmt.Errorf("inventory target: %w", err)
		}
		targetEmpty = len(targetObjects) == 0
	}
	sourceObjects, sourceDigest, err := source.Inventory(ctx)
	if err != nil {
		return EngineResult{}, fmt.Errorf("inventory source: %w", err)
	}

	result := EngineResult{SourceInventoryDigest: sourceDigest, TargetInventoryDigest: targetDigest}
	if targetEmpty {
		result.PlannerMode = PlannerModeBootstrap
		result.Items, err = PlanBootstrap(sourceObjects, targetObjects)
	} else {
		result.PlannerMode = PlannerModeCurrentState
		result.Items, err = PlanCurrentState(sourceObjects, targetObjects, links)
	}
	if err != nil {
		return EngineResult{}, err
	}
	result.Batches, err = BuildPhysicalBatches(result.Items, options.BatchLimits)
	if err != nil {
		return EngineResult{}, err
	}
	if options.DryRun {
		return result, nil
	}
	if sink == nil {
		return EngineResult{}, fmt.Errorf("sync apply requires a sink")
	}
	for _, batch := range result.Batches {
		if err := sink.ApplyBatch(ctx, batch); err != nil {
			return EngineResult{}, fmt.Errorf("apply batch %s: %w", batch.Key, err)
		}
		for _, item := range batch.Items {
			if err := sink.Verify(ctx, item); err != nil {
				return EngineResult{}, fmt.Errorf("verify %s %s: %w", item.ObjectType, item.ObjectKey, err)
			}
		}
	}
	return result, nil
}
