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

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"

	"github.com/eminwux/kukeon/cmd/config"
	applycmd "github.com/eminwux/kukeon/cmd/kuke/apply"
	attachcmd "github.com/eminwux/kukeon/cmd/kuke/attach"
	autocompletecmd "github.com/eminwux/kukeon/cmd/kuke/autocomplete"
	buildcmd "github.com/eminwux/kukeon/cmd/kuke/build"
	createcmd "github.com/eminwux/kukeon/cmd/kuke/create"
	daemoncmd "github.com/eminwux/kukeon/cmd/kuke/daemon"
	deletecmd "github.com/eminwux/kukeon/cmd/kuke/delete"
	doctorcmd "github.com/eminwux/kukeon/cmd/kuke/doctor"
	getcmd "github.com/eminwux/kukeon/cmd/kuke/get"
	imagecmd "github.com/eminwux/kukeon/cmd/kuke/image"
	initcmd "github.com/eminwux/kukeon/cmd/kuke/init"
	killcmd "github.com/eminwux/kukeon/cmd/kuke/kill"
	logcmd "github.com/eminwux/kukeon/cmd/kuke/log"
	purgecmd "github.com/eminwux/kukeon/cmd/kuke/purge"
	refreshcmd "github.com/eminwux/kukeon/cmd/kuke/refresh"
	restartcmd "github.com/eminwux/kukeon/cmd/kuke/restart"
	runcmd "github.com/eminwux/kukeon/cmd/kuke/run"
	startcmd "github.com/eminwux/kukeon/cmd/kuke/start"
	statuscmd "github.com/eminwux/kukeon/cmd/kuke/status"
	stopcmd "github.com/eminwux/kukeon/cmd/kuke/stop"
	teamcmd "github.com/eminwux/kukeon/cmd/kuke/team"
	uninstallcmd "github.com/eminwux/kukeon/cmd/kuke/uninstall"
	"github.com/eminwux/kukeon/cmd/kuke/version"
	"github.com/eminwux/kukeon/cmd/types"
	"github.com/eminwux/kukeon/internal/clientconfig"
	"github.com/eminwux/kukeon/internal/cni"
	"github.com/eminwux/kukeon/internal/consts"
	"github.com/eminwux/kukeon/internal/errdefs"
	"github.com/eminwux/kukeon/internal/logging"
	v1beta1 "github.com/eminwux/kukeon/pkg/api/model/v1beta1"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

type ConfigLoader interface {
	LoadConfig() error
}

// MockConfigLoaderKey is used to inject mock config loaders in tests via context.
type MockConfigLoaderKey struct{}

