package app

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

	"git.warky.dev/wdevs/amcs/internal/ai"
	"git.warky.dev/wdevs/amcs/internal/auth"
	"git.warky.dev/wdevs/amcs/internal/buildinfo"
	"git.warky.dev/wdevs/amcs/internal/config"
	"git.warky.dev/wdevs/amcs/internal/mcpserver"
	"git.warky.dev/wdevs/amcs/internal/observability"
	"git.warky.dev/wdevs/amcs/internal/session"
	"git.warky.dev/wdevs/amcs/internal/store"
	"git.warky.dev/wdevs/amcs/internal/tools"
)

func Run(ctx context.Context, configPath string) error {
	cfg, loadedFrom, err := config.Load(configPath)
	if err != nil {
		return err
	}
	info := buildinfo.Current()
	cfg.MCP.Version = info.Version

	logger, err := observability.NewLogger(cfg.Logging)
	if err != nil {
		return err
	}

	logger.Info("loaded configuration",
		slog.String("path", loadedFrom),
		slog.Int("config_version", cfg.Version),
		slog.String("version", info.Version),
		slog.String("tag_name", info.TagName),
		slog.String("build_date", info.BuildDate),
		slog.String("commit", info.Commit),
	)

	db, err := store.New(ctx, cfg.Database)
	if err != nil {
		return err
	}
	defer db.Close()

	if err := db.VerifyRequirements(ctx); err != nil {
		return err
	}

	httpClient := &http.Client{Timeout: 30 * time.Second}
	registry, err := ai.NewRegistry(cfg.AI.Providers, httpClient, logger)
	if err != nil {
		return err
	}

	foregroundEmbeddings, err := ai.NewEmbeddingRunner(registry, cfg.AI.Embeddings.Chain(), cfg.AI.Embeddings.Dimensions, logger)
	if err != nil {
		return err
	}
	foregroundMetadata, err := ai.NewMetadataRunner(registry, cfg.AI.Metadata.Chain(), cfg.AI.Metadata.Temperature, cfg.AI.Metadata.LogConversations, logger)
	if err != nil {
		return err
	}

	backgroundEmbeddings := foregroundEmbeddings
	backgroundMetadata := foregroundMetadata
	if cfg.AI.Background != nil {
		if cfg.AI.Background.Embeddings != nil {
			backgroundEmbeddings, err = ai.NewEmbeddingRunner(registry, cfg.AI.Background.Embeddings.AsTargets(), cfg.AI.Embeddings.Dimensions, logger)
			if err != nil {
				return err
			}
		}
		if cfg.AI.Background.Metadata != nil {
			backgroundMetadata, err = ai.NewMetadataRunner(registry, cfg.AI.Background.Metadata.AsTargets(), cfg.AI.Metadata.Temperature, cfg.AI.Metadata.LogConversations, logger)
			if err != nil {
				return err
			}
		}
	}

	var keyring *auth.Keyring
	var oauthRegistry *auth.OAuthRegistry
	var tokenStore *auth.TokenStore
	if len(cfg.Auth.Keys) > 0 {
		keyring, err = auth.NewKeyring(cfg.Auth.Keys)
		if err != nil {
			return err
		}
	}
	tokenStore = auth.NewTokenStore(0)
	if len(cfg.Auth.OAuth.Clients) > 0 {
		oauthRegistry, err = auth.NewOAuthRegistry(cfg.Auth.OAuth.Clients)
		if err != nil {
			return err
		}
	}
	authCodes := auth.NewAuthCodeStore()
	dynClients := auth.NewDynamicClientStore()
	activeProjects := session.NewActiveProjects()

	logger.Info("ai providers initialised",
		slog.String("embedding_primary", foregroundEmbeddings.PrimaryProvider()+"/"+foregroundEmbeddings.PrimaryModel()),
		slog.String("metadata_primary", foregroundMetadata.PrimaryProvider()+"/"+foregroundMetadata.PrimaryModel()),
	)

	if cfg.Backfill.Enabled && cfg.Backfill.RunOnStartup {
		go runBackfillPass(ctx, db, backgroundEmbeddings, cfg.Backfill, logger)
	}

	if cfg.Backfill.Enabled && cfg.Backfill.Interval > 0 {
		go func() {
			ticker := time.NewTicker(cfg.Backfill.Interval)
			defer ticker.Stop()
			for {
				select {
				case <-ctx.Done():
					return
				case <-ticker.C:
					runBackfillPass(ctx, db, backgroundEmbeddings, cfg.Backfill, logger)
				}
			}
		}()
	}

	if cfg.MetadataRetry.Enabled && cfg.MetadataRetry.RunOnStartup {
		go runMetadataRetryPass(ctx, db, backgroundMetadata, cfg, activeProjects, logger)
	}

	if cfg.MetadataRetry.Enabled && cfg.MetadataRetry.Interval > 0 {
		go func() {
			ticker := time.NewTicker(cfg.MetadataRetry.Interval)
			defer ticker.Stop()
			for {
				select {
				case <-ctx.Done():
					return
				case <-ticker.C:
					runMetadataRetryPass(ctx, db, backgroundMetadata, cfg, activeProjects, logger)
				}
			}
		}()
	}

	handler, err := routes(logger, cfg, info, db, foregroundEmbeddings, foregroundMetadata, backgroundEmbeddings, backgroundMetadata, keyring, oauthRegistry, tokenStore, authCodes, dynClients, activeProjects)
	if err != nil {
		return err
	}

	server := &http.Server{
		Addr:         fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
		Handler:      handler,
		ReadTimeout:  cfg.Server.ReadTimeout,
		WriteTimeout: cfg.Server.WriteTimeout,
		IdleTimeout:  cfg.Server.IdleTimeout,
	}

	errCh := make(chan error, 1)
	go func() {
		logger.Info("starting HTTP server",
			slog.String("addr", server.Addr),
			slog.String("mcp_path", cfg.MCP.Path),
		)

		if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			errCh <- err
		}
	}()

	select {
	case <-ctx.Done():
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()

		logger.Info("shutting down HTTP server")
		return server.Shutdown(shutdownCtx)
	case err := <-errCh:
		return fmt.Errorf("run server: %w", err)
	}
}

