// 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 (
	"errors"
	"fmt"
	"os"
	"strings"

	"github.com/eminwux/kukeon/internal/cni"
	"github.com/eminwux/kukeon/internal/ctr"
	"github.com/eminwux/kukeon/internal/errdefs"
	"github.com/eminwux/kukeon/internal/metadata"
	intmodel "github.com/eminwux/kukeon/internal/modelhub"
	"github.com/eminwux/kukeon/internal/util/fs"
)

func (r *Exec) DeleteRealm(realm intmodel.Realm) error {
	realmName := strings.TrimSpace(realm.Metadata.Name)
	if realmName == "" {
		return errdefs.ErrRealmNameRequired
	}

	internalRealm, err := r.GetRealm(realm)
	if err != nil {
		if errors.Is(err, errdefs.ErrRealmNotFound) {
			// Idempotent: realm doesn't exist, consider it deleted
			return nil
		}
		return fmt.Errorf("%w: %w", errdefs.ErrGetRealm, err)
	}
	if err = r.ensureClientConnected(); err != nil {
		return fmt.Errorf("%w: %w", errdefs.ErrConnectContainerd, err)
	}

	// Find and clean up orphaned containers FIRST so their tasks are killed and
	// their committed snapshots released before CleanupNamespaceResources tries
	// to remove them — otherwise a live task holds the snapshot mount and
	// snapshotService.Remove silently fails, leaving the namespace non-empty
	// for the subsequent DeleteNamespace call.
	r.logger.DebugContext(r.ctx, "starting to find orphaned containers", "namespace", internalRealm.Spec.Namespace)
	containers, err := r.findOrphanedContainers(internalRealm.Spec.Namespace, "")
	if err != nil {
		r.logger.WarnContext(r.ctx, "failed to find orphaned containers", "error", err)
	} else {
		r.logger.DebugContext(r.ctx, "found orphaned containers", "count", len(containers))
		r.processOrphanedContainers(r.ctx, internalRealm.Spec.Namespace, containers)
	}

	// Clean up all namespace resources (images, snapshots, blobs) before deleting namespace
	// Namespace must be empty before deletion
	// Pass "" to walk every known snapshotter — see ctr.KukeonKnownSnapshotters.
	if err = r.ctrClient.CleanupNamespaceResources(internalRealm.Spec.Namespace, ""); err != nil {
		r.logger.WarnContext(
			r.ctx,
			"failed to cleanup namespace resources",
			"namespace",
			internalRealm.Spec.Namespace,
			"error",
			err,
		)
		// Continue with namespace deletion attempt anyway
	}

	// Delete realm cgroup after task processes are gone (otherwise cgroup
	// removal fails with EBUSY because PIDs are still attached).
	// Use the stored CgroupPath from realm status (includes full hierarchy path)
	// instead of rebuilding from DefaultRealmSpec which only has the relative path
	mountpoint := r.ctrClient.GetCgroupMountpoint()
	cgroupGroup := internalRealm.Status.CgroupPath
	if cgroupGroup == "" {
		// Fallback to DefaultRealmSpec if CgroupPath is not set (for backwards compatibility)
		spec := ctr.DefaultRealmSpec(internalRealm)
		cgroupGroup = spec.Group
	}
	if err = r.ctrClient.DeleteCgroup(cgroupGroup, mountpoint); err != nil {
		return fmt.Errorf("%w: failed to delete realm cgroup: %w", errdefs.ErrDeleteRealm, err)
	}

	// Tear down each conflist + bridge link for this realm. Driven from
	// CniConfigDir because that is the source of truth for which bridges
	// were created — IPAM dirs under CNINetworksDir can be missing if a
	// space's containers never started.
	r.teardownRealmCNI(internalRealm.Metadata.Name)

	// Purge IPAM/cache state for any leftover networks the realm owned.
	// Network names follow the pattern: {realmName}-{spaceName}
	// Scan /var/lib/cni/networks/ for all networks starting with realm name
	cniNetworksDir := cni.CNINetworksDir
	entries, err := os.ReadDir(cniNetworksDir)
	if err != nil {
		if !errors.Is(err, os.ErrNotExist) {
			r.logger.WarnContext(r.ctx, "failed to read CNI networks directory", "dir", cniNetworksDir, "error", err)
		}
	} else {
		realmPrefix := internalRealm.Metadata.Name + "-"
		for _, entry := range entries {
			if !entry.IsDir() {
				continue
			}
			networkName := entry.Name()
			// Check if network name starts with realm name prefix
			if strings.HasPrefix(networkName, realmPrefix) {
				r.logger.DebugContext(r.ctx, "purging CNI network for realm", "realm", internalRealm.Metadata.Name, "network", networkName)
				_ = r.purgeCNIForNetwork(networkName)
			}
		}
	}

	// Delete containerd namespace
	if err = r.ctrClient.DeleteNamespace(internalRealm.Spec.Namespace); err != nil {
		return fmt.Errorf("%w: failed to delete containerd namespace: %w", errdefs.ErrDeleteRealm, err)
	}

	// Tear down the BuildKit history-store companion (<namespace>_history) with
	// the same drain-then-delete the realm namespace got. A `kuke build` into
	// this realm leaves the companion behind; without this `kuke delete realm`
	// strands exactly the residue `kuke status` now flags (issue #1183). The
	// same "a deleted realm's build history should not survive" rationale that
	// folds this into the purge path applies verbatim here.
	if err = r.purgeBuildKitHistoryNamespace(internalRealm.Spec.Namespace); err != nil {
		return fmt.Errorf("%w: %w", errdefs.ErrDeleteRealm, err)
	}

	// Delete realm metadata file
	metadataFilePath := fs.RealmMetadataPath(r.opts.RunPath, internalRealm.Metadata.Name)
	if err = metadata.DeleteMetadata(r.ctx, r.logger, metadataFilePath); err != nil {
		return fmt.Errorf("%w: failed to delete realm metadata: %w", errdefs.ErrDeleteRealm, err)
	}

	return nil
}
