package app

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"

	"github.com/ClareAI/astra-insight-mcp/internal/agent"
	"github.com/ClareAI/astra-insight-mcp/internal/config"
	"github.com/ClareAI/astra-insight-mcp/internal/gateway"
	"github.com/ClareAI/astra-insight-mcp/internal/llm"
	"github.com/ClareAI/astra-insight-mcp/internal/mcp"
	"github.com/ClareAI/astra-insight-mcp/internal/middleware"
	"github.com/ClareAI/astra-insight-mcp/internal/oauth"
	"github.com/ClareAI/astra-insight-mcp/internal/pkg/gcs"
	redispkg "github.com/ClareAI/astra-insight-mcp/internal/pkg/redis"
	"github.com/ClareAI/astra-insight-mcp/internal/tools"
	"github.com/ClareAI/astra-insight-mcp/internal/version"
	"github.com/ClareAI/astra-insight-mcp/internal/webhook"
)

// RunMCPServer starts the Streamable HTTP MCP server (and optional webhook stack).
func RunMCPServer(debug bool) {
	slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: config.SlogLevel()})))

	cfg := config.Load()

	gw := gateway.NewClient(cfg.GatewayBaseURL)
	if debug {
		gw.SetDebug(true)
		slog.Info("debug mode enabled — curl commands will be printed to stderr")
	}

	srv := mcp.NewServer("astra-insight-mcp", version.Version)
	tools.RegisterCore(srv, gw)
	registerSkillPrompts(srv)

	rl := middleware.NewRateLimiter(cfg.RateLimitPerMin)

	mux := http.NewServeMux()
	mux.Handle("/mcp", middleware.RequireBearerAuth(rl.Wrap(srv)))
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "ok")
	})

	webhookEnabled := false
	oauthEnabled := false
	var wh *webhook.Handler
	llmCfg := config.LoadLLMFromEnv()
	if llmCfg.APIKey != "" {
		llmClient := llm.NewClient(llmCfg.APIKey, llmCfg.BaseURL, llmCfg.Model)
		var fastClient *llm.Client
		if llmCfg.FastModel != "" && llmCfg.FastModel != llmCfg.Model {
			fastClient = llmClient.WithModel(llmCfg.FastModel)
		}

		agent.SetRegisterFunc(func(s *mcp.Server, g *gateway.Client) agent.SkillPruner {
			return tools.RegisterCore(s, g)
		})

		redisCfg := redispkg.Config{
			URL:      os.Getenv("REDIS_URL"),
			Host:     os.Getenv("REDIS_HOST"),
			Port:     os.Getenv("REDIS_PORT"),
			Password: os.Getenv("REDIS_PASSWORD"),
			DB:       os.Getenv("REDIS_DB"),
		}
		var rdb *redispkg.Client
		if redisCfg.URL != "" || redisCfg.Host != "" {
			var err error
			rdb, err = redispkg.New(redisCfg)
			if err != nil {
				slog.Error("failed to connect to redis", "error", err)
				os.Exit(1)
			}
			defer rdb.Close()
		}

		var gcsClient *gcs.GCSClient
		gcsBucket := os.Getenv("GCS_SESSION_BUCKET")
		if gcsBucket != "" {
			var err error
			gcsClient, err = gcs.NewGCSClient(context.Background(), gcsBucket)
			if err != nil {
				slog.Error("failed to create gcs client", "error", err, "bucket", gcsBucket)
				os.Exit(1)
			}
			defer gcsClient.Close()
			slog.Info("gcs session storage enabled", "bucket", gcsBucket)
		}

		gcsSessionDir := os.Getenv("GCS_SESSION_DIR")
		if gcsSessionDir == "" {
			gcsSessionDir = "wa_sessions/"
		}

		var recipeStore agent.RecipeStore
		dbURL := os.Getenv("DATABASE_URL")
		if dbURL == "" {
			if host := os.Getenv("DATABASE_HOST"); host != "" {
				port := os.Getenv("DATABASE_PORT")
				if port == "" {
					port = "5432"
				}
				user := os.Getenv("DATABASE_USER")
				pass := os.Getenv("DATABASE_PASSWORD")
				name := os.Getenv("DATABASE_NAME")
				sslmode := os.Getenv("DATABASE_SSLMODE")
				if sslmode == "" {
					sslmode = "disable"
				}
				dbURL = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s",
					user, pass, host, port, name, sslmode)
			}
		}
		if dbURL != "" {
			pgStore, err := agent.NewPostgresRecipeStore(context.Background(), dbURL)
			if err != nil {
				slog.Error("failed to connect recipe store to postgres", "error", err)
			} else {
				recipeStore = pgStore
				defer pgStore.Close()
				slog.Info("recipe store: postgres enabled")
			}
		}

		var whOpts webhook.HandlerOpts
		oauthCallbackURL := os.Getenv("OAUTH_CALLBACK_BASE_URL")
		oauthEnv := os.Getenv("OAUTH_ENV")
		if oauthEnv == "" {
			oauthEnv = config.DeriveEnvFromGatewayURL(cfg.GatewayBaseURL)
		}

		if oauthCallbackURL != "" && rdb != nil {
			tokenStore := oauth.NewTokenStore(rdb)
			pendingAuth := oauth.NewPendingAuthStore(rdb)
			whOpts = webhook.HandlerOpts{
				TokenStore:       tokenStore,
				PendingAuth:      pendingAuth,
				OAuthEnv:         oauthEnv,
				OAuthCallbackURL: oauthCallbackURL,
				RedisClient:      rdb,
				GCSClient:        gcsClient,
				GCSSessionDir:    gcsSessionDir,
				RecipeStore:      recipeStore,
			}
			oauthEnabled = true
			slog.Info("per-user OAuth enabled",
				"callback_url", oauthCallbackURL+"/oauth/callback",
				"env", oauthEnv,
			)
		} else {
			whOpts = webhook.HandlerOpts{
				RedisClient:   rdb,
				GCSClient:     gcsClient,
				GCSSessionDir: gcsSessionDir,
				RecipeStore:   recipeStore,
			}
		}

		wh = webhook.NewHandler(llmClient, fastClient, gw, whOpts)
		mux.Handle("/webhook/wati", rl.Wrap(wh))
		if oauthEnabled {
			mux.Handle("/oauth/callback", rl.Wrap(http.HandlerFunc(wh.OAuthCallbackHandler())))
		}
		webhookEnabled = true
		slog.Info("webhook endpoint enabled",
			"path", "/webhook/wati",
			"llm_model", llmCfg.Model,
			"fast_model", llmCfg.FastModel,
			"oauth", oauthEnabled,
			"gcs_sessions", gcsClient != nil,
		)
	}

	addr := ":" + cfg.Port
	slog.Info("astra-insight-mcp starting",
		"addr", addr,
		"gateway", cfg.GatewayBaseURL,
		"rate_limit", cfg.RateLimitPerMin,
		"debug", debug,
		"webhook", webhookEnabled,
	)

	server := &http.Server{Addr: addr, Handler: mux}

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
	go func() {
		sig := <-sigCh
		slog.Info("received signal, shutting down", "signal", sig.String())
		if wh != nil {
			wh.Shutdown()
		}
		server.Shutdown(context.Background())
	}()

	if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		slog.Error("server failed", "error", err)
		os.Exit(1)
	}
	slog.Info("server stopped")
}

