package http

import (
	"context"
	"fmt"
	"io"
	"log/slog"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"

	ghcontext "github.com/github/github-mcp-server/pkg/context"
	"github.com/github/github-mcp-server/pkg/github"
	"github.com/github/github-mcp-server/pkg/http/middleware"
	"github.com/github/github-mcp-server/pkg/http/oauth"
	"github.com/github/github-mcp-server/pkg/inventory"
	"github.com/github/github-mcp-server/pkg/lockdown"
	"github.com/github/github-mcp-server/pkg/observability"
	"github.com/github/github-mcp-server/pkg/observability/metrics"
	"github.com/github/github-mcp-server/pkg/scopes"
	"github.com/github/github-mcp-server/pkg/translations"
	"github.com/github/github-mcp-server/pkg/utils"
	"github.com/go-chi/chi/v5"
)

type ServerConfig struct {
	// Version of the server
	Version string

	// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
	Host string

	// Port to listen on (default: 8082).
	Port int

	// ListenHost is the host the HTTP server binds to (e.g. "127.0.0.1").
	// When empty, the server binds to all interfaces. Combined with Port.
	ListenHost string

	// BaseURL is the publicly accessible URL of this server for OAuth resource metadata.
	// If not set, the server will derive the URL from incoming request headers.
	BaseURL string

	// ResourcePath is the externally visible base path for this server (e.g., "/mcp").
	// This is used to restore the original path when a proxy strips a base path before forwarding.
	ResourcePath string

	// TrustProxyHeaders indicates whether X-Forwarded-Host and X-Forwarded-Proto
	// should be honored when constructing OAuth resource metadata URLs. Only
	// enable this when the server is deployed behind a trusted proxy that sets
	// these headers. When BaseURL is set, it always wins and this setting has
	// no effect.
	TrustProxyHeaders bool

	// ExportTranslations indicates if we should export translations
	// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#i18n--overriding-descriptions
	ExportTranslations bool

	// EnableCommandLogging indicates if we should log commands
	EnableCommandLogging bool

	// Path to the log file if not stderr
	LogFilePath string

	// Content window size
	ContentWindowSize int

	// LockdownMode indicates if we should enable lockdown mode
	LockdownMode bool

	// RepoAccessCacheTTL overrides the default TTL for repository access cache entries.
	RepoAccessCacheTTL *time.Duration

	// ScopeChallenge indicates if we should return OAuth scope challenges, and if we should perform
	// tool filtering based on token scopes.
	ScopeChallenge bool

	// ReadOnly indicates if we should only register read-only tools.
	// When set via CLI flag, this acts as an upper bound — per-request headers
	// cannot re-enable write tools.
	ReadOnly bool

	// EnabledToolsets is a list of toolsets to enable.
	// When set via CLI flag, per-request headers can only narrow within these toolsets.
	EnabledToolsets []string

	// EnabledTools is a list of specific tools to enable (additive to toolsets).
	EnabledTools []string

	// ExcludeTools is a list of tool names to disable regardless of other settings.
	// When set via CLI flag, per-request headers cannot re-include these tools.
	ExcludeTools []string

	// EnabledFeatures is a list of feature flags that are enabled.
	EnabledFeatures []string

	// InsidersMode expands to the curated set of feature flags enabled for insiders.
	InsidersMode bool
}

