package kubernetes

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"time"

	mcpv1beta1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1beta1"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"

	"github.com/stacklok/toolhive-registry-server/internal/config"
	"github.com/stacklok/toolhive-registry-server/internal/sync/writer"
)

const (
	defaultRegistryExportAnnotation          = "toolhive.stacklok.dev/registry-export"
	defaultRegistryURLAnnotation             = "toolhive.stacklok.dev/registry-url"
	defaultRegistryDescriptionAnnotation     = "toolhive.stacklok.dev/registry-description"
	defaultRegistryTitleAnnotation           = "toolhive.stacklok.dev/registry-title"
	defaultRegistryToolDefinitionsAnnotation = "toolhive.stacklok.dev/tool-definitions"
	defaultRegistryToolsAnnotation           = "toolhive.stacklok.dev/tools"
	defaultAuthzClaimsAnnotation             = "toolhive.stacklok.dev/authz-claims"

	defaultRequeueAfter = 10 * time.Second

	leaderElectionID = "toolhive-registry-server-leader-election"
)

var (
	// serviceAccountNamespaceFile is the path to the file containing the namespace
	// of the service account running in a Kubernetes pod.
	// This is a variable to allow tests to override it.
	serviceAccountNamespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
)

// hasRequiredRegistryAnnotations checks if the given annotations map contains
// all required annotations for registry export (description and URL).
func hasRequiredRegistryAnnotations(annotations map[string]string) bool {
	if annotations == nil {
		return false
	}
	_, hasDesc := annotations[defaultRegistryDescriptionAnnotation]
	_, hasURL := annotations[defaultRegistryURLAnnotation]
	return hasDesc && hasURL
}

type mcpServerReconcilerOptions struct {
	namespaces       []string
	requeueAfter     time.Duration
	syncWriter       writer.SyncWriter
	registryName     string
	leaderElectionID string
}

// Option is a function that sets an option for the MCPServerReconciler.
type Option func(*mcpServerReconcilerOptions) error

// WithNamespaces sets the namespaces to watch.
func WithNamespaces(namespaces ...string) Option {
	return func(o *mcpServerReconcilerOptions) error {
		if err := validateNamespaces(namespaces); err != nil {
			// Don't wrap error, return it directly
			return err
		}

		nsSet := make(map[string]bool)
		for _, namespace := range namespaces {
			if !nsSet[namespace] {
				nsSet[namespace] = true
			} else {
				return fmt.Errorf("duplicate namespace: %s", namespace)
			}
		}

		if o.namespaces == nil {
			o.namespaces = make([]string, 0)
		}
		o.namespaces = append(o.namespaces, namespaces...)
		return nil
	}
}

// WithCurrentNamespace configures the reconciler to watch the namespace
// of the current Kubernetes pod by reading from the service account namespace file.
func WithCurrentNamespace() Option {
	return func(o *mcpServerReconcilerOptions) error {
		namespace, err := readNamespaceFromFile(serviceAccountNamespaceFile)
		if err != nil {
			return err
		}
		if o.namespaces == nil {
			o.namespaces = make([]string, 0)
		}
		o.namespaces = append(o.namespaces, namespace)
		return nil
	}
}

// readNamespaceFromFile reads a Kubernetes namespace from the specified file path.
// It reads at most 256 bytes since Kubernetes namespace names have a maximum length
// of 253 characters as per DNS subdomain naming rules.
// See: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
func readNamespaceFromFile(path string) (string, error) {
	f, err := os.Open(filepath.Clean(path))
	if err != nil {
		return "", fmt.Errorf("failed to open namespace file: %w", err)
	}
	defer f.Close()

	// Read up to 257 bytes to detect if file exceeds the 256 byte limit
	buf := make([]byte, 257)
	n, err := f.Read(buf)
	if err != nil && !errors.Is(err, io.EOF) {
		return "", fmt.Errorf("failed to read namespace: %w", err)
	}
	if n > 256 {
		return "", fmt.Errorf("namespace file exceeds maximum size of 256 bytes")
	}
	return strings.TrimSpace(string(buf[:n])), nil
}