func NewKukeCmd() (*cobra.Command, error) {
	cmd := &cobra.Command{
		Use:   "kuke",
		Short: "Kukeon is a tool for managing Kukeon entities",
		PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
			if err := loadClientConfiguration(cmd); err != nil {
				return err
			}

			var logger *slog.Logger
			if viper.GetBool(config.KUKEON_ROOT_VERBOSE.ViperKey) {
				logLevel := viper.GetString(config.KUKEON_ROOT_LOG_LEVEL.ViperKey)
				if logLevel == "" {
					logLevel = "info"
				}

				// Create a new logger that writes to the file with the specified log level
				levelVar := new(slog.LevelVar)
				levelVar.Set(logging.ParseLevel(logLevel))

				textHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: levelVar})
				handler := &logging.ReformatHandler{Inner: textHandler, Writer: os.Stdout}
				logger = slog.New(handler)

				// Store both logger and levelVar in context using struct keys
				ctx := cmd.Context()
				ctx = context.WithValue(ctx, types.CtxLogger, logger)
				ctx = context.WithValue(ctx, types.CtxLevelVar, &levelVar)
				ctx = context.WithValue(ctx, types.CtxHandler, handler)
				cmd.SetContext(ctx)
				logger.DebugContext(
					cmd.Context(),
					"enabling verbose",
					"log-level",
					viper.GetString(config.KUKEON_ROOT_LOG_LEVEL.ViperKey),
				)
			}

			// Check for mock config loader in context (for testing)
			var loader ConfigLoader
			if mockLoader, ok := cmd.Context().Value(MockConfigLoaderKey{}).(ConfigLoader); ok {
				loader = mockLoader
			} else {
				loader = &realConfigLoader{}
			}

			err := loader.LoadConfig()
			if err != nil {
				// Only log if logger was created (verbose mode)
				if logger != nil {
					logger.DebugContext(cmd.Context(), "config error", "error", err)
				}
				return fmt.Errorf("%w: %w", errdefs.ErrConfig, err)
			}

			rebindNoDaemonViperToLeaf(cmd)
			applyRunPathImpliesNoDaemon(cmd)
			applyRunPathImpliesKukeondSocket(cmd)
			return nil
		},
		Run: func(cmd *cobra.Command, _ []string) {
			_ = cmd.Help()
		},
	}

	// Wire the root to real streams so cobra's cmd.Print* helpers route
	// non-error output to stdout. Without this, cobra's OutOrStderr() falls
	// back to os.Stderr and every cmd.Print* in the tree lands on stderr,
	// breaking `out=$(kuke …)` and `kuke … | xargs …`. Subcommands inherit
	// these via cobra's getOut/getErr.
	cmd.SetOut(os.Stdout)
	cmd.SetErr(os.Stderr)

	if err := SetupKukeCmd(cmd); err != nil {
		return nil, fmt.Errorf("failed to setup kukeon command: %w", err)
	}

	return cmd, nil
}

func SetupKukeCmd(rootCmd *cobra.Command) error {
	rootCmd.AddCommand(initcmd.NewInitCmd())
	rootCmd.AddCommand(applycmd.NewApplyCmd())
	rootCmd.AddCommand(createcmd.NewCreateCmd())
	rootCmd.AddCommand(buildcmd.NewBuildCmd())
	rootCmd.AddCommand(daemoncmd.NewDaemonCmd())
	rootCmd.AddCommand(getcmd.NewGetCmd())
	rootCmd.AddCommand(deletecmd.NewDeleteCmd())
	rootCmd.AddCommand(doctorcmd.NewDoctorCmd())
	rootCmd.AddCommand(startcmd.NewStartCmd())
	rootCmd.AddCommand(statuscmd.NewStatusCmd())
	rootCmd.AddCommand(stopcmd.NewStopCmd())
	rootCmd.AddCommand(teamcmd.NewTeamCmd())
	rootCmd.AddCommand(killcmd.NewKillCmd())
	rootCmd.AddCommand(purgecmd.NewPurgeCmd())
	rootCmd.AddCommand(refreshcmd.NewRefreshCmd())
	rootCmd.AddCommand(restartcmd.NewRestartCmd())
	rootCmd.AddCommand(runcmd.NewRunCmd())
	rootCmd.AddCommand(attachcmd.NewAttachCmd())
	rootCmd.AddCommand(logcmd.NewLogCmd())
	rootCmd.AddCommand(autocompletecmd.NewAutocompleteCmd())
	rootCmd.AddCommand(imagecmd.NewImageCmd())
	rootCmd.AddCommand(uninstallcmd.NewUninstallCmd())
	rootCmd.AddCommand(version.NewVersionCmd())

	// Persistent flags
	if err := SetPersistentLoggingFlags(rootCmd); err != nil {
		return err
	}

	// Bind Non-persistent Flags to Viper
	if err := SetFlags(rootCmd); err != nil {
		return err
	}

	return nil
}