func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *store.DB, embeddings *ai.EmbeddingRunner, metadata *ai.MetadataRunner, bgEmbeddings *ai.EmbeddingRunner, bgMetadata *ai.MetadataRunner, keyring *auth.Keyring, oauthRegistry *auth.OAuthRegistry, tokenStore *auth.TokenStore, authCodes *auth.AuthCodeStore, dynClients *auth.DynamicClientStore, activeProjects *session.ActiveProjects) (http.Handler, error) {
	mux := http.NewServeMux()
	accessTracker := auth.NewAccessTracker()
	oauthEnabled := oauthRegistry != nil
	authMiddleware := auth.Middleware(cfg.Auth, keyring, oauthRegistry, tokenStore, accessTracker, logger)
	filesTool := tools.NewFilesTool(db, activeProjects)
	enrichmentRetryer := tools.NewEnrichmentRetryer(context.Background(), db, bgMetadata, cfg.Capture, cfg.AI.Metadata.Timeout, activeProjects, logger)
	backfillTool := tools.NewBackfillTool(db, bgEmbeddings, activeProjects, logger)
	adminActions := newAdminActions(backfillTool, enrichmentRetryer, logger)

	toolSet := mcpserver.ToolSet{
		Capture:       tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
		Search:        tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects),
		List:          tools.NewListTool(db, cfg.Search, activeProjects),
		Stats:         tools.NewStatsTool(db),
		Get:           tools.NewGetTool(db),
		Update:        tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger),
		Delete:        tools.NewDeleteTool(db),
		Archive:       tools.NewArchiveTool(db),
		Projects:      tools.NewProjectsTool(db, activeProjects),
		Version:       tools.NewVersionTool(cfg.MCP.ServerName, info),
		Learnings:     tools.NewLearningsTool(db, activeProjects, cfg.Search),
		Plans:         tools.NewPlansTool(db, activeProjects, cfg.Search),
		Context:       tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
		Recall:        tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
		Summarize:     tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
		Links:         tools.NewLinksTool(db, embeddings, cfg.Search),
		Files:         filesTool,
		Backfill:      backfillTool,
		Reparse:       tools.NewReparseMetadataTool(db, bgMetadata, cfg.Capture, activeProjects, logger),
		RetryMetadata: tools.NewRetryEnrichmentTool(enrichmentRetryer),
		//Maintenance:   tools.NewMaintenanceTool(db),
		Skills:      tools.NewSkillsTool(db, activeProjects),
		ChatHistory: tools.NewChatHistoryTool(db, activeProjects),
		Describe:    tools.NewDescribeTool(db, mcpserver.BuildToolCatalog()),
	}

	mcpHandlers, err := mcpserver.NewHandlers(cfg.MCP, logger, toolSet, activeProjects.Clear)
	if err != nil {
		return nil, fmt.Errorf("build mcp handler: %w", err)
	}
	mux.Handle(cfg.MCP.Path, authMiddleware(mcpHandlers.StreamableHTTP))
	if mcpHandlers.SSE != nil {
		mux.Handle(cfg.MCP.SSEPath, authMiddleware(mcpHandlers.SSE))
		logger.Info("SSE transport enabled", slog.String("sse_path", cfg.MCP.SSEPath))
	}
	if err := registerResolveSpecAdminRoutes(mux, db, authMiddleware, logger); err != nil {
		return nil, fmt.Errorf("setup resolvespec admin routes: %w", err)
	}
	mux.Handle("/files", authMiddleware(fileHandler(filesTool)))
	mux.Handle("/files/{id}", authMiddleware(fileHandler(filesTool)))
	mux.HandleFunc("/.well-known/oauth-authorization-server", oauthMetadataHandler())
	mux.HandleFunc("/api/oauth/register", oauthRegisterHandler(dynClients, logger))
	mux.HandleFunc("/api/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
	mux.HandleFunc("/api/oauth/token", oauthTokenHandler(oauthRegistry, tokenStore, authCodes, logger))
	mux.Handle("/api/admin/actions/backfill", authMiddleware(adminActions.backfillHandler()))
	mux.Handle("/api/admin/actions/retry-metadata", authMiddleware(adminActions.retryMetadataHandler()))
	mux.HandleFunc("/favicon.ico", serveFavicon)
	mux.HandleFunc("/images/project.jpg", serveHomeImage)
	mux.HandleFunc("/images/icon.png", serveIcon)
	mux.HandleFunc("/llm", serveLLMInstructions)
	mux.HandleFunc("/llms.txt", serveLLMSTXT)
	mux.HandleFunc("/.well-known/llms.txt", serveLLMSTXT)
	mux.HandleFunc("/robots.txt", serveRobotsTXT)
	mux.Handle("/api/status", authMiddleware(statusAPIHandler(info, accessTracker, oauthEnabled)))
	mux.HandleFunc("/status", publicStatusHandler(accessTracker))

	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	})

	mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
		if err := db.Ready(r.Context()); err != nil {
			logger.Error("readiness check failed", slog.String("error", err.Error()))
			http.Error(w, "not ready", http.StatusServiceUnavailable)
			return
		}

		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ready"))
	})

	mux.HandleFunc("/", homeHandler(info, accessTracker, oauthEnabled))

	return observability.Chain(
		mux,
		observability.RequestID(),
		observability.Recover(logger),
		observability.AccessLog(logger),
		observability.Timeout(cfg.Server.WriteTimeout),
	), nil
}

