package operator

const finalizer = "net.10xdev.io/deprogram"

// Reconciler is the level-triggered controller. Reconcile(name) derives
// the correct state from CURRENT state every call — it is idempotent and
// self-healing, and survives missed events / restarts.
type Reconciler struct {
	Store *Store
	DP    DataPlane
}

// Reconcile converges the world toward the desired state of one EdgeRoute.
func (r *Reconciler) Reconcile(name string) error {
	o, ok := r.Store.Get(name)
	if !ok {
		// Deleted entirely: re-derive the data plane from what remains.
		return r.reprogram()
	}

	// Deletion path: run finalizer cleanup, then drop the finalizer so the
	// object can actually be removed.
	if o.DeletionTimestamp {
		if o.hasFinalizer(finalizer) {
			// Deprogram BEFORE the object disappears (it's already excluded
			// from ListLive because DeletionTimestamp is set), so traffic
			// stops flowing to a route that's going away.
			if err := r.reprogram(); err != nil {
				return err // retry cleanup; object stays in Terminating
			}
			o.removeFinalizer(finalizer)
			r.Store.Update(o) // no finalizers + deletion -> object removed
		}
		return nil
	}

	// Ensure the finalizer is present on live objects.
	if !o.hasFinalizer(finalizer) {
		o.Finalizers = append(o.Finalizers, finalizer)
		r.Store.Update(o)
	}

	// Observe actual state: does the target Service exist?
	if !r.Store.HasService(o.Spec.Service) {
		setCond(o, "Accepted", false, "ServiceNotFound")
		setCond(o, "Programmed", false, "ServiceNotFound")
		r.Store.Update(o)
		return nil
	}

	// Converge the data plane (idempotent; rebuilds from all live routes).
	if err := r.reprogram(); err != nil {
		setCond(o, "Programmed", false, "PushFailed") // the CR analog of an xDS NACK
		r.Store.Update(o)
		return err
	}

	// Report success — Programmed=true only AFTER the push succeeded.
	setCond(o, "Accepted", true, "Valid")
	setCond(o, "Programmed", true, "Synced")
	r.Store.Update(o)
	return nil
}

// reprogram rebuilds the data-plane config from ALL live EdgeRoutes.
// Rebuilding the whole set (not mutating incrementally) keeps reconcile
// idempotent and correct after missed events.
func (r *Reconciler) reprogram() error {
	live := r.Store.ListLive()
	specs := make([]EdgeRouteSpec, 0, len(live))
	for _, o := range live {
		specs = append(specs, o.Spec)
	}
	return r.DP.Program(specs)
}

func setCond(o *EdgeRoute, typ string, status bool, reason string) {
	if o.Status.Conditions == nil {
		o.Status.Conditions = map[string]Condition{}
	}
	o.Status.Conditions[typ] = Condition{Status: status, Reason: reason}
}

// Condition reads a status condition from the store (for tests/CLI).
func (r *Reconciler) Condition(name, typ string) (Condition, bool) {
	o, ok := r.Store.Get(name)
	if !ok {
		return Condition{}, false
	}
	c, ok := o.Status.Conditions[typ]
	return c, ok
}