func SetPersistentLoggingFlags(rootCmd *cobra.Command) error {
	rootCmd.PersistentFlags().String(
		"configuration", config.DefaultClientConfigurationFile(),
		"Path to a ClientConfiguration YAML; absent file uses hardcoded defaults",
	)
	if err := viper.BindPFlag(
		config.KUKE_CONFIGURATION.ViperKey,
		rootCmd.PersistentFlags().Lookup("configuration"),
	); err != nil {
		return err
	}

	rootCmd.PersistentFlags().String("run-path", "/opt/kukeon", "Run path for the kukeon runtime")
	if err := viper.BindPFlag(config.KUKEON_ROOT_RUN_PATH.ViperKey, rootCmd.PersistentFlags().Lookup("run-path")); err != nil {
		return err
	}

	rootCmd.PersistentFlags().String("containerd-socket", "/run/containerd/containerd.sock", "containerd socket file")
	if err := viper.BindPFlag(config.KUKEON_ROOT_CONTAINERD_SOCKET.ViperKey, rootCmd.PersistentFlags().Lookup("containerd-socket")); err != nil {
		return err
	}

	rootCmd.PersistentFlags().String(
		"host", config.KUKEON_ROOT_HOST.Default,
		"kukeond endpoint (unix:///path or ssh://user@host)",
	)
	if err := viper.BindPFlag(config.KUKEON_ROOT_HOST.ViperKey, rootCmd.PersistentFlags().Lookup("host")); err != nil {
		return err
	}

	rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging")
	if err := viper.BindPFlag(config.KUKEON_ROOT_VERBOSE.ViperKey, rootCmd.PersistentFlags().Lookup("verbose")); err != nil {
		return err
	}

	rootCmd.PersistentFlags().String("log-level", "", "Log level (debug, info, warn, error)")
	if err := viper.BindPFlag(config.KUKEON_ROOT_LOG_LEVEL.ViperKey, rootCmd.PersistentFlags().Lookup("log-level")); err != nil {
		return err
	}

	return nil
}

func SetFlags(_ *cobra.Command) error {
	// rootCmd.Flags().String("terminal-id", "", "Optional terminal ID (random if omitted)")
	// if err := viper.BindPFlag(config.KUKEON_ROOT_TERM_ID.ViperKey, rootCmd.Flags().Lookup("terminal-id")); err != nil {
	// 	return err
	// }

	return nil
}

type realConfigLoader struct{}

func (r *realConfigLoader) LoadConfig() error {
	return loadConfig()
}

func loadConfig() error {
	_ = config.KUKEON_ROOT_HOST.BindEnv()
	_ = config.KUKEON_ROOT_CONTAINERD_SOCKET.BindEnv()

	_ = config.KUKEOND_SOCKET.BindEnv()
	_ = config.KUKEOND_SOCKET_GID.BindEnv()

	// `--no-daemon` is no longer a root-persistent flag (#222) — only the
	// commands that still accept it user-facing register it locally: `init`,
	// `uninstall`, `purge` (persistent on the parent), and the parent `get`
	// command (persistent — every `get <kind>` inherits it per the user
	// override on #222). The env binding is what keeps `KUKEON_NO_DAEMON=true`
	// reaching viper for callers that never go through one of those flag
	// instances (e.g. the `--run-path` promotion path and the
	// `KUKEON_NO_DAEMON=false` envSet check).
	_ = config.KUKEON_ROOT_NO_DAEMON.BindEnv()

	_ = config.KUKEON_ROOT_RUN_PATH.BindEnv()
	if viper.GetString(config.KUKEON_ROOT_RUN_PATH.ViperKey) == "" {
		viper.Set(config.KUKEON_ROOT_RUN_PATH.ViperKey, config.DefaultRunPath())
	}

	_ = config.KUKEON_ROOT_LOG_LEVEL.BindEnv()
	if viper.GetString(config.KUKEON_ROOT_LOG_LEVEL.ViperKey) == "" {
		viper.Set(config.KUKEON_ROOT_LOG_LEVEL.ViperKey, "info")
	}

	_ = config.KUKE_GET_OUTPUT.BindEnv()

	return nil
}

// LoadConfig is a public wrapper for backward compatibility.
func LoadConfig() error {
	return loadConfig()
}

