// 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 local provides an in-process kukeonv1.Client backed by a
// controller.Exec. Used by the CLI in --no-daemon mode (init, uninstall,
// purge *, get *, plus log/refresh/start/stop/doctor-cgroups via the
// `--run-path` promotion — workload commands routed through here pre-#566
// were moved off to shared.DaemonClientFromCmd in phase 3), by the
// `kuke image *` subcommands as the always-in-process path (#226), and by
// the daemon itself when servicing RPC calls.
package local

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strings"

	"github.com/eminwux/kukeon/internal/apischeme"
	"github.com/eminwux/kukeon/internal/apply/parser"
	"github.com/eminwux/kukeon/internal/controller"
	"github.com/eminwux/kukeon/internal/errdefs"
	intmodel "github.com/eminwux/kukeon/internal/modelhub"
	"github.com/eminwux/kukeon/internal/util/fs"
	"github.com/eminwux/kukeon/pkg/api/kukeonv1"
	v1beta1 "github.com/eminwux/kukeon/pkg/api/model/v1beta1"
)

// Client is an in-process kukeonv1.Client.
type Client struct {
	ctrl *controller.Exec
}

// New returns a Client that owns its controller.Exec. Close releases it.
func New(ctx context.Context, logger *slog.Logger, opts controller.Options) *Client {
	return &Client{ctrl: controller.NewControllerExec(ctx, logger, opts)}
}

// NewFromExec wraps an already-constructed controller.Exec. The caller
// retains ownership of ctrl; Close is a no-op in this mode.
func NewFromExec(ctrl *controller.Exec) *Client {
	return &Client{ctrl: ctrl}
}

// Close releases the underlying controller if this Client owns it.
func (c *Client) Close() error {
	if c.ctrl == nil {
		return nil
	}
	return c.ctrl.Close()
}