// WithRequeueAfter sets the requeue after duration.
func WithRequeueAfter(requeueAfter time.Duration) Option {
	return func(o *mcpServerReconcilerOptions) error {
		if requeueAfter <= 0 {
			return fmt.Errorf("requeueAfter must be greater than 0")
		}
		o.requeueAfter = requeueAfter
		return nil
	}
}

// WithSyncWriter sets the sync writer.
func WithSyncWriter(sw writer.SyncWriter) Option {
	return func(o *mcpServerReconcilerOptions) error {
		if sw == nil {
			return fmt.Errorf("sync writer is required")
		}
		o.syncWriter = sw
		return nil
	}
}

// WithRegistryName sets the registry name. This is used to identify the
// registry in the sync writer.
func WithRegistryName(name string) Option {
	return func(o *mcpServerReconcilerOptions) error {
		if name == "" {
			return fmt.Errorf("registry name is required")
		}
		o.registryName = name
		return nil
	}
}

// WithLeaderElectionID sets the leader election lease ID.
// This allows multiple independent deployments in the same namespace
// to use separate leases.
func WithLeaderElectionID(id string) Option {
	return func(o *mcpServerReconcilerOptions) error {
		if id == "" {
			return fmt.Errorf("leader election ID is required")
		}
		o.leaderElectionID = id
		return nil
	}
}

// NewMCPServerReconciler creates a new MCPServerReconciler.
func NewMCPServerReconciler(
	ctx context.Context,
	opts ...Option,
) (ctrl.Manager, error) {
	o := &mcpServerReconcilerOptions{
		namespaces:   []string{},
		requeueAfter: defaultRequeueAfter,
	}

	for _, opt := range opts {
		if err := opt(o); err != nil {
			return nil, err
		}
	}

	if o.syncWriter == nil {
		return nil, fmt.Errorf("sync writer is required")
	}
	if o.registryName == "" {
		return nil, fmt.Errorf("registry name is required")
	}

	// Default leader election ID to the hardcoded constant if not set
	if o.leaderElectionID == "" {
		o.leaderElectionID = leaderElectionID
	}

	// This is validated in the options, so we can safely use the namespaces.
	defaultNamespaces := map[string]cache.Config{}
	for _, namespace := range o.namespaces {
		slog.Info("Watching namespace", "namespace", namespace)
		defaultNamespaces[namespace] = cache.Config{}
	}

	scheme := runtime.NewScheme()
	if err := mcpv1beta1.AddToScheme(scheme); err != nil {
		return nil, fmt.Errorf("failed to add MCPv1beta1 scheme: %w", err)
	}

	options := ctrl.Options{
		Scheme:           scheme,
		LeaderElection:   true,
		LeaderElectionID: o.leaderElectionID,
		// disable metrics server
		Metrics: metricsserver.Options{
			BindAddress: "0",
		},
	}

	if len(defaultNamespaces) > 0 {
		options.Cache = cache.Options{
			DefaultNamespaces: defaultNamespaces,
		}
	}

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
	if err != nil {
		return nil, fmt.Errorf("failed to create manager: %w", err)
	}

	controller := &MCPServerReconciler{
		requeueAfter: o.requeueAfter,
		syncWriter:   o.syncWriter,
		registryName: o.registryName,
	}

	if err := controller.SetupWithManager(mgr); err != nil {
		return nil, fmt.Errorf("failed to setup controller with manager: %w", err)
	}

	go func() {
		if err := mgr.Start(ctx); err != nil {
			slog.Error("Failed to start manager", "error", err)
		}
	}()

	return mgr, nil
}

func validateNamespaces(namespaces []string) error {
	for _, namespace := range namespaces {
		if !config.IsValidDNSSubdomain(namespace) {
			return fmt.Errorf("invalid namespace name: %s", namespace)
		}
	}
	return nil
}