// loadClientConfiguration reads the ClientConfiguration document at
// `--configuration` (default `~/.kuke/kuke.yaml`) and layers its values onto
// viper for fields the operator did not set explicitly. Called from the root
// PersistentPreRunE so every subcommand sees the seeded defaults before
// reading viper.
func loadClientConfiguration(cmd *cobra.Command) error {
	path := viper.GetString(config.KUKE_CONFIGURATION.ViperKey)
	if path == "" {
		path = config.DefaultClientConfigurationFile()
	}
	// First-run dump: when the operator did not pick a custom --configuration,
	// write the commented defaults to the well-known path so a fresh host has a
	// template to edit. Skipped silently on permission/IO failures — kuke must
	// still run with hardcoded defaults.
	maybeWriteClientConfigurationDefault(cmd, path)
	doc, err := clientconfig.Load(path)
	if err != nil {
		return fmt.Errorf("load client configuration: %w", err)
	}
	applyClientConfiguration(cmd, doc.Spec)
	// Mirror the daemon-side LoadServerConfigurationFromFlag: drive
	// consts.ConfigureRuntime from the configured (or default) suffix and
	// cgroup root so --no-daemon workload paths read the same realm
	// namespaces and cgroup tree the daemon side does. Admin verbs run
	// LoadServerConfigurationFromFlag in their RunE; their later
	// ConfigureRuntime call wins for those paths.
	suffix := viper.GetString(config.KUKEON_ROOT_NAMESPACE_SUFFIX.ViperKey)
	if suffix == "" {
		suffix = config.KUKEON_ROOT_NAMESPACE_SUFFIX.Default
	}
	cgroupRoot := viper.GetString(config.KUKEON_ROOT_CGROUP_ROOT.ViperKey)
	if cgroupRoot == "" {
		cgroupRoot = config.KUKEON_ROOT_CGROUP_ROOT.Default
	}
	if cfgErr := consts.ConfigureRuntime(suffix, cgroupRoot); cfgErr != nil {
		return fmt.Errorf("configure runtime: %w", cfgErr)
	}
	podSubnetCIDR := viper.GetString(config.KUKEON_ROOT_POD_SUBNET_CIDR.ViperKey)
	if podSubnetCIDR == "" {
		podSubnetCIDR = config.KUKEON_ROOT_POD_SUBNET_CIDR.Default
	}
	if cfgErr := cni.ConfigureSubnetParentCIDR(podSubnetCIDR); cfgErr != nil {
		return fmt.Errorf("configure pod subnet CIDR: %w", cfgErr)
	}
	return nil
}

// maybeWriteClientConfigurationDefault writes the commented default
// ClientConfiguration when the resolved --configuration path equals the
// project default. The path-equality guard is the only filter: pointing
// --configuration at a custom path opts out of the default-location dump
// (the path won't equal the project default), while pointing it at the
// default path is a no-op overwrite-guarded by O_EXCL inside WriteDefault.
// Errors are intentionally swallowed: kuke must still run when $HOME is
// read-only or the parent directory cannot be created.
func maybeWriteClientConfigurationDefault(_ *cobra.Command, path string) {
	if path != config.DefaultClientConfigurationFile() {
		return
	}
	_, _ = clientconfig.WriteDefault(path)
}