func RunHTTPServer(cfg ServerConfig) error {
	// Create app context
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	t, dumpTranslations := translations.TranslationHelper()

	var slogHandler slog.Handler
	var logOutput io.Writer
	if cfg.LogFilePath != "" {
		file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
		if err != nil {
			return fmt.Errorf("failed to open log file: %w", err)
		}
		logOutput = file
		slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelDebug})
	} else {
		logOutput = os.Stderr
		slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelInfo})
	}
	logger := slog.New(slogHandler)
	logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "lockdownEnabled", cfg.LockdownMode, "readOnly", cfg.ReadOnly, "insidersMode", cfg.InsidersMode)

	apiHost, err := utils.NewAPIHost(cfg.Host)
	if err != nil {
		return fmt.Errorf("failed to parse API host: %w", err)
	}

	repoAccessOpts := []lockdown.RepoAccessOption{
		lockdown.WithLogger(logger.With("component", "lockdown")),
	}
	if cfg.RepoAccessCacheTTL != nil {
		repoAccessOpts = append(repoAccessOpts, lockdown.WithTTL(*cfg.RepoAccessCacheTTL))
	}

	featureChecker := createHTTPFeatureChecker(cfg.EnabledFeatures, cfg.InsidersMode)

	obs, err := observability.NewExporters(logger, metrics.NewNoopMetrics())
	if err != nil {
		return fmt.Errorf("failed to create observability exporters: %w", err)
	}

	deps := github.NewRequestDeps(
		apiHost,
		cfg.Version,
		cfg.LockdownMode,
		repoAccessOpts,
		t,
		cfg.ContentWindowSize,
		featureChecker,
		obs,
	)

	// Initialize the global tool scope map
	err = initGlobalToolScopeMap(t)
	if err != nil {
		return fmt.Errorf("failed to initialize tool scope map: %w", err)
	}

	// Register OAuth protected resource metadata endpoints
	oauthCfg := &oauth.Config{
		BaseURL:           cfg.BaseURL,
		ResourcePath:      cfg.ResourcePath,
		TrustProxyHeaders: cfg.TrustProxyHeaders,
	}

	serverOptions := []HandlerOption{}
	if cfg.ScopeChallenge {
		scopeFetcher := scopes.NewFetcher(apiHost, scopes.FetcherOptions{})
		serverOptions = append(serverOptions, WithScopeFetcher(scopeFetcher))
	}

	r := chi.NewRouter()
	handler := NewHTTPMcpHandler(ctx, &cfg, deps, t, logger, apiHost, append(serverOptions, WithFeatureChecker(featureChecker), WithOAuthConfig(oauthCfg))...)
	oauthHandler, err := oauth.NewAuthHandler(oauthCfg, apiHost)
	if err != nil {
		return fmt.Errorf("failed to create OAuth handler: %w", err)
	}

	r.Group(func(r chi.Router) {
		r.Use(middleware.SetCorsHeaders)

		// Register Middleware First, needs to be before route registration
		handler.RegisterMiddleware(r)

		// Register MCP server routes
		handler.RegisterRoutes(r)
	})
	logger.Info("MCP endpoints registered", "baseURL", cfg.BaseURL)

	r.Group(func(r chi.Router) {
		// Register OAuth protected resource metadata endpoints
		oauthHandler.RegisterRoutes(r)
	})
	logger.Info("OAuth protected resource endpoints registered", "baseURL", cfg.BaseURL)

	addr := resolveListenAddress(cfg.ListenHost, cfg.Port)
	httpSvr := http.Server{
		Addr:              addr,
		Handler:           r,
		ReadHeaderTimeout: 60 * time.Second,
	}

	go func() {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		logger.Info("shutting down server")
		if err := httpSvr.Shutdown(shutdownCtx); err != nil {
			logger.Error("error during server shutdown", "error", err)
		}
	}()

	if cfg.ExportTranslations {
		// Once server is initialized, all translations are loaded
		dumpTranslations()
	}

	logger.Info("HTTP server listening", "addr", addr)
	if err := httpSvr.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		return fmt.Errorf("HTTP server error: %w", err)
	}

	logger.Info("server stopped gracefully")
	return nil
}

// resolveListenAddress returns the address string passed to http.Server.
// When host is empty the server binds to all interfaces on the given port;
// otherwise host and port are joined into a single address.
func resolveListenAddress(host string, port int) string {
	if host == "" {
		return fmt.Sprintf(":%d", port)
	}
	return net.JoinHostPort(host, strconv.Itoa(port))
}

func initGlobalToolScopeMap(t translations.TranslationHelperFunc) error {
	// Build inventory with all tools to extract scope information
	inv, err := inventory.NewBuilder().
		SetTools(github.AllTools(t)).
		Build()

	if err != nil {
		return fmt.Errorf("failed to build inventory for tool scope map: %w", err)
	}

	// Initialize the global scope map
	scopes.SetToolScopeMapFromInventory(inv)

	return nil
}

// createHTTPFeatureChecker creates a feature checker that resolves static CLI
// features plus per-request header features and insiders mode.
func createHTTPFeatureChecker(enabledFeatures []string, insidersMode bool) inventory.FeatureFlagChecker {
	return func(ctx context.Context, flag string) (bool, error) {
		headerFeatures := ghcontext.GetHeaderFeatures(ctx)
		features := make([]string, 0, len(enabledFeatures)+len(headerFeatures))
		features = append(features, enabledFeatures...)
		features = append(features, headerFeatures...)

		effective := github.ResolveFeatureFlags(features, insidersMode || ghcontext.IsInsidersMode(ctx))
		return effective[flag], nil
	}
}
