// Copyright 2025 Emiliano Spinella (eminwux)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package runner

import (
	"fmt"

	"github.com/eminwux/kukeon/internal/errdefs"
	intmodel "github.com/eminwux/kukeon/internal/modelhub"
)

// UpdateCell updates an existing cell with new metadata and container changes.
// It handles:
// - Metadata updates (labels)
// - Container additions (containers in desired but not in actual)
// - Container updates (containers in both, with spec changes)
// - Container removals (orphans: containers in actual but not in desired)
//
// Breaking changes (root container spec changes, parent associations) should be rejected before calling this method.
func (r *Exec) UpdateCell(desired intmodel.Cell) (intmodel.Cell, error) {
	// Get existing cell
	existing, err := r.GetCell(desired)
	if err != nil {
		return intmodel.Cell{}, fmt.Errorf("%w: %w", errdefs.ErrGetCell, err)
	}

	// Update compatible metadata fields. Preserve controller-managed
	// `*.kukeon.io` canonical labels (injected during create_cell.go) when
	// the user's desired doc omits them — same rule the diff layer applies
	// via `filterManagedLabels` (issue #437 / PR #454). Without this, a
	// genuine spec change that legitimately routes through UpdateCell
	// strips the canonical labels (issue #455).
	existing.Metadata.Labels = mergeManagedLabels(existing.Metadata.Labels, desired.Metadata.Labels)

	// Build maps of desired and actual containers by ID
	desiredContainers := make(map[string]*intmodel.ContainerSpec)
	actualContainers := make(map[string]*intmodel.ContainerSpec)

	for i := range desired.Spec.Containers {
		container := &desired.Spec.Containers[i]
		if container.ID != "" {
			desiredContainers[container.ID] = container
		}
	}

	for i := range existing.Spec.Containers {
		container := &existing.Spec.Containers[i]
		if container.ID != "" {
			actualContainers[container.ID] = container
		}
	}

	// Whether the desired side authored a root container. When the user YAML
	// omits one (docs/examples/hello-world.yaml is the canonical case), the
	// runner synthesized a root during create and the apply layer's diff
	// already treats it as implementation detail (see
	// `internal/controller/apply/diff.go`). UpdateCell must mirror that:
	// preserve the actual root container in place, do not treat it as an
	// orphan, and do not clear `Spec.RootContainerID` from the cell. Without
	// this, every `kuke apply -f` of an unchanged file (issue #437) errored
	// with "root container ... cannot be removed".
	desiredHasRoot := false
	for _, c := range desired.Spec.Containers {
		if c.Root {
			desiredHasRoot = true
			break
		}
	}
	var preservedRoot *intmodel.ContainerSpec
	if !desiredHasRoot {
		for i := range existing.Spec.Containers {
			if existing.Spec.Containers[i].Root {
				preservedRoot = &existing.Spec.Containers[i]
				break
			}
		}
	}

	// Handle orphan containers (in actual but not in desired)
	for id := range actualContainers {
		if _, exists := desiredContainers[id]; !exists {
			if actualContainers[id].Root && preservedRoot != nil {
				// Runner-synthesized root: the user YAML omitted it, so the
				// orphan check would otherwise refuse the update. Leave it
				// alone (handled by the newContainers prepend below).
				continue
			}
			// Root container cannot be removed from the cell
			if actualContainers[id].Root {
				return intmodel.Cell{}, fmt.Errorf(
					"root container %q cannot be removed from cell %q, delete the cell instead using 'kuke delete cell %s'",
					id,
					existing.Metadata.Name,
					existing.Metadata.Name,
				)
			}
			// Container should be removed
			if stopErr := r.StopContainer(existing, id); stopErr != nil {
				if isValidationError(stopErr) {
					return intmodel.Cell{}, fmt.Errorf(
						"validation error stopping container %q for removal: %w",
						id,
						stopErr,
					)
				}
				r.logger.WarnContext(
					r.ctx,
					"failed to stop container for removal, continuing",
					"cell", existing.Metadata.Name,
					"container", id,
					"error", stopErr,
				)
			}
			if deleteErr := r.DeleteContainer(existing, id); deleteErr != nil {
				if isValidationError(deleteErr) {
					return intmodel.Cell{}, fmt.Errorf(
						"validation error deleting container %q for removal: %w",
						id,
						deleteErr,
					)
				}
				r.logger.WarnContext(
					r.ctx,
					"failed to delete container for removal, continuing",
					"cell", existing.Metadata.Name,
					"container", id,
					"error", deleteErr,
				)
			}
		}
	}

	// Update existing cell's containers list to match desired
	// Preserve root container and existing containerd IDs where possible
	newContainers := make([]intmodel.ContainerSpec, 0, len(desired.Spec.Containers)+1)
	if preservedRoot != nil {
		// Keep the synthesized root in front of the user-authored containers
		// so RootContainerID still resolves and runner/refresh paths that
		// scan Containers for the root see it.
		newContainers = append(newContainers, *preservedRoot)
	}
	// recreatedIDs collects non-root container IDs whose image/command/args
	// changed under the breaking-spec branch below. ensureCellContainers
	// recreates the containerd container with the new spec (pulling the new
	// image), but does not start it; the start happens further down via
	// r.StartContainer once metadata has been persisted, so a successful
	// apply leaves the recreated child running and the cell Ready. Issue
	// #485.
	var recreatedIDs []string
	for _, desiredContainer := range desired.Spec.Containers {
		if actualContainer, exists := actualContainers[desiredContainer.ID]; exists {
			// Container exists, preserve containerd ID but update spec
			desiredContainer.ContainerdID = actualContainer.ContainerdID
			// Check if container spec changed (image, command, args) - if so, recreate
			if containerSpecChanged(&desiredContainer, actualContainer) {
				// Root container cannot be deleted for updates
				if actualContainer.Root {
					return intmodel.Cell{}, fmt.Errorf(
						"root container %q cannot be deleted for update in cell %q, recreate the cell instead",
						desiredContainer.ID,
						existing.Metadata.Name,
					)
				}
				// Stop and delete old container
				if stopErr := r.StopContainer(existing, desiredContainer.ID); stopErr != nil {
					if isValidationError(stopErr) {
						return intmodel.Cell{}, fmt.Errorf(
							"validation error stopping container %q for update: %w",
							desiredContainer.ID,
							stopErr,
						)
					}
					r.logger.WarnContext(
						r.ctx,
						"failed to stop container for update, continuing",
						"cell", existing.Metadata.Name,
						"container", desiredContainer.ID,
						"error", stopErr,
					)
				}
				if deleteErr := r.DeleteContainer(existing, desiredContainer.ID); deleteErr != nil {
					if isValidationError(deleteErr) {
						return intmodel.Cell{}, fmt.Errorf(
							"validation error deleting container %q for update: %w",
							desiredContainer.ID,
							deleteErr,
						)
					}
					r.logger.WarnContext(
						r.ctx,
						"failed to delete container for update, continuing",
						"cell", existing.Metadata.Name,
						"container", desiredContainer.ID,
						"error", deleteErr,
					)
				}
				// Clear containerd ID so it will be recreated
				desiredContainer.ContainerdID = ""
				recreatedIDs = append(recreatedIDs, desiredContainer.ID)
			}
		}
		// Ensure container has proper parent references
		if desiredContainer.RealmName == "" {
			desiredContainer.RealmName = existing.Spec.RealmName
		}
		if desiredContainer.SpaceName == "" {
			desiredContainer.SpaceName = existing.Spec.SpaceName
		}
		if desiredContainer.StackName == "" {
			desiredContainer.StackName = existing.Spec.StackName
		}
		if desiredContainer.CellName == "" {
			desiredContainer.CellName = existing.Spec.ID
			if desiredContainer.CellName == "" {
				desiredContainer.CellName = existing.Metadata.Name
			}
		}
		newContainers = append(newContainers, desiredContainer)
	}

	// Update cell with new containers list
	existing.Spec.Containers = newContainers
	if preservedRoot != nil && desired.Spec.RootContainerID == "" {
		// User YAML omitted the root container; preserve the existing
		// RootContainerID so subsequent runner lookups still resolve.
		// existing.Spec.RootContainerID is already set, so this is a no-op
		// in the happy path — the explicit guard exists so a future
		// refactor that clears existing.Spec.RootContainerID up-front does
		// not silently drop it.
	} else {
		existing.Spec.RootContainerID = desired.Spec.RootContainerID
	}

	// Ensure all containers exist (will create missing ones, update existing ones)
	if connectErr := r.ensureClientConnected(); connectErr != nil {
		return intmodel.Cell{}, fmt.Errorf("%w: %w", errdefs.ErrConnectContainerd, connectErr)
	}

	_, ensureErr := r.ensureCellContainers(&existing)
	if ensureErr != nil {
		return intmodel.Cell{}, fmt.Errorf("failed to ensure cell containers: %w", ensureErr)
	}

	// Update metadata
	if updateErr := r.UpdateCellMetadata(existing); updateErr != nil {
		return intmodel.Cell{}, fmt.Errorf("%w: %w", errdefs.ErrUpdateCellMetadata, updateErr)
	}

	// Start non-root containers that were recreated above. ensureCellContainers
	// pulled the new image and built a fresh containerd container, but did not
	// start it; without this loop a `kuke apply -f` that bumps a non-root
	// container's image leaves the recreated child in the Created state and
	// the reconciler drives the cell back out of Ready. r.StartContainer is
	// idempotent (delete + create + start) so the second create-call here is
	// harmless — the freshly-pulled snapshot is reused. Issue #485.
	for _, id := range recreatedIDs {
		started, startErr := r.StartContainer(existing, id)
		if startErr != nil {
			return intmodel.Cell{}, fmt.Errorf("failed to start recreated container %q: %w", id, startErr)
		}
		existing = started
	}

	return existing, nil
}