// applyClientConfiguration layers the loaded ClientConfiguration on top of
// viper for fields the operator did not explicitly set on the command line
// or via environment. Order of precedence: explicit `--flag` > env >
// ClientConfiguration > flag default. The flag check skips fields whose
// `--flag` was changed; the env check skips fields whose env var is set —
// without it, `viper.Set` would override viper's env binding and silently
// invert env > YAML.
func applyClientConfiguration(cmd *cobra.Command, spec v1beta1.ClientConfigurationSpec) {
	if spec.Host != "" && !flagChanged(cmd, "host") && !envSet(config.KUKEON_ROOT_HOST) {
		viper.Set(config.KUKEON_ROOT_HOST.ViperKey, spec.Host)
	}
	if spec.RunPath != "" && !flagChanged(cmd, "run-path") && !envSet(config.KUKEON_ROOT_RUN_PATH) {
		viper.Set(config.KUKEON_ROOT_RUN_PATH.ViperKey, spec.RunPath)
	}
	if spec.ContainerdSocket != "" && !flagChanged(cmd, "containerd-socket") &&
		!envSet(config.KUKEON_ROOT_CONTAINERD_SOCKET) {
		viper.Set(config.KUKEON_ROOT_CONTAINERD_SOCKET.ViperKey, spec.ContainerdSocket)
	}
	if spec.LogLevel != "" && !flagChanged(cmd, "log-level") && !envSet(config.KUKEON_ROOT_LOG_LEVEL) {
		viper.Set(config.KUKEON_ROOT_LOG_LEVEL.ViperKey, spec.LogLevel)
	}
	if spec.ContainerdNamespaceSuffix != "" &&
		!flagChanged(cmd, "containerd-namespace-suffix") &&
		!envSet(config.KUKEON_ROOT_NAMESPACE_SUFFIX) {
		viper.Set(config.KUKEON_ROOT_NAMESPACE_SUFFIX.ViperKey, spec.ContainerdNamespaceSuffix)
	}
	if spec.CgroupRoot != "" &&
		!flagChanged(cmd, "cgroup-root") &&
		!envSet(config.KUKEON_ROOT_CGROUP_ROOT) {
		viper.Set(config.KUKEON_ROOT_CGROUP_ROOT.ViperKey, spec.CgroupRoot)
	}
	if spec.PodSubnetCIDR != "" &&
		!flagChanged(cmd, "pod-subnet-cidr") &&
		!envSet(config.KUKEON_ROOT_POD_SUBNET_CIDR) {
		viper.Set(config.KUKEON_ROOT_POD_SUBNET_CIDR.ViperKey, spec.PodSubnetCIDR)
	}
}

// rebindNoDaemonViperToLeaf rebinds the `kukeon/noDaemon` viper key to the
// `--no-daemon` flag on the leaf cmd being executed, if that cmd has one.
// Required because #222 demoted `--no-daemon` from a single root-persistent
// flag to per-command local flags registered on init/uninstall/purge/get
// realm — viper.BindPFlag has last-bind-wins semantics, so binding at
// command-tree construction time would leave viper pointing at whichever
// command happened to register last and silently drop the actual leaf's
// flag value. Rebinding here, at PreRun time, makes the binding follow the
// command actually being executed.
//
// Cobra merges inherited persistent flags into Flags() before PersistentPreRunE
// fires, so cmd.Flags().Lookup picks up both local registrations (init's own
// flag) and persistent ones inherited from a parent (purge's parent flag
// reaching purge realm). Commands without `--no-daemon` (apply, create, run,
// attach, delete, kill, start, stop, log, refresh, image *, daemon *) get
// nil from Lookup and viper falls back to the env binding from loadConfig
// (KUKEON_NO_DAEMON) and the Set override from applyRunPathImpliesNoDaemon
// below. Of those, the workload commands (apply, create *, run, attach,
// delete *, kill *) ignore the viper key entirely after #566 — they go
// through DaemonClientFromCmd, which always dials kukeond — so for them
// the rebind path is a no-op even when `KUKEON_NO_DAEMON=true` or the
// `--run-path` promotion lands.
func rebindNoDaemonViperToLeaf(cmd *cobra.Command) {
	if flag := cmd.Flags().Lookup("no-daemon"); flag != nil {
		_ = viper.BindPFlag(config.KUKEON_ROOT_NO_DAEMON.ViperKey, flag)
	}
}