func registerSkillPrompts(srv *mcp.Server) {
	candidates := []string{"skills", "../skills", "/skills"}
	if exe, err := os.Executable(); err == nil {
		candidates = append(candidates, filepath.Join(filepath.Dir(exe), "skills"))
	}

	var dir string
	for _, c := range candidates {
		if info, err := os.Stat(c); err == nil && info.IsDir() {
			dir = c
			break
		}
	}
	if dir == "" {
		return
	}

	entries, err := os.ReadDir(dir)
	if err != nil {
		return
	}

	count := 0
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
			continue
		}
		data, err := os.ReadFile(filepath.Join(dir, e.Name()))
		if err != nil {
			continue
		}
		content := string(data)
		name := strings.TrimSuffix(e.Name(), ".md")

		desc := name
		for _, line := range strings.Split(content, "\n") {
			if strings.HasPrefix(line, "# ") {
				desc = strings.TrimPrefix(line, "# ")
				break
			}
		}

		srv.RegisterPrompt(mcp.Prompt{
			Name:        name,
			Description: desc,
		}, func(args map[string]string) mcp.PromptGetResult {
			return mcp.PromptGetResult{
				Description: desc,
				Messages: []mcp.PromptMessage{
					{Role: "user", Content: mcp.Content{Type: "text", Text: content}},
				},
			}
		})
		count++
	}

	if count > 0 {
		slog.Info("loaded skill prompts", "count", count, "dir", dir)
	}
}