// CreateRealm normalizes the external doc, delegates to the controller, and
// reshapes the result back into external v1beta1 types.
func (c *Client) CreateRealm(_ context.Context, doc v1beta1.RealmDoc) (kukeonv1.CreateRealmResult, error) {
	internal, version, err := apischeme.NormalizeRealm(doc)
	if err != nil {
		return kukeonv1.CreateRealmResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.CreateRealm(internal)
	if err != nil {
		return kukeonv1.CreateRealmResult{}, err
	}
	extRealm, err := apischeme.BuildRealmExternalFromInternal(res.Realm, version)
	if err != nil {
		return kukeonv1.CreateRealmResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.CreateRealmResult{
		Realm:                         extRealm,
		MetadataExistsPre:             res.MetadataExistsPre,
		MetadataExistsPost:            res.MetadataExistsPost,
		CgroupExistsPre:               res.CgroupExistsPre,
		CgroupExistsPost:              res.CgroupExistsPost,
		CgroupCreated:                 res.CgroupCreated,
		ContainerdNamespaceExistsPre:  res.ContainerdNamespaceExistsPre,
		ContainerdNamespaceExistsPost: res.ContainerdNamespaceExistsPost,
		ContainerdNamespaceCreated:    res.ContainerdNamespaceCreated,
		Created:                       res.Created,
	}, nil
}

// CreateSpace normalizes the external doc, delegates to the controller, and
// reshapes the result back into external v1beta1 types.
func (c *Client) CreateSpace(_ context.Context, doc v1beta1.SpaceDoc) (kukeonv1.CreateSpaceResult, error) {
	internal, version, err := apischeme.NormalizeSpace(doc)
	if err != nil {
		return kukeonv1.CreateSpaceResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.CreateSpace(internal)
	if err != nil {
		return kukeonv1.CreateSpaceResult{}, err
	}
	extSpace, err := apischeme.BuildSpaceExternalFromInternal(res.Space, version)
	if err != nil {
		return kukeonv1.CreateSpaceResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.CreateSpaceResult{
		Space:                extSpace,
		MetadataExistsPre:    res.MetadataExistsPre,
		MetadataExistsPost:   res.MetadataExistsPost,
		CgroupExistsPre:      res.CgroupExistsPre,
		CgroupExistsPost:     res.CgroupExistsPost,
		CgroupCreated:        res.CgroupCreated,
		CNINetworkExistsPre:  res.CNINetworkExistsPre,
		CNINetworkExistsPost: res.CNINetworkExistsPost,
		CNINetworkCreated:    res.CNINetworkCreated,
		Created:              res.Created,
	}, nil
}

// CreateStack normalizes the external doc, delegates to the controller, and
// reshapes the result back into external v1beta1 types.
func (c *Client) CreateStack(_ context.Context, doc v1beta1.StackDoc) (kukeonv1.CreateStackResult, error) {
	internal, version, err := apischeme.NormalizeStack(doc)
	if err != nil {
		return kukeonv1.CreateStackResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.CreateStack(internal)
	if err != nil {
		return kukeonv1.CreateStackResult{}, err
	}
	extStack, err := apischeme.BuildStackExternalFromInternal(res.Stack, version)
	if err != nil {
		return kukeonv1.CreateStackResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.CreateStackResult{
		Stack:              extStack,
		MetadataExistsPre:  res.MetadataExistsPre,
		MetadataExistsPost: res.MetadataExistsPost,
		CgroupExistsPre:    res.CgroupExistsPre,
		CgroupExistsPost:   res.CgroupExistsPost,
		CgroupCreated:      res.CgroupCreated,
		Created:            res.Created,
	}, nil
}

// CreateCell normalizes the external doc, delegates to the controller, and
// reshapes the result back into external v1beta1 types. Starts the cell's
// containers after creation; see MaterializeCell for the don't-start variant.
func (c *Client) CreateCell(_ context.Context, doc v1beta1.CellDoc) (kukeonv1.CreateCellResult, error) {
	return c.createCell(doc, c.ctrl.CreateCell)
}

// MaterializeCell normalizes the external doc, delegates to the controller's
// MaterializeCell (which skips the StartCell step), and reshapes the result
// back into external v1beta1 types. See kukeonv1.Client.MaterializeCell for
// the contract.
func (c *Client) MaterializeCell(_ context.Context, doc v1beta1.CellDoc) (kukeonv1.CreateCellResult, error) {
	return c.createCell(doc, c.ctrl.MaterializeCell)
}

// createCell is the shared body for CreateCell and MaterializeCell. The
// controllerFn argument picks between the start-after-create variant
// (CreateCell) and the don't-start variant (MaterializeCell); the doc
// normalisation and result reshape are identical for both.
func (c *Client) createCell(
	doc v1beta1.CellDoc,
	controllerFn func(intmodel.Cell) (controller.CreateCellResult, error),
) (kukeonv1.CreateCellResult, error) {
	internal, version, err := apischeme.NormalizeCell(doc)
	if err != nil {
		return kukeonv1.CreateCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}

	res, err := controllerFn(internal)
	if err != nil {
		return kukeonv1.CreateCellResult{}, err
	}

	extCell, err := apischeme.BuildCellExternalFromInternal(res.Cell, version)
	if err != nil {
		return kukeonv1.CreateCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}

	out := kukeonv1.CreateCellResult{
		Cell:                    extCell,
		MetadataExistsPre:       res.MetadataExistsPre,
		MetadataExistsPost:      res.MetadataExistsPost,
		CgroupExistsPre:         res.CgroupExistsPre,
		CgroupExistsPost:        res.CgroupExistsPost,
		CgroupCreated:           res.CgroupCreated,
		RootContainerExistsPre:  res.RootContainerExistsPre,
		RootContainerExistsPost: res.RootContainerExistsPost,
		RootContainerCreated:    res.RootContainerCreated,
		StartedPre:              res.StartedPre,
		StartedPost:             res.StartedPost,
		Started:                 res.Started,
		Created:                 res.Created,
	}
	if len(res.Containers) > 0 {
		out.Containers = make([]kukeonv1.ContainerCreationOutcome, len(res.Containers))
		for i, container := range res.Containers {
			out.Containers[i] = kukeonv1.ContainerCreationOutcome{
				Name:       container.Name,
				ExistsPre:  container.ExistsPre,
				ExistsPost: container.ExistsPost,
				Created:    container.Created,
			}
		}
	}
	return out, nil
}

// ---- Get ----

func (c *Client) GetRealm(_ context.Context, doc v1beta1.RealmDoc) (kukeonv1.GetRealmResult, error) {
	internal, _, err := apischeme.NormalizeRealm(doc)
	if err != nil {
		return kukeonv1.GetRealmResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.GetRealm(internal)
	if err != nil {
		return kukeonv1.GetRealmResult{}, err
	}
	extRealm, err := fs.ConvertRealmToExternal(res.Realm)
	if err != nil {
		return kukeonv1.GetRealmResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.GetRealmResult{
		Realm:                     *extRealm,
		MetadataExists:            res.MetadataExists,
		CgroupExists:              res.CgroupExists,
		ContainerdNamespaceExists: res.ContainerdNamespaceExists,
	}, nil
}

func (c *Client) GetSpace(_ context.Context, doc v1beta1.SpaceDoc) (kukeonv1.GetSpaceResult, error) {
	internal, _, err := apischeme.NormalizeSpace(doc)
	if err != nil {
		return kukeonv1.GetSpaceResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.GetSpace(internal)
	if err != nil {
		return kukeonv1.GetSpaceResult{}, err
	}
	extSpace, err := fs.ConvertSpaceToExternal(res.Space)
	if err != nil {
		return kukeonv1.GetSpaceResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.GetSpaceResult{
		Space:            *extSpace,
		MetadataExists:   res.MetadataExists,
		CgroupExists:     res.CgroupExists,
		CNINetworkExists: res.CNINetworkExists,
	}, nil
}

func (c *Client) GetStack(_ context.Context, doc v1beta1.StackDoc) (kukeonv1.GetStackResult, error) {
	internal, _, err := apischeme.NormalizeStack(doc)
	if err != nil {
		return kukeonv1.GetStackResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.GetStack(internal)
	if err != nil {
		return kukeonv1.GetStackResult{}, err
	}
	extStack, err := fs.ConvertStackToExternal(res.Stack)
	if err != nil {
		return kukeonv1.GetStackResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.GetStackResult{
		Stack:          *extStack,
		MetadataExists: res.MetadataExists,
		CgroupExists:   res.CgroupExists,
	}, nil
}

func (c *Client) GetCell(_ context.Context, doc v1beta1.CellDoc) (kukeonv1.GetCellResult, error) {
	internal, _, err := apischeme.NormalizeCell(doc)
	if err != nil {
		return kukeonv1.GetCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.GetCell(internal)
	if err != nil {
		return kukeonv1.GetCellResult{}, err
	}
	extCell, err := fs.ConvertCellToExternal(res.Cell)
	if err != nil {
		return kukeonv1.GetCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.GetCellResult{
		Cell:                     *extCell,
		MetadataExists:           res.MetadataExists,
		CgroupExists:             res.CgroupExists,
		RootContainerExists:      res.RootContainerExists,
		RootContainerTaskRunning: res.RootContainerTaskRunning,
	}, nil
}

func (c *Client) GetContainer(_ context.Context, doc v1beta1.ContainerDoc) (kukeonv1.GetContainerResult, error) {
	internal, _, err := apischeme.NormalizeContainer(doc)
	if err != nil {
		return kukeonv1.GetContainerResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.GetContainer(internal)
	if err != nil {
		return kukeonv1.GetContainerResult{}, err
	}
	extContainer, err := fs.ConvertContainerToExternal(res.Container)
	if err != nil {
		return kukeonv1.GetContainerResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.GetContainerResult{
		Container:          *extContainer,
		CellMetadataExists: res.CellMetadataExists,
		ContainerExists:    res.ContainerExists,
	}, nil
}

func (c *Client) GetSecret(_ context.Context, doc v1beta1.SecretDoc) (kukeonv1.GetSecretResult, error) {
	internal, _, err := apischeme.NormalizeSecret(doc)
	if err != nil {
		return kukeonv1.GetSecretResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.GetSecret(internal)
	if err != nil {
		return kukeonv1.GetSecretResult{}, err
	}
	return kukeonv1.GetSecretResult{
		Secret:         apischeme.ConvertSecretToExternal(res.Secret),
		MetadataExists: res.MetadataExists,
	}, nil
}

func (c *Client) GetBlueprint(
	_ context.Context, doc v1beta1.CellBlueprintDoc,
) (kukeonv1.GetBlueprintResult, error) {
	internal, _, err := apischeme.NormalizeCellBlueprint(doc)
	if err != nil {
		return kukeonv1.GetBlueprintResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.GetBlueprint(internal)
	if err != nil {
		return kukeonv1.GetBlueprintResult{}, err
	}
	if !res.MetadataExists {
		return kukeonv1.GetBlueprintResult{MetadataExists: false}, nil
	}
	ext, err := apischeme.ConvertCellBlueprintToExternal(res.Blueprint)
	if err != nil {
		return kukeonv1.GetBlueprintResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.GetBlueprintResult{
		Blueprint:      ext,
		MetadataExists: true,
	}, nil
}

func (c *Client) CreateConfig(
	_ context.Context, doc v1beta1.CellConfigDoc,
) (kukeonv1.CreateConfigResult, error) {
	internal, _, err := apischeme.NormalizeCellConfig(doc)
	if err != nil {
		return kukeonv1.CreateConfigResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.CreateConfig(internal)
	if err != nil {
		return kukeonv1.CreateConfigResult{}, err
	}
	ext, err := apischeme.ConvertCellConfigToExternal(res.Config)
	if err != nil {
		return kukeonv1.CreateConfigResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.CreateConfigResult{
		Config:  ext,
		Created: res.Created,
	}, nil
}

func (c *Client) CreateSecret(
	_ context.Context, doc v1beta1.SecretDoc,
) (kukeonv1.CreateSecretResult, error) {
	internal, _, err := apischeme.NormalizeSecret(doc)
	if err != nil {
		return kukeonv1.CreateSecretResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.CreateSecret(internal)
	if err != nil {
		return kukeonv1.CreateSecretResult{}, err
	}
	ext := apischeme.ConvertSecretToExternal(res.Secret)
	return kukeonv1.CreateSecretResult{
		Secret:  ext,
		Created: res.Created,
	}, nil
}

func (c *Client) GetConfig(
	_ context.Context, doc v1beta1.CellConfigDoc,
) (kukeonv1.GetConfigResult, error) {
	internal, _, err := apischeme.NormalizeCellConfig(doc)
	if err != nil {
		return kukeonv1.GetConfigResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.GetConfig(internal)
	if err != nil {
		return kukeonv1.GetConfigResult{}, err
	}
	if !res.MetadataExists {
		return kukeonv1.GetConfigResult{MetadataExists: false}, nil
	}
	ext, err := apischeme.ConvertCellConfigToExternal(res.Config)
	if err != nil {
		return kukeonv1.GetConfigResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.GetConfigResult{
		Config:         ext,
		MetadataExists: true,
	}, nil
}

// ---- List ----

func (c *Client) ListRealms(_ context.Context) ([]v1beta1.RealmDoc, error) {
	realms, err := c.ctrl.ListRealms()
	if err != nil {
		return nil, err
	}
	ext, err := fs.ConvertRealmListToExternal(realms)
	if err != nil {
		return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return derefDocs(ext), nil
}

func (c *Client) ListSpaces(_ context.Context, realmName string) ([]v1beta1.SpaceDoc, error) {
	spaces, err := c.ctrl.ListSpaces(realmName)
	if err != nil {
		return nil, err
	}
	ext, err := fs.ConvertSpaceListToExternal(spaces)
	if err != nil {
		return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return derefDocs(ext), nil
}

func (c *Client) ListStacks(_ context.Context, realmName, spaceName string) ([]v1beta1.StackDoc, error) {
	stacks, err := c.ctrl.ListStacks(realmName, spaceName)
	if err != nil {
		return nil, err
	}
	ext, err := fs.ConvertStackListToExternal(stacks)
	if err != nil {
		return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return derefDocs(ext), nil
}

func (c *Client) ListCells(_ context.Context, realmName, spaceName, stackName string) ([]v1beta1.CellDoc, error) {
	cells, err := c.ctrl.ListCells(realmName, spaceName, stackName)
	if err != nil {
		return nil, err
	}
	ext, err := fs.ConvertCellListToExternal(cells)
	if err != nil {
		return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return derefDocs(ext), nil
}

func (c *Client) ListContainers(
	_ context.Context,
	realmName, spaceName, stackName, cellName string,
) ([]v1beta1.ContainerSpec, error) {
	specs, err := c.ctrl.ListContainers(realmName, spaceName, stackName, cellName)
	if err != nil {
		return nil, err
	}
	ext, err := fs.ConvertContainerSpecListToExternal(specs)
	if err != nil {
		return nil, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return derefDocs(ext), nil
}

func (c *Client) ListSecrets(
	_ context.Context,
	realmName, spaceName, stackName, cellName string,
) ([]v1beta1.SecretDoc, error) {
	secrets, err := c.ctrl.ListSecrets(realmName, spaceName, stackName, cellName)
	if err != nil {
		return nil, err
	}
	return apischeme.ConvertSecretListToExternal(secrets), nil
}

func (c *Client) ListBlueprints(
	_ context.Context,
	realmName, spaceName, stackName string,
) ([]v1beta1.CellBlueprintDoc, error) {
	blueprints, err := c.ctrl.ListBlueprints(realmName, spaceName, stackName)
	if err != nil {
		return nil, err
	}
	return apischeme.ConvertCellBlueprintListToExternal(blueprints), nil
}

func (c *Client) ListConfigs(
	_ context.Context,
	realmName, spaceName, stackName string,
) ([]v1beta1.CellConfigDoc, error) {
	configs, err := c.ctrl.ListConfigs(realmName, spaceName, stackName)
	if err != nil {
		return nil, err
	}
	return apischeme.ConvertCellConfigListToExternal(configs), nil
}

// derefDocs converts a []*T returned by fs.Convert* helpers into the
// wire-friendly []T form (pointer slices don't round-trip cleanly through
// net/rpc+jsonrpc — jsonrpc would encode them correctly but gob couldn't,
// and value slices keep the interface signature simpler).
func derefDocs[T any](in []*T) []T {
	out := make([]T, len(in))
	for i, p := range in {
		if p != nil {
			out[i] = *p
		}
	}
	return out
}

// ---- Lifecycle ----

func (c *Client) StartCell(_ context.Context, doc v1beta1.CellDoc) (kukeonv1.StartCellResult, error) {
	internal, version, err := apischeme.NormalizeCell(doc)
	if err != nil {
		return kukeonv1.StartCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.StartCell(internal)
	if err != nil {
		return kukeonv1.StartCellResult{}, err
	}
	ext, err := apischeme.BuildCellExternalFromInternal(res.Cell, version)
	if err != nil {
		return kukeonv1.StartCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.StartCellResult{Cell: ext, Started: res.Started}, nil
}

func (c *Client) StopCell(_ context.Context, doc v1beta1.CellDoc) (kukeonv1.StopCellResult, error) {
	internal, version, err := apischeme.NormalizeCell(doc)
	if err != nil {
		return kukeonv1.StopCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.StopCell(internal)
	if err != nil {
		return kukeonv1.StopCellResult{}, err
	}
	ext, err := apischeme.BuildCellExternalFromInternal(res.Cell, version)
	if err != nil {
		return kukeonv1.StopCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.StopCellResult{Cell: ext, Stopped: res.Stopped}, nil
}

func (c *Client) KillCell(_ context.Context, doc v1beta1.CellDoc) (kukeonv1.KillCellResult, error) {
	internal, version, err := apischeme.NormalizeCell(doc)
	if err != nil {
		return kukeonv1.KillCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.KillCell(internal)
	if err != nil {
		return kukeonv1.KillCellResult{}, err
	}
	ext, err := apischeme.BuildCellExternalFromInternal(res.Cell, version)
	if err != nil {
		return kukeonv1.KillCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.KillCellResult{Cell: ext, Killed: res.Killed}, nil
}

// ---- Delete ----

func (c *Client) DeleteRealm(
	_ context.Context,
	doc v1beta1.RealmDoc,
	force, cascade bool,
) (kukeonv1.DeleteRealmResult, error) {
	internal, version, err := apischeme.NormalizeRealm(doc)
	if err != nil {
		return kukeonv1.DeleteRealmResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.DeleteRealm(internal, force, cascade)
	if err != nil {
		return kukeonv1.DeleteRealmResult{}, err
	}
	ext, err := apischeme.BuildRealmExternalFromInternal(res.Realm, version)
	if err != nil {
		return kukeonv1.DeleteRealmResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.DeleteRealmResult{
		Realm:                      ext,
		Deleted:                    res.Deleted,
		MetadataDeleted:            res.MetadataDeleted,
		CgroupDeleted:              res.CgroupDeleted,
		ContainerdNamespaceDeleted: res.ContainerdNamespaceDeleted,
	}, nil
}

func (c *Client) DeleteSpace(
	_ context.Context,
	doc v1beta1.SpaceDoc,
	force, cascade bool,
) (kukeonv1.DeleteSpaceResult, error) {
	internal, version, err := apischeme.NormalizeSpace(doc)
	if err != nil {
		return kukeonv1.DeleteSpaceResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.DeleteSpace(internal, force, cascade)
	if err != nil {
		return kukeonv1.DeleteSpaceResult{}, err
	}
	ext, err := apischeme.BuildSpaceExternalFromInternal(res.Space, version)
	if err != nil {
		return kukeonv1.DeleteSpaceResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.DeleteSpaceResult{
		Space:             ext,
		SpaceName:         res.SpaceName,
		RealmName:         res.RealmName,
		MetadataDeleted:   res.MetadataDeleted,
		CgroupDeleted:     res.CgroupDeleted,
		CNINetworkDeleted: res.CNINetworkDeleted,
		Deleted:           res.Deleted,
	}, nil
}

func (c *Client) DeleteStack(
	_ context.Context,
	doc v1beta1.StackDoc,
	force, cascade bool,
) (kukeonv1.DeleteStackResult, error) {
	internal, version, err := apischeme.NormalizeStack(doc)
	if err != nil {
		return kukeonv1.DeleteStackResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.DeleteStack(internal, force, cascade)
	if err != nil {
		return kukeonv1.DeleteStackResult{}, err
	}
	ext, err := apischeme.BuildStackExternalFromInternal(res.Stack, version)
	if err != nil {
		return kukeonv1.DeleteStackResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.DeleteStackResult{
		Stack:           ext,
		StackName:       res.StackName,
		RealmName:       res.RealmName,
		SpaceName:       res.SpaceName,
		MetadataDeleted: res.MetadataDeleted,
		CgroupDeleted:   res.CgroupDeleted,
		Deleted:         res.Deleted,
	}, nil
}

func (c *Client) DeleteCell(_ context.Context, doc v1beta1.CellDoc) (kukeonv1.DeleteCellResult, error) {
	internal, version, err := apischeme.NormalizeCell(doc)
	if err != nil {
		return kukeonv1.DeleteCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.DeleteCell(internal)
	if err != nil {
		return kukeonv1.DeleteCellResult{}, err
	}
	ext, err := apischeme.BuildCellExternalFromInternal(res.Cell, version)
	if err != nil {
		return kukeonv1.DeleteCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.DeleteCellResult{
		Cell:              ext,
		ContainersDeleted: res.ContainersDeleted,
		CgroupDeleted:     res.CgroupDeleted,
		MetadataDeleted:   res.MetadataDeleted,
	}, nil
}

func (c *Client) DeleteSecret(_ context.Context, doc v1beta1.SecretDoc) (kukeonv1.DeleteSecretResult, error) {
	internal, _, err := apischeme.NormalizeSecret(doc)
	if err != nil {
		return kukeonv1.DeleteSecretResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.DeleteSecret(internal)
	if err != nil {
		return kukeonv1.DeleteSecretResult{}, err
	}
	return kukeonv1.DeleteSecretResult{
		Secret:  apischeme.ConvertSecretToExternal(res.Secret),
		Deleted: res.Deleted,
	}, nil
}

func (c *Client) DeleteBlueprint(
	_ context.Context, doc v1beta1.CellBlueprintDoc,
) (kukeonv1.DeleteBlueprintResult, error) {
	internal, _, err := apischeme.NormalizeCellBlueprint(doc)
	if err != nil {
		return kukeonv1.DeleteBlueprintResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.DeleteBlueprint(internal)
	if err != nil {
		return kukeonv1.DeleteBlueprintResult{}, err
	}
	return kukeonv1.DeleteBlueprintResult{
		Blueprint: apischeme.ConvertCellBlueprintMetadataToExternal(res.Blueprint),
		Deleted:   res.Deleted,
	}, nil
}

func (c *Client) DeleteConfig(
	_ context.Context, doc v1beta1.CellConfigDoc,
) (kukeonv1.DeleteConfigResult, error) {
	internal, _, err := apischeme.NormalizeCellConfig(doc)
	if err != nil {
		return kukeonv1.DeleteConfigResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.DeleteConfig(internal)
	if err != nil {
		return kukeonv1.DeleteConfigResult{}, err
	}
	return kukeonv1.DeleteConfigResult{
		Config:       apischeme.ConvertCellConfigMetadataToExternal(res.Config),
		Deleted:      res.Deleted,
		BackRefCells: res.BackRefCells,
	}, nil
}

// ---- Purge ----

func (c *Client) PurgeRealm(
	_ context.Context,
	doc v1beta1.RealmDoc,
	force, cascade bool,
) (kukeonv1.PurgeRealmResult, error) {
	internal, version, err := apischeme.NormalizeRealm(doc)
	if err != nil {
		return kukeonv1.PurgeRealmResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.PurgeRealm(internal, force, cascade)
	if err != nil {
		return kukeonv1.PurgeRealmResult{}, err
	}
	ext, err := apischeme.BuildRealmExternalFromInternal(res.Realm, version)
	if err != nil {
		return kukeonv1.PurgeRealmResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.PurgeRealmResult{
		Realm:          ext,
		RealmDeleted:   res.RealmDeleted,
		PurgeSucceeded: res.PurgeSucceeded,
		Force:          res.Force,
		Cascade:        res.Cascade,
		Deleted:        res.Deleted,
		Purged:         res.Purged,
	}, nil
}

func (c *Client) PurgeSpace(
	_ context.Context,
	doc v1beta1.SpaceDoc,
	force, cascade bool,
) (kukeonv1.PurgeSpaceResult, error) {
	internal, version, err := apischeme.NormalizeSpace(doc)
	if err != nil {
		return kukeonv1.PurgeSpaceResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.PurgeSpace(internal, force, cascade)
	if err != nil {
		return kukeonv1.PurgeSpaceResult{}, err
	}
	ext, err := apischeme.BuildSpaceExternalFromInternal(res.Space, version)
	if err != nil {
		return kukeonv1.PurgeSpaceResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.PurgeSpaceResult{
		Space:             ext,
		MetadataDeleted:   res.MetadataDeleted,
		CgroupDeleted:     res.CgroupDeleted,
		CNINetworkDeleted: res.CNINetworkDeleted,
		PurgeSucceeded:    res.PurgeSucceeded,
		Force:             res.Force,
		Cascade:           res.Cascade,
		Deleted:           res.Deleted,
		Purged:            res.Purged,
	}, nil
}

func (c *Client) PurgeStack(
	_ context.Context,
	doc v1beta1.StackDoc,
	force, cascade bool,
) (kukeonv1.PurgeStackResult, error) {
	internal, version, err := apischeme.NormalizeStack(doc)
	if err != nil {
		return kukeonv1.PurgeStackResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.PurgeStack(internal, force, cascade)
	if err != nil {
		return kukeonv1.PurgeStackResult{}, err
	}
	ext, err := apischeme.BuildStackExternalFromInternal(res.Stack, version)
	if err != nil {
		return kukeonv1.PurgeStackResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.PurgeStackResult{
		Stack:   ext,
		Deleted: res.Deleted,
		Purged:  res.Purged,
	}, nil
}

func (c *Client) PurgeCell(
	_ context.Context,
	doc v1beta1.CellDoc,
	force, cascade bool,
) (kukeonv1.PurgeCellResult, error) {
	internal, version, err := apischeme.NormalizeCell(doc)
	if err != nil {
		return kukeonv1.PurgeCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.PurgeCell(internal, force, cascade)
	if err != nil {
		return kukeonv1.PurgeCellResult{}, err
	}
	ext, err := apischeme.BuildCellExternalFromInternal(res.Cell, version)
	if err != nil {
		return kukeonv1.PurgeCellResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	return kukeonv1.PurgeCellResult{
		Cell:              ext,
		ContainersDeleted: res.ContainersDeleted,
		CgroupDeleted:     res.CgroupDeleted,
		MetadataDeleted:   res.MetadataDeleted,
		PurgeSucceeded:    res.PurgeSucceeded,
		Force:             res.Force,
		Cascade:           res.Cascade,
		Deleted:           res.Deleted,
		Purged:            res.Purged,
	}, nil
}

// ---- Reconcile ----

// ReconcileCells runs one pass of the daemon-side cell reconciliation loop.
// Daemon-internal: not surfaced over the kukeonv1 wire — `kuke refresh` is
// the on-demand, all-resources counterpart that clients use.
func (c *Client) ReconcileCells() (controller.ReconcileResult, error) {
	return c.ctrl.ReconcileCells()
}

// ---- Refresh ----

func (c *Client) RefreshAll(_ context.Context) (kukeonv1.RefreshAllResult, error) {
	res, err := c.ctrl.RefreshAll()
	if err != nil {
		return kukeonv1.RefreshAllResult{}, err
	}
	return kukeonv1.RefreshAllResult{
		RealmsFound:       res.RealmsFound,
		SpacesFound:       res.SpacesFound,
		StacksFound:       res.StacksFound,
		CellsFound:        res.CellsFound,
		ContainersFound:   res.ContainersFound,
		RealmsUpdated:     res.RealmsUpdated,
		SpacesUpdated:     res.SpacesUpdated,
		StacksUpdated:     res.StacksUpdated,
		CellsUpdated:      res.CellsUpdated,
		ContainersUpdated: res.ContainersUpdated,
		Errors:            res.Errors,
	}, nil
}

// ---- Apply ----

func (c *Client) ApplyDocuments(ctx context.Context, rawYAML []byte) (kukeonv1.ApplyDocumentsResult, error) {
	return c.applyDocuments(ctx, rawYAML, "")
}

// ApplyDocumentsForTeam runs the in-process equivalent of the wire RPC
// per-team prune-apply path (issue #1027). Empty team rejected at the
// boundary so a caller cannot accidentally degrade into the historical
// no-prune apply by passing "".
func (c *Client) ApplyDocumentsForTeam(
	ctx context.Context, rawYAML []byte, team string,
) (kukeonv1.ApplyDocumentsResult, error) {
	if team == "" {
		return kukeonv1.ApplyDocumentsResult{}, errors.New("apply for team: team is required")
	}
	return c.applyDocuments(ctx, rawYAML, team)
}

func (c *Client) applyDocuments(
	_ context.Context, rawYAML []byte, team string,
) (kukeonv1.ApplyDocumentsResult, error) {
	docs, validationErrors, err := parseAndValidate(rawYAML)
	if err != nil {
		return kukeonv1.ApplyDocumentsResult{}, err
	}
	if len(validationErrors) > 0 {
		return kukeonv1.ApplyDocumentsResult{}, formatValidationErrors(validationErrors)
	}
	if len(docs) == 0 {
		return kukeonv1.ApplyDocumentsResult{}, errors.New("no valid documents found in input")
	}

	res, err := c.ctrl.ApplyDocuments(docs, team)
	if err != nil {
		return kukeonv1.ApplyDocumentsResult{}, err
	}

	out := kukeonv1.ApplyDocumentsResult{
		Resources: make([]kukeonv1.ApplyResourceResult, 0, len(res.Resources)),
	}
	for _, r := range res.Resources {
		item := kukeonv1.ApplyResourceResult{
			Index:   r.Index,
			Kind:    r.Kind,
			Name:    r.Name,
			Action:  r.Action,
			Changes: r.Changes,
			Details: r.Details,
		}
		if r.Error != nil {
			item.Error = r.Error.Error()
		}
		out.Resources = append(out.Resources, item)
	}
	return out, nil
}

func (c *Client) DeleteDocuments(
	_ context.Context,
	rawYAML []byte,
	cascade, force bool,
) (kukeonv1.DeleteDocumentsResult, error) {
	docs, validationErrors, err := parseAndValidate(rawYAML)
	if err != nil {
		return kukeonv1.DeleteDocumentsResult{}, err
	}
	if len(validationErrors) > 0 {
		return kukeonv1.DeleteDocumentsResult{}, formatValidationErrors(validationErrors)
	}
	if len(docs) == 0 {
		return kukeonv1.DeleteDocumentsResult{}, errors.New("no valid documents found in input")
	}

	res, err := c.ctrl.DeleteDocuments(docs, cascade, force)
	if err != nil {
		return kukeonv1.DeleteDocumentsResult{}, err
	}

	out := kukeonv1.DeleteDocumentsResult{
		Resources: make([]kukeonv1.DeleteResourceResult, 0, len(res.Resources)),
	}
	for _, r := range res.Resources {
		item := kukeonv1.DeleteResourceResult{
			Index:    r.Index,
			Kind:     r.Kind,
			Name:     r.Name,
			Action:   r.Action,
			Cascaded: r.Cascaded,
			Details:  r.Details,
		}
		if r.Error != nil {
			item.Error = r.Error.Error()
		}
		out.Resources = append(out.Resources, item)
	}
	return out, nil
}

// parseAndValidate mirrors cmd/kuke/shared.ParseAndValidateDocuments, but
// takes a byte slice so it works both server-side (from the wire) and in
// the --no-daemon CLI path.
func parseAndValidate(rawYAML []byte) ([]parser.Document, []*parser.ValidationError, error) {
	rawDocs, err := parser.ParseDocuments(bytes.NewReader(rawYAML))
	if err != nil {
		return nil, nil, fmt.Errorf("failed to parse YAML: %w", err)
	}

	docs := make([]parser.Document, 0, len(rawDocs))
	var validationErrors []*parser.ValidationError

	for i, rawDoc := range rawDocs {
		doc, parseErr := parser.ParseDocument(i, rawDoc)
		if parseErr != nil {
			validationErrors = append(validationErrors, &parser.ValidationError{Index: i, Err: parseErr})
			continue
		}
		validationErr := parser.ValidateDocument(doc)
		if validationErr != nil {
			validationErrors = append(validationErrors, validationErr)
			continue
		}
		docs = append(docs, *doc)
	}

	return docs, validationErrors, nil
}

func formatValidationErrors(validationErrors []*parser.ValidationError) error {
	msgs := make([]string, 0, len(validationErrors))
	for _, ve := range validationErrors {
		msgs = append(msgs, ve.Error())
	}
	return fmt.Errorf("validation failed:\n  %s", strings.Join(msgs, "\n  "))
}

// ---- Image ----

// LoadImage imports an OCI/docker image tarball into the realm's containerd
// namespace. The byte slice is wrapped in a bytes.Reader so the controller
// can stream it through to containerd's Import API.
func (c *Client) LoadImage(_ context.Context, realm string, tarball []byte) (kukeonv1.LoadImageResult, error) {
	if len(tarball) == 0 {
		return kukeonv1.LoadImageResult{}, errdefs.ErrTarballRequired
	}
	res, err := c.ctrl.LoadImage(realm, bytes.NewReader(tarball))
	if err != nil {
		return kukeonv1.LoadImageResult{}, err
	}
	return kukeonv1.LoadImageResult{
		Realm:     res.Realm,
		Namespace: res.Namespace,
		Images:    res.Images,
	}, nil
}

// ListImages enumerates images in the realm's containerd namespace. The
// realm is validated by the controller layer; this wrapper only re-encodes
// the controller's ImageInfo onto the wire type so callers never import
// the controller package.
func (c *Client) ListImages(_ context.Context, realm string) (kukeonv1.ListImagesResult, error) {
	res, err := c.ctrl.ListImages(realm)
	if err != nil {
		return kukeonv1.ListImagesResult{}, err
	}
	out := kukeonv1.ListImagesResult{
		Realm:     res.Realm,
		Namespace: res.Namespace,
		Images:    make([]kukeonv1.ImageInfo, 0, len(res.Images)),
	}
	for _, img := range res.Images {
		out.Images = append(out.Images, controllerImageToWire(img))
	}
	return out, nil
}

// GetImage returns metadata for the named image ref in the realm.
// errdefs.ErrImageNotFound is propagated unchanged so the wire layer can
// emit the matching APIError Kind.
func (c *Client) GetImage(_ context.Context, realm, ref string) (kukeonv1.GetImageResult, error) {
	res, err := c.ctrl.GetImage(realm, ref)
	if err != nil {
		return kukeonv1.GetImageResult{}, err
	}
	return kukeonv1.GetImageResult{
		Realm:     res.Realm,
		Namespace: res.Namespace,
		Image:     controllerImageToWire(res.Image),
	}, nil
}

// DeleteImage removes the named image ref from the realm's containerd
// namespace. errdefs.ErrImageNotFound is propagated unchanged so the wire
// layer can emit the matching APIError Kind.
func (c *Client) DeleteImage(_ context.Context, realm, ref string) (kukeonv1.DeleteImageResult, error) {
	res, err := c.ctrl.DeleteImage(realm, ref)
	if err != nil {
		return kukeonv1.DeleteImageResult{}, err
	}
	return kukeonv1.DeleteImageResult{
		Realm:     res.Realm,
		Namespace: res.Namespace,
		Ref:       res.Ref,
	}, nil
}

// PruneImages reclaims dangling image layers and the orphaned leases pinning
// them in the realm's containerd namespace. The realm is validated by the
// controller layer; this wrapper only re-encodes the controller result onto
// the wire type.
func (c *Client) PruneImages(_ context.Context, realm string) (kukeonv1.PruneImagesResult, error) {
	res, err := c.ctrl.PruneImages(realm)
	if err != nil {
		return kukeonv1.PruneImagesResult{}, err
	}
	return kukeonv1.PruneImagesResult{
		Realm:          res.Realm,
		Namespace:      res.Namespace,
		LeasesDeleted:  res.LeasesDeleted,
		LeasesRetained: res.LeasesRetained,
	}, nil
}

func controllerImageToWire(img controller.ImageInfo) kukeonv1.ImageInfo {
	return kukeonv1.ImageInfo{
		Name:      img.Name,
		Size:      img.Size,
		CreatedAt: img.CreatedAt,
		Digest:    img.Digest,
		MediaType: img.MediaType,
		Labels:    img.Labels,
	}
}

// ---- Attach ----

// AttachContainer enforces the Attachable gate, the server-side task-
// liveness gate, and resolves the host-side sbsh control-socket path.
// Bytes never traverse this RPC — the caller (`kuke attach`) opens
// HostSocketPath directly and runs the sbsh client loop against it.
//
// Returns the SUN_PATH-safe symlink path (fs.ContainerSocketSymlinkPath)
// rather than the deep socket inode path so `connect(2)` never sees a
// sun_path longer than 107 bytes regardless of how deep the resolved
// metadata layout is (issue #521). The runner stages the symlink at
// provision time so a re-derived path on a freshly-spawned `kuke attach`
// is always pre-materialised.
//
// Task-liveness gate (#852): the in-CLI cmd/kuke/shared.GuardCellTaskLiveness
// catches the common case, but any consumer that bypasses the CLI (raw
// RPC clients, scripts, in-process callers in alternative branches) would
// otherwise receive a socket path backed by nothing whenever the target
// container's task is not Running. Refuse with ErrAttachTaskNotRunning
// instead, mirroring the client-side guard's predicate at the server
// boundary so every caller — CLI or not — gets the same typed refusal.
func (c *Client) AttachContainer(_ context.Context, doc v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
	container, err := c.resolveAttachable(doc)
	if err != nil {
		return kukeonv1.AttachContainerResult{}, err
	}
	if err := guardAttachableTaskLiveness(container); err != nil {
		return kukeonv1.AttachContainerResult{}, err
	}
	spec := container.Spec
	// Heal a wrong-mode live tty socket in place before handing back its
	// path (#1169). `kuke run` against an already-Ready cell short-circuits
	// to attach without re-entering StartCell, so the #935 StartCell-skip
	// heal never runs on the path run/attach take: a socket an old kuketty
	// bound 0o640 (group-read only) stays undialable forever, while
	// `kuke restart` heals it via its stop+start rebind. Re-chmod'ing the
	// live inode here closes that asymmetry without bouncing the workload or
	// re-entering the #630-prone start-on-running path. Best-effort and
	// idempotent: a socket already at 0o660 is left untouched.
	c.ctrl.ReapplyAttachableSocketPerms(spec)
	return kukeonv1.AttachContainerResult{
		HostSocketPath: fs.ContainerSocketSymlinkPath(
			c.ctrl.RunPath(),
			spec.RealmName, spec.SpaceName, spec.StackName, spec.CellName, spec.ID,
		),
	}, nil
}

// guardAttachableTaskLiveness is the server-side task-liveness gate for
// AttachContainer (#852): refuses to hand back a socket path when the
// target container's task is not Running. Extracted so the predicate is
// unit-testable without a controller.Exec round-trip. Mirrors the
// client-side cmd/kuke/shared.GuardCellTaskLiveness predicate at the
// per-attachable-container granularity that's natural at the server
// boundary (the container's own task is what binds the kuketty socket).
func guardAttachableTaskLiveness(container intmodel.Container) error {
	if container.Status.State == intmodel.ContainerStateReady {
		return nil
	}
	// ContainerState ordinals are kept in lockstep with the v1beta1 enum
	// (see modelhub.ContainerState's doc); cast for the String() method.
	state := v1beta1.ContainerState(container.Status.State)
	return fmt.Errorf(
		"%w: container %q in cell %q has state %s",
		errdefs.ErrAttachTaskNotRunning,
		container.Spec.ID,
		container.Spec.CellName,
		state.String(),
	)
}

// ---- Log ----

// LogContainer resolves the host-side path of the per-container output
// stream and returns it via LogContainerResult. The path varies by IO
// model:
//
//   - Attachable containers are sbsh-wrapped, so stdout/stderr land in
//     the sbsh capture file at HostCapturePath (current behavior).
//   - Non-Attachable containers (including kukeond) have the runtime
//     shim write stdout/stderr to HostLogPath via cio.LogFile. The
//     file is shim-owned; kuke just hands the path back so `kuke log`
//     can read it.
//
// Bytes never traverse this RPC — the caller opens the returned path.
func (c *Client) LogContainer(_ context.Context, doc v1beta1.ContainerDoc) (kukeonv1.LogContainerResult, error) {
	internal, _, err := apischeme.NormalizeContainer(doc)
	if err != nil {
		return kukeonv1.LogContainerResult{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.GetContainer(internal)
	if err != nil {
		return kukeonv1.LogContainerResult{}, err
	}
	if !res.ContainerExists {
		return kukeonv1.LogContainerResult{}, errdefs.ErrContainerNotFound
	}
	spec := res.Container.Spec
	if spec.Attachable {
		return kukeonv1.LogContainerResult{
			HostCapturePath: fs.ContainerCapturePath(
				c.ctrl.RunPath(),
				spec.RealmName, spec.SpaceName, spec.StackName, spec.CellName, spec.ID,
			),
		}, nil
	}
	return kukeonv1.LogContainerResult{
		HostLogPath: fs.ContainerLogPath(
			c.ctrl.RunPath(),
			spec.RealmName, spec.SpaceName, spec.StackName, spec.CellName, spec.ID,
		),
	}, nil
}

// resolveAttachable normalizes doc, looks up the container, and returns
// the full intmodel.Container (spec + freshly-queried status) only when
// Attachable=true. It surfaces ErrConversionFailed for malformed docs,
// ErrContainerNotFound when the container does not exist, and
// ErrAttachNotSupported for non-Attachable targets — the invariants both
// AttachContainer and LogContainer require before handing a host path
// back to the caller. AttachContainer also inspects Status.State to gate
// on task liveness (#852); LogContainer ignores Status since reading a
// per-container log file does not require a live task.
func (c *Client) resolveAttachable(doc v1beta1.ContainerDoc) (intmodel.Container, error) {
	internal, _, err := apischeme.NormalizeContainer(doc)
	if err != nil {
		return intmodel.Container{}, fmt.Errorf("%w: %w", errdefs.ErrConversionFailed, err)
	}
	res, err := c.ctrl.GetContainer(internal)
	if err != nil {
		return intmodel.Container{}, err
	}
	if !res.ContainerExists {
		return intmodel.Container{}, errdefs.ErrContainerNotFound
	}
	if !res.Container.Spec.Attachable {
		return intmodel.Container{}, errdefs.ErrAttachNotSupported
	}
	return res.Container, nil
}

// ---- Ping ----

// Ping is a no-op in the in-process client: the controller is alive if this
// method can run. Implemented so *local.Client satisfies kukeonv1.Client.
func (c *Client) Ping(_ context.Context) error {
	return nil
}

// PingVersion returns the empty string for the in-process branch: there
// is no separate daemon to interrogate, and pulling the running
// binary's version here would force this package to import cmd/config,
// reintroducing the autocomplete cycle (cmd/config → internal/client/local).
// `kuke status` only calls PingVersion on the daemon-RPC client; the
// in-process branch implements it just to satisfy the interface.
func (c *Client) PingVersion(_ context.Context) (string, error) {
	return "", nil
}

// Verify interface compliance at compile time.
var _ kukeonv1.Client = (*Client)(nil)