func runMetadataRetryPass(ctx context.Context, db *store.DB, metadataRunner *ai.MetadataRunner, cfg *config.Config, activeProjects *session.ActiveProjects, logger *slog.Logger) {
	retryer := tools.NewMetadataRetryer(ctx, db, metadataRunner, cfg.Capture, cfg.AI.Metadata.Timeout, activeProjects, logger)
	_, out, err := retryer.Handle(ctx, nil, tools.RetryMetadataInput{
		Limit:           cfg.MetadataRetry.MaxPerRun,
		IncludeArchived: cfg.MetadataRetry.IncludeArchived,
		OlderThanDays:   1,
	})
	if err != nil {
		logger.Error("auto metadata retry failed", slog.String("error", err.Error()))
		return
	}
	logger.Info("auto metadata retry pass",
		slog.Int("scanned", out.Scanned),
		slog.Int("retried", out.Retried),
		slog.Int("updated", out.Updated),
		slog.Int("failed", out.Failed),
	)
}

func runBackfillPass(ctx context.Context, db *store.DB, embeddings *ai.EmbeddingRunner, cfg config.BackfillConfig, logger *slog.Logger) {
	backfiller := tools.NewBackfillTool(db, embeddings, nil, logger)
	_, out, err := backfiller.Handle(ctx, nil, tools.BackfillInput{
		Limit:           cfg.MaxPerRun,
		IncludeArchived: cfg.IncludeArchived,
	})
	if err != nil {
		logger.Error("auto backfill failed", slog.String("error", err.Error()))
		return
	}
	logger.Info("auto backfill pass",
		slog.String("model", out.Model),
		slog.Int("scanned", out.Scanned),
		slog.Int("embedded", out.Embedded),
		slog.Int("failed", out.Failed),
	)
}

func serveHomeImage(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet && r.Method != http.MethodHead {
		w.Header().Set("Allow", "GET, HEAD")
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	w.Header().Set("Content-Type", "image/jpeg")
	w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
	w.WriteHeader(http.StatusOK)

	if r.Method == http.MethodHead {
		return
	}

	_, _ = w.Write(homeImage)
}

func serveIcon(w http.ResponseWriter, r *http.Request) {
	if iconImage == nil {
		http.NotFound(w, r)
		return
	}

	if r.Method != http.MethodGet && r.Method != http.MethodHead {
		w.Header().Set("Allow", "GET, HEAD")
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	w.Header().Set("Content-Type", "image/png")
	w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
	w.WriteHeader(http.StatusOK)

	if r.Method == http.MethodHead {
		return
	}

	_, _ = w.Write(iconImage)
}
