// 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 shared

import (
	"log/slog"

	"github.com/eminwux/kukeon/cmd/config"
	"github.com/eminwux/kukeon/cmd/types"
	"github.com/eminwux/kukeon/internal/client/local"
	"github.com/eminwux/kukeon/internal/controller"
	"github.com/eminwux/kukeon/internal/errdefs"
	"github.com/eminwux/kukeon/pkg/api/kukeonv1"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

// LoggerFromCmd extracts the slog logger from the Cobra command context.
func LoggerFromCmd(cmd *cobra.Command) (*slog.Logger, error) {
	logger, ok := cmd.Context().Value(types.CtxLogger).(*slog.Logger)
	if !ok || logger == nil {
		return nil, errdefs.ErrLoggerNotFound
	}
	return logger, nil
}

// ControllerFromCmd instantiates a controller.Exec configured with the shared
// persistent flags (run path, containerd socket) used by the parent command.
func ControllerFromCmd(cmd *cobra.Command) (*controller.Exec, error) {
	logger, err := LoggerFromCmd(cmd)
	if err != nil {
		return nil, err
	}

	opts := controller.Options{
		RunPath:          viper.GetString(config.KUKEON_ROOT_RUN_PATH.ViperKey),
		ContainerdSocket: viper.GetString(config.KUKEON_ROOT_CONTAINERD_SOCKET.ViperKey),
	}

	return controller.NewControllerExec(cmd.Context(), logger, opts), nil
}

// GetControllerWithMock is a generic helper to get a controller from context,
// supporting mock injection via a context key. If a mock is found in the context,
// it is returned. Otherwise, a real controller is created using ControllerFromCmd.
// The mockKey should be a unique type used as the context key.
func GetControllerWithMock[T any](
	cmd *cobra.Command,
	mockKey any,
	realController func(*cobra.Command) (T, error),
) (T, error) {
	// Check for mock controller in context
	if mockCtrl, ok := cmd.Context().Value(mockKey).(T); ok {
		return mockCtrl, nil
	}

	// Get real controller
	return realController(cmd)
}

// GetControllerWithMockWrapper is a convenience function that wraps GetControllerWithMock
// to use ControllerFromCmd as the real controller factory.
func GetControllerWithMockWrapper[T any](cmd *cobra.Command, mockKey any, wrapper func(*controller.Exec) T) (T, error) {
	var zero T

	// Check for mock controller in context
	if mockCtrl, ok := cmd.Context().Value(mockKey).(T); ok {
		return mockCtrl, nil
	}

	// Get real controller and wrap it
	realCtrl, err := ControllerFromCmd(cmd)
	if err != nil {
		return zero, err
	}

	return wrapper(realCtrl), nil
}

// ClientFromCmd returns a kukeonv1.Client selected by the
// `kukeon/noDaemon` viper key, which is fed (in precedence order) by:
//   - `--no-daemon` on the commands that still expose it: `init`,
//     `uninstall`, `purge`, and every `get <kind>` (see #222; the get
//     kinds were retained per a user override on the original AC).
//   - `KUKEON_NO_DAEMON=true` env var.
//   - `applyRunPathImpliesNoDaemon`, which sets the key when `--run-path`
//     is explicit and `--no-daemon` was not.
//
// True → in-process Client backed by a fresh controller.Exec (requires
// privileges). False → JSON-RPC Client dialing KUKEON_HOST (unix:///...
// today). The caller owns the returned Client and must Close it.
//
// Workload commands (apply, create *, run, attach, delete *, kill *) no
// longer route through here — #566 routed them through DaemonClientFromCmd
// so the in-process branch is unreachable from the workload CLI surface.
// Surviving callers: log, refresh, start, stop, doctor/cgroups (in-process
// reachable only via the `--run-path` promotion since they don't expose
// `--no-daemon`), plus get *, purge *, init, uninstall (in-process
// reachable via either the explicit `--no-daemon` flag or the promotion).
func ClientFromCmd(cmd *cobra.Command) (kukeonv1.Client, error) {
	if viper.GetBool(config.KUKEON_ROOT_NO_DAEMON.ViperKey) {
		logger, err := LoggerFromCmd(cmd)
		if err != nil {
			return nil, err
		}
		return local.New(cmd.Context(), logger, controller.Options{
			RunPath:          viper.GetString(config.KUKEON_ROOT_RUN_PATH.ViperKey),
			ContainerdSocket: viper.GetString(config.KUKEON_ROOT_CONTAINERD_SOCKET.ViperKey),
		}), nil
	}

	return dialDaemon(cmd)
}

// DaemonClientFromCmd returns a kukeonv1.Client that always dials kukeond
// (RPC only — never the in-process branch). #566 phase 3 routes every
// workload command (apply, create *, run, attach, delete *, kill *)
// through here so the user-facing CLI surface for those verbs has no
// in-process escape hatch even when `--run-path` (which would otherwise
// promote `KUKEON_NO_DAEMON=true` via applyRunPathImpliesNoDaemon) is set.
// The promotion still fires for the surviving ClientFromCmd callers
// enumerated above; for the workload commands it is now a no-op because
// this helper ignores the `kukeon/noDaemon` viper key.
//
// The caller owns the returned Client and must Close it.
func DaemonClientFromCmd(cmd *cobra.Command) (kukeonv1.Client, error) {
	return dialDaemon(cmd)
}

// dialDaemon resolves the configured kukeond endpoint (KUKEON_HOST, with
// fallback to the default) and dials it. Shared by ClientFromCmd's RPC
// branch and DaemonClientFromCmd.
func dialDaemon(cmd *cobra.Command) (kukeonv1.Client, error) {
	host := viper.GetString(config.KUKEON_ROOT_HOST.ViperKey)
	if host == "" {
		host = config.KUKEON_ROOT_HOST.Default
	}
	return kukeonv1.Dial(cmd.Context(), host)
}
