// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

// Package health provides health monitoring for vMCP backend MCP servers.
//
// This package implements the HealthChecker interface and provides periodic
// health monitoring with configurable intervals and failure thresholds.
package health

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"time"

	"github.com/stacklok/toolhive/pkg/vmcp"
	authtypes "github.com/stacklok/toolhive/pkg/vmcp/auth/types"
)

// healthChecker implements vmcp.HealthChecker using ListCapabilities as the health check.
type healthChecker struct {
	// client is the backend client used to communicate with backends.
	client vmcp.BackendClient

	// timeout is the timeout for health check operations.
	timeout time.Duration

	// degradedThreshold is the response time threshold for marking a backend as degraded.
	// If a health check succeeds but takes longer than this duration, the backend is marked degraded.
	// Zero means disabled (backends will never be marked degraded based on response time alone).
	degradedThreshold time.Duration
}

// NewHealthChecker creates a new health checker that uses BackendClient.ListCapabilities
// as the health check mechanism. This validates the full MCP communication stack:
// network connectivity, MCP protocol compliance, authentication, and responsiveness.
//
// Parameters:
//   - client: BackendClient for communicating with backend MCP servers
//   - timeout: Maximum duration for health check operations (0 = no timeout)
//   - degradedThreshold: Response time threshold for marking backend as degraded (0 = disabled)
//
// Returns a new HealthChecker implementation.
func NewHealthChecker(
	client vmcp.BackendClient,
	timeout time.Duration,
	degradedThreshold time.Duration,
) vmcp.HealthChecker {
	return &healthChecker{
		client:            client,
		timeout:           timeout,
		degradedThreshold: degradedThreshold,
	}
}

// CheckHealth performs a health check on a backend by calling ListCapabilities.
// This validates the full MCP communication stack and returns the backend's health status.
//
// Health determination logic:
//   - Success with fast response: Backend is healthy (BackendHealthy)
//   - Success with slow response (> degradedThreshold): Backend is degraded (BackendDegraded)
//   - Authentication error (HTTP 401/403) AND backend has an outgoing auth strategy
//     configured: Backend is healthy (BackendHealthy). Health probes deliberately do
//     not carry user credentials, so the backend's auth challenge proves reachability
//     and a working auth layer — that is success for probe purposes.
//   - Authentication error AND backend has no outgoing auth strategy configured
//     (AuthConfig nil or StrategyTypeUnauthenticated): Backend is unauthenticated
//     (BackendUnauthenticated). This signals operator misconfiguration — the backend
//     requires authentication but none was configured on the backend target.
//   - Timeout or connection error: Backend is unhealthy (BackendUnhealthy)
//   - Other errors: Backend is unhealthy (BackendUnhealthy)
//
// The error return is informational and provides context about what failed.
// The BackendHealthStatus return indicates the categorized health state.
func (h *healthChecker) CheckHealth(ctx context.Context, target *vmcp.BackendTarget) (vmcp.BackendHealthStatus, error) {
	// Mark context as health check to bypass authentication logging
	// Health checks verify backend availability and should not require user credentials
	healthCheckCtx := WithHealthCheckMarker(ctx)

	// Apply timeout if configured (after adding health check marker)
	checkCtx := healthCheckCtx
	var cancel context.CancelFunc
	if h.timeout > 0 {
		checkCtx, cancel = context.WithTimeout(healthCheckCtx, h.timeout)
		defer cancel()
	}

	slog.Debug("performing health check for backend", "backend", target.WorkloadName, "url", target.BaseURL)

	// Track response time for degraded detection
	startTime := time.Now()

	// Use ListCapabilities as the health check - it performs:
	// 1. Client creation with transport setup
	// 2. MCP protocol initialization handshake
	// 3. Capabilities query (tools, resources, prompts)
	// This validates the full communication stack
	_, err := h.client.ListCapabilities(checkCtx, target)
	responseDuration := time.Since(startTime)

	if err != nil {
		// Categorize the error to determine health status. The target's outgoing
		// auth config is consulted: a 401/403 from a backend with an outgoing auth
		// strategy is the expected response to a no-credential probe and maps to
		// BackendHealthy. In that case we return a nil error so the monitor records
		// this as a successful check and does not open the circuit breaker.
		status := categorizeError(target, err)
		if status == vmcp.BackendHealthy {
			slog.Debug("health check received expected auth challenge — treating as healthy",
				"backend", target.WorkloadName,
				"error", err,
				"duration", responseDuration)
			return vmcp.BackendHealthy, nil
		}
		slog.Debug("health check failed for backend",
			"backend", target.WorkloadName,
			"error", err,
			"status", status,
			"duration", responseDuration)
		return status, fmt.Errorf("health check failed: %w", err)
	}

	// Check if response time indicates degraded performance
	if h.degradedThreshold > 0 && responseDuration > h.degradedThreshold {
		slog.Warn("health check succeeded but response was slow - marking as degraded",
			"backend", target.WorkloadName,
			"duration", responseDuration,
			"threshold", h.degradedThreshold)
		return vmcp.BackendDegraded, nil
	}

	slog.Debug("health check succeeded for backend", "backend", target.WorkloadName, "duration", responseDuration)
	return vmcp.BackendHealthy, nil
}

