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

import (
	"context"
	"log/slog"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"
	"time"

	"github.com/eminwux/kukeon/cmd/config"
	"github.com/eminwux/kukeon/internal/controller"
	"github.com/eminwux/kukeon/internal/daemon"
	"github.com/eminwux/kukeon/internal/instance"
	"github.com/eminwux/kukeon/internal/logging"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

// Socket modes applied to the kukeond unix listener. The narrow mode is the
// fallback when no SocketGID was passed (root-only access); the group-readable
// mode is used when init plumbed a kukeon GID through `--socket-gid` so a
// non-root operator in the kukeon group can dial the daemon after restart.
const (
	socketModeRootOnly      os.FileMode = 0o600
	socketModeGroupReadable os.FileMode = 0o660
)

func newServeCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:          "serve",
		Short:        "Run the kukeond daemon in the foreground",
		SilenceUsage: true,
		RunE:         runServe,
	}
	return cmd
}

func runServe(cmd *cobra.Command, _ []string) error {
	logLevel := viper.GetString(config.KUKEON_ROOT_LOG_LEVEL.ViperKey)
	if logLevel == "" {
		logLevel = "info"
	}
	levelVar := new(slog.LevelVar)
	levelVar.Set(logging.ParseLevel(logLevel))
	handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: levelVar})
	logger := slog.New(handler)

	ctx, stop := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	socketPath := viper.GetString(config.KUKEOND_SOCKET.ViperKey)
	if socketPath == "" {
		socketPath = config.KUKEOND_SOCKET.Default
	}
	runPath := viper.GetString(config.KUKEON_ROOT_RUN_PATH.ViperKey)
	if runPath == "" {
		runPath = config.DefaultRunPath()
	}

	socketGID := viper.GetInt(config.KUKEOND_SOCKET_GID.ViperKey)
	socketMode := socketModeRootOnly
	if socketGID > 0 {
		socketMode = socketModeGroupReadable
	}

	if mismatchErr := instance.VerifyOrWrite(
		runPath,
		viper.GetString(config.KUKEON_ROOT_NAMESPACE_SUFFIX.ViperKey),
		viper.GetString(config.KUKEON_ROOT_CGROUP_ROOT.ViperKey),
	); mismatchErr != nil {
		logger.ErrorContext(ctx, "instance metadata check failed", "error", mismatchErr)
		return mismatchErr
	}

	reconcileInterval := parseReconcileInterval(logger, cmd.Context())

	defaultMemoryLimitBytes := viper.GetInt64(config.KUKEOND_DEFAULT_MEMORY_LIMIT_BYTES.ViperKey)
	if defaultMemoryLimitBytes < 0 {
		// A negative value is meaningless for memory.max — clamp to zero so a
		// typo doesn't get plumbed down to oci.WithMemoryLimit, which takes a
		// uint64 and would silently wrap.
		logger.WarnContext(ctx,
			"default-memory-limit-bytes is negative; treating as 0 (disabled)",
			"value", defaultMemoryLimitBytes,
		)
		defaultMemoryLimitBytes = 0
	}

	kukettyLogLevel := viper.GetString(config.KUKEOND_KUKETTY_LOG_LEVEL.ViperKey)
	if kukettyLogLevel == "" {
		kukettyLogLevel = config.KUKEOND_KUKETTY_LOG_LEVEL.Default
	}

	diskPressureWarnPct := viper.GetInt(config.KUKEOND_DISK_PRESSURE_WARN_PCT.ViperKey)
	diskPressureBlockPct := viper.GetInt(config.KUKEOND_DISK_PRESSURE_BLOCK_PCT.ViperKey)

	opts := daemon.Options{
		SocketPath:        socketPath,
		SocketMode:        socketMode,
		SocketGID:         socketGID,
		PIDFile:           filepath.Join(filepath.Dir(socketPath), "kukeond.pid"),
		ReconcileInterval: reconcileInterval,
		Controller: controller.Options{
			RunPath:          runPath,
			ContainerdSocket: viper.GetString(config.KUKEON_ROOT_CONTAINERD_SOCKET.ViperKey),
			// Forward the kukeon group GID into the runner so per-container
			// Attachable tty directories created by daemon-mediated apply/start
			// inherit the same group-traversal layout `kuke init` applies to
			// /opt/kukeon. Without this the runner sees KukeonGroupGID=0 and
			// falls back to 0700 root-only — regressing #258 repro A under the
			// daemon path even when --socket-gid was set.
			KukeondSocketGID:        socketGID,
			DefaultMemoryLimitBytes: defaultMemoryLimitBytes,
			// Daemon-wide kuketty log-level fallback. The renderer uses it
			// when a cell omits per-container Tty.LogLevel; empty here would
			// trip sbsh's NewFileLogger inside the container at attach time
			// (issue #599), so the empty-resolved case above pins "info".
			KukettyLogLevel: kukettyLogLevel,
			// Data-volume disk-pressure observability (reconcile-loop WARN) and
			// the CreateCell guard (issue #1035). Both are observation-only —
			// the guard refuses new cell creation but never deletes data.
			DiskPressureWarnPercent:  diskPressureWarnPct,
			DiskPressureBlockPercent: diskPressureBlockPct,
		},
	}

	server := daemon.NewServer(ctx, logger, opts)

	serveErr := make(chan error, 1)
	go func() { serveErr <- server.Serve() }()

	select {
	case <-ctx.Done():
		logger.InfoContext(ctx, "received shutdown signal", "signal", ctx.Err())
	case err := <-serveErr:
		if err != nil {
			logger.ErrorContext(cmd.Context(), "serve exited with error", "error", err)
			_ = server.Stop()
			return err
		}
	}

	if err := server.Stop(); err != nil {
		logger.ErrorContext(cmd.Context(), "stop error", "error", err)
		return err
	}
	logger.InfoContext(cmd.Context(), "kukeond stopped")
	return nil
}

// parseReconcileInterval reads the resolved reconcile-interval string out of
// viper and parses it as a Go time.Duration. An empty value or a parse
// failure logs a warning and falls back to the in-binary default so a typo
// in the YAML never blocks the daemon from starting. A zero or negative
// duration disables the loop on purpose — Server.startReconcileLoop honors
// that and emits a "reconcile loop disabled" line.
func parseReconcileInterval(logger *slog.Logger, ctx context.Context) time.Duration {
	raw := viper.GetString(config.KUKEOND_RECONCILE_INTERVAL.ViperKey)
	if raw == "" {
		raw = config.KUKEOND_RECONCILE_INTERVAL.Default
	}
	d, err := time.ParseDuration(raw)
	if err != nil {
		fallback, _ := time.ParseDuration(config.KUKEOND_RECONCILE_INTERVAL.Default)
		logger.WarnContext(ctx,
			"invalid reconcile-interval; falling back to default",
			"value", raw, "error", err, "fallback", fallback)
		return fallback
	}
	return d
}