// applyRunPathImpliesNoDaemon promotes an explicit `--run-path` (flag or
// `KUKEON_RUN_PATH` env) into `--no-daemon=true` when the operator did not
// set `--no-daemon` themselves. The daemon ignores the client's run-path
// (it serves whatever path it was started with), so the only way for a
// caller-supplied `--run-path` to take effect is in-process. Without this
// promotion, passing `--run-path /tmp/foo` silently dials the default
// daemon socket and reads/writes someone else's run-path — the failure
// mode that broke 40/75 e2e tests under per-test `--run-path` isolation.
//
// YAML-configured run-path does not trip this promotion: contributors
// with a non-default ClientConfiguration shouldn't have their daemon
// access silently disabled. Flag/env are explicit per-invocation intent;
// YAML is ambient configuration.
//
// Surviving callers after #566 (workload commands no longer consult the
// `kukeon/noDaemon` viper key — they route through DaemonClientFromCmd):
//   - init, uninstall, purge *, get * — expose `--no-daemon` user-facing
//     and inherit the promotion when the operator passes `--run-path`
//     without `--no-daemon`.
//   - log, refresh, start *, stop *, doctor/cgroups — no `--no-daemon`
//     flag at the CLI surface; the promotion is the only path that
//     reaches the in-process branch in shared.ClientFromCmd for these.
//
// For the workload commands (apply, create *, run, attach, delete *,
// kill *) the promotion still fires (the PreRun hook is unconditional),
// but the resulting viper bit is now inert because DaemonClientFromCmd
// does not read it.
func applyRunPathImpliesNoDaemon(cmd *cobra.Command) {
	runPathExplicit := flagChanged(cmd, "run-path") || envSet(config.KUKEON_ROOT_RUN_PATH)
	noDaemonExplicit := flagChanged(cmd, "no-daemon") || envSet(config.KUKEON_ROOT_NO_DAEMON)
	if runPathExplicit && !noDaemonExplicit {
		viper.Set(config.KUKEON_ROOT_NO_DAEMON.ViperKey, true)
	}
}

// applyRunPathImpliesKukeondSocket derives the daemon's listen socket as
// `<runPath>/kukeond.sock` when the operator passed `--run-path` explicitly
// (flag or KUKEON_RUN_PATH env) but did not pin KUKEOND_SOCKET themselves.
// Promoted to root-PersistentPreRunE per #570 so non-init verbs that also
// resolve the socket (notably `kuke daemon reset`, plus the other daemon
// lifecycle verbs and any caller dialing kukeond) honor the same
// `--run-path → <X>/kukeond.sock` contract `kuke init` does — without this,
// `kuke init --run-path X` lays the socket under X but `kuke daemon reset
// --run-path X` cleans the default `/run/kukeon/`, an operator footgun for
// custom run-paths outside `t.TempDir()`.
//
// Init's `applyServerConfiguration` still wins for `spec.Socket` (it runs
// later inside runInit and unconditionally viper.Set's when env is unset),
// so YAML-configured socket paths remain authoritative for `kuke init`.
//
// KUKEOND_SOCKET is registered via DefineKVNoViperDefault (cmd/config/env.go)
// precisely so this viper.IsSet check stays meaningful — viper.SetDefault
// would trip IsSet even with no env/flag/YAML and silently disable
// derivation.
func applyRunPathImpliesKukeondSocket(cmd *cobra.Command) {
	if viper.IsSet(config.KUKEOND_SOCKET.ViperKey) {
		return
	}
	if !flagChanged(cmd, "run-path") && !envSet(config.KUKEON_ROOT_RUN_PATH) {
		return
	}
	runPath := viper.GetString(config.KUKEON_ROOT_RUN_PATH.ViperKey)
	if runPath == "" {
		runPath = config.DefaultRunPath()
	}
	viper.Set(config.KUKEOND_SOCKET.ViperKey, filepath.Join(runPath, "kukeond.sock"))
}

// flagChanged checks both the local and persistent flag sets so the helper
// is correct in tests (where cmd is the root and persistent flags are not
// yet merged into cmd.Flags()) and in production (where cmd is the leaf
// subcommand and the merged set already contains the parent's persistent
// flags).
func flagChanged(cmd *cobra.Command, name string) bool {
	if f := cmd.Flags().Lookup(name); f != nil && f.Changed {
		return true
	}
	if f := cmd.PersistentFlags().Lookup(name); f != nil && f.Changed {
		return true
	}
	return false
}

// envSet reports whether the OS env var backing v is present (any value,
// including empty string, counts as set — same semantics as viper's BindEnv).
func envSet(v config.Var) bool {
	_, ok := os.LookupEnv(v.EnvVar())
	return ok
}