// categorizeError determines the appropriate health status based on the error type
// and the backend's outgoing auth configuration.
//
// This uses sentinel error checking with errors.Is() for type-safe error categorization.
// Falls back to string-based detection for backwards compatibility with non-wrapped errors.
//
// For auth errors (HTTP 401/403), the target's AuthConfig is consulted to distinguish
// an expected auth challenge (backend has outgoing auth configured) from a misconfiguration
// (backend has no outgoing auth strategy). See authErrorStatus for details.
func categorizeError(target *vmcp.BackendTarget, err error) vmcp.BackendHealthStatus {
	if err == nil {
		return vmcp.BackendHealthy
	}

	// 1. Type-safe detection: Check for sentinel errors using errors.Is()
	// BackendClient now wraps all errors with appropriate sentinel errors
	if errors.Is(err, vmcp.ErrAuthenticationFailed) || errors.Is(err, vmcp.ErrAuthorizationFailed) {
		return authErrorStatus(target)
	}

	if errors.Is(err, vmcp.ErrTimeout) || errors.Is(err, vmcp.ErrCancelled) {
		return vmcp.BackendUnhealthy
	}

	if errors.Is(err, vmcp.ErrBackendUnavailable) {
		return vmcp.BackendUnhealthy
	}

	// 2. String-based detection: Fallback for backwards compatibility
	// This handles errors from sources that don't wrap with sentinel errors
	if vmcp.IsAuthenticationError(err) {
		return authErrorStatus(target)
	}

	if vmcp.IsTimeoutError(err) || vmcp.IsConnectionError(err) {
		return vmcp.BackendUnhealthy
	}

	// Default to unhealthy for unknown errors
	return vmcp.BackendUnhealthy
}

// authErrorStatus maps an authentication error (HTTP 401/403) to a health status
// using the backend's outgoing auth configuration.
//
// Health probes deliberately do not carry user credentials. If the backend is
// configured with an outgoing auth strategy, a 401/403 from the backend proves
// that the backend is alive, the auth layer works, and the network+TLS path is
// healthy — this is the expected response to an unauthenticated probe and is
// therefore treated as BackendHealthy.
//
// If the backend has no outgoing auth strategy configured (AuthConfig nil or
// StrategyTypeUnauthenticated), a 401/403 indicates operator misconfiguration:
// the backend requires authentication but none was configured on the backend
// target. This is reported as BackendUnauthenticated so it surfaces in status.
func authErrorStatus(target *vmcp.BackendTarget) vmcp.BackendHealthStatus {
	if target != nil && target.AuthConfig != nil &&
		target.AuthConfig.Type != authtypes.StrategyTypeUnauthenticated {
		return vmcp.BackendHealthy
	}
	return vmcp.BackendUnauthenticated
}