// containerSpecChanged reports whether a non-root container must be
// stop-removed and recreated for the change to take effect, rather than
// updated in place. The gate covers the OCI-baked fields whose values the
// runner fixes at container create and never re-resolves on the in-place
// task-restart path: image/command/args (snapshot + Process), workingDir
// (Process.Cwd), securityOpts (Process.NoNewPrivileges / Linux.Seccomp),
// volumes (OCI Mounts), and secrets (env-injected Process.Env via
// resolveSecrets, plus file-form Mounts). Without recreating, a secrets edit
// on a workload container never reaches the running OCI Process.Env — the
// defect issue #1154 fixes on the non-root side (the root side routes through
// RecreateCell via the Breaking-on-root diff classification).
//
// Other Breaking-on-root fields (privileged, user, readOnlyRootFilesystem,
// capabilities, tmpfs, resources) are not yet gated here; their non-root
// in-place gap predates #1154 and is tracked separately.
func containerSpecChanged(desired, actual *intmodel.ContainerSpec) bool {
	return desired.Image != actual.Image ||
		desired.Command != actual.Command ||
		!stringSlicesEqual(desired.Args, actual.Args) ||
		desired.WorkingDir != actual.WorkingDir ||
		!stringSlicesEqual(desired.SecurityOpts, actual.SecurityOpts) ||
		!volumeMountsEqual(desired.Volumes, actual.Volumes) ||
		!containerSecretsEqual(desired.Secrets, actual.Secrets)
}

// volumeMountsEqual reports whether two VolumeMount slices are equal in
// declaration order. VolumeMount is a flat struct of comparable fields, so
// `==` matches the diff layer's `volumeMountsEqual` semantics.
func volumeMountsEqual(a, b []intmodel.VolumeMount) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

// containerSecretsEqual reports whether two ContainerSecret slices carry
// the same references in declaration order, mirroring the diff layer's
// `secretsEqual` (reference-only, never the resolved value).
func containerSecretsEqual(a, b []intmodel.ContainerSecret) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i].Name != b[i].Name ||
			a[i].FromFile != b[i].FromFile ||
			a[i].FromEnv != b[i].FromEnv ||
			a[i].MountPath != b[i].MountPath ||
			!secretRefsEqual(a[i].SecretRef, b[i].SecretRef) {
			return false
		}
	}
	return true
}

func secretRefsEqual(a, b *intmodel.ContainerSecretRef) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return *a == *b
}

func stringSlicesEqual(a, b []string) bool {
	if len(a) != len(b) {
		return false // Different length means not equal
	}
	for i := range a {
		if a[i] != b[i] {
			return false // Different content means not equal
		}
	}
	return true // Same content means equal
}
