package cli

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/mark3labs/mcp-go/server"

	"{{ module_path }}/internal/auth"
	"{{ module_path }}/internal/core"
	"{{ module_path }}/internal/data"
	httptransport "{{ module_path }}/internal/http"
	"{{ module_path }}/internal/services"
	"{{ module_path }}/internal/tools"
)

// Services bundles every dependency the Terminal Client / Harness Server
// roles need — built once by newServices and shared across every
// subcommand. mcpify always emits both stdio and HTTP capability;
// transport *selection* is runtime config (REQ-2.2's cascade), not a
// generation-time decision — mirrors `targets::python`'s
// `_run_harness_server()`.
type Services struct {
	Config      core.Config
	Store       *data.Store
	EmbedSvc    *services.EmbeddingService
	VectorStore *services.VectorStore
	ApiClient   *services.ApiClient
	AuthManager *auth.Manager
	Health      *core.Registry
	MCPServer   *server.MCPServer
}

func newServices(ctx context.Context, overrides core.CLIOverrides) (*Services, error) {
	config, err := core.Load(overrides)
	if err != nil {
		return nil, err
	}

	// CachedStore (not Open): backs the on-disk file up into memory once
	// per process and never re-touches the disk file afterward, so this
	// long-lived Harness Server process doesn't hold a lasting lock on
	// mcp_store.db against a fresh populate-embeddings run or a deployment
	// replacing the file — see CachedStore's doc comment.
	store, err := data.CachedStore(config.ApiVersion)
	if err != nil {
		return nil, err
	}

	home, err := os.UserHomeDir()
	if err != nil {
		return nil, err
	}
	embedSvc, err := services.NewEmbeddingService(
		ctx,
		filepath.Join(home, ".{{ project_name }}", "models"),
		services.ResolveSharedLibraryPath(),
	)
	if err != nil {
		return nil, err
	}

	chromemPath, err := data.ChromemPathFor(config.ApiVersion)
	if err != nil {
		return nil, err
	}
	vectorStore, err := services.NewVectorStore(chromemPath, embedSvc.Embed)
	if err != nil {
		return nil, err
	}

	credentialStore, err := core.NewCredentialStore()
	if err != nil {
		return nil, err
	}
	authManager, err := auth.NewManager(config.AuthMethod, auth.NewStrategies(), credentialStore)
	if err != nil {
		return nil, err
	}

	apiClient := services.NewApiClient(config)
	health := core.NewRegistry()
	mcpServer := core.NewMCPServer()
	tools.Register(mcpServer, store, vectorStore, apiClient, authManager, config.ApiVersion)

	return &Services{
		Config:      config,
		Store:       store,
		EmbedSvc:    embedSvc,
		VectorStore: vectorStore,
		ApiClient:   apiClient,
		AuthManager: authManager,
		Health:      health,
		MCPServer:   mcpServer,
	}, nil
}

func (s *Services) Close() {
	// s.Store is intentionally not closed here — CachedStore returns a
	// process-wide connection shared and reused across the process's
	// lifetime, not one owned by this particular Services instance.
	_ = s.EmbedSvc.Close()
}

// RunStdioHarness starts the Harness Server over stdio.
func RunStdioHarness(ctx context.Context, overrides core.CLIOverrides) error {
	svc, err := newServices(ctx, overrides)
	if err != nil {
		return err
	}
	defer svc.Close()
	return server.ServeStdio(svc.MCPServer)
}

// RunHTTPHarness starts the Harness Server over HTTP.
func RunHTTPHarness(ctx context.Context, overrides core.CLIOverrides) error {
	svc, err := newServices(ctx, overrides)
	if err != nil {
		return err
	}
	defer svc.Close()

	{%- if "oauth1" in auth_method_keys %}
	if svc.Config.AuthMethod == core.AuthMethodOAuth1 {
		return &auth.Error{Message: "OAuth1 is not supported over HTTP transport: its signature must be recomputed per request against the destination URL using the server's consumer key/secret, which a simple incoming-header relay cannot provide. Use stdio transport for OAuth1-authenticated APIs, or switch auth_method."}
	}
	{%- endif %}
	headerLocation, headerName := auth.HeaderLocationFor(svc.Config.AuthMethod)

	// server.WithHTTPContextFunc is mcp-go's per-request (not per-session)
	// context-injection hook: it runs fresh for every incoming message
	// regardless of how long the underlying session's internal handling
	// lives, so extracting credentials here and threading them through ctx
	// works correctly across concurrent sessions — unlike a naive
	// middleware-set value, which would only cover the one HTTP request
	// that happened to carry the `initialize` call.
	mcpHandler := server.NewStreamableHTTPServer(svc.MCPServer, server.WithHTTPContextFunc(
		func(requestCtx context.Context, r *http.Request) context.Context {
			credentials, err := httptransport.ExtractRequestCredentials(r.Header, headerLocation, headerName)
			if err != nil {
				return requestCtx
			}
			return context.WithValue(requestCtx, auth.RequestCredentialsContextKey, credentials)
		},
	))
	handler, err := httptransport.NewHandler(mcpHandler, svc.Health, svc.Config.CorsAllow, headerLocation, headerName)
	if err != nil {
		return err
	}

	addr := fmt.Sprintf("%s:%d", svc.Config.Host, svc.Config.Port)
	fmt.Printf("listening on http://%s\n", addr)
	return http.ListenAndServe(addr, handler)
}

// PrintVersion prints the installed version.
func PrintVersion() {
	fmt.Println("1.0.0")
}

// PrintConfig prints the resolved configuration with secrets redacted
// (REQ-2.3.1's redaction principle applied to this output too).
func PrintConfig(overrides core.CLIOverrides) error {
	config, err := core.Load(overrides)
	if err != nil {
		return err
	}
	fmt.Printf("URL: %s\n", config.URL)
	fmt.Printf("AuthMethod: %s\n", config.AuthMethod)
	fmt.Println("Credentials: ***REDACTED***")
	fmt.Printf("LogLevel: %s\n", config.LogLevel)
	fmt.Printf("Transport: %s\n", config.Transport)
	fmt.Printf("Host: %s\n", config.Host)
	fmt.Printf("Port: %d\n", config.Port)
	return nil
}

// PrintVersions lists the API spec versions this project has a store for
// (v8 multi-version support), showing which is default/active.
func PrintVersions(overrides core.CLIOverrides) error {
	config, err := core.Load(overrides)
	if err != nil {
		return err
	}
	for _, row := range KnownVersions {
		var markers []string
		if row.IsDefault {
			markers = append(markers, "default")
		}
		if row.Label == config.ApiVersion {
			markers = append(markers, "active")
		}
		if len(markers) > 0 {
			fmt.Printf("%s (%s)\n", row.Label, strings.Join(markers, ", "))
		} else {
			fmt.Println(row.Label)
		}
	}
	return nil
}

// RunSearch runs the search tool in-process, printing its result — the
// same tool implementation the MCP-protocol path uses (Story G6's
// tools.Search), just called directly instead of over stdio/HTTP.
func RunSearch(ctx context.Context, overrides core.CLIOverrides, query string, limit int) error {
	svc, err := newServices(ctx, overrides)
	if err != nil {
		return err
	}
	defer svc.Close()

	results, err := tools.Search(ctx, svc.VectorStore, query, limit)
	if err != nil {
		return err
	}
	return printJSON(results)
}

// RunGet runs the get tool in-process, printing its result.
func RunGet(ctx context.Context, overrides core.CLIOverrides, operationID string) error {
	svc, err := newServices(ctx, overrides)
	if err != nil {
		return err
	}
	defer svc.Close()

	result, err := tools.Get(svc.Store, operationID)
	if err != nil {
		return err
	}
	return printJSON(result)
}

// RunCall runs the call tool in-process, printing its result.
func RunCall(ctx context.Context, overrides core.CLIOverrides, operationID, argsJSON string) error {
	svc, err := newServices(ctx, overrides)
	if err != nil {
		return err
	}
	defer svc.Close()

	var args map[string]any
	if argsJSON != "" {
		if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
			return fmt.Errorf("invalid --args JSON: %w", err)
		}
	}

	result, err := tools.Call(ctx, svc.Store, svc.ApiClient, svc.AuthManager, svc.Config.ApiVersion, operationID, args, nil)
	if err != nil {
		return err
	}
	return printJSON(result)
}

// RunPopulateEmbeddings backfills the vector store, printing how many
// operations were indexed.
func RunPopulateEmbeddings(ctx context.Context, overrides core.CLIOverrides) error {
	svc, err := newServices(ctx, overrides)
	if err != nil {
		return err
	}
	defer svc.Close()

	count, err := services.PopulateEmbeddings(ctx, svc.Store, svc.VectorStore)
	if err != nil {
		return err
	}
	fmt.Printf("populated %d embedding(s)\n", count)
	return nil
}

// RunSetup is implemented in setup.go (Story G7) — the interactive
// credential setup wizard.

// RunTestConnection verifies the configured API URL is reachable.
func RunTestConnection(ctx context.Context, overrides core.CLIOverrides) error {
	config, err := core.Load(overrides)
	if err != nil {
		return err
	}

	client := &http.Client{Timeout: 5 * time.Second}
	req, err := http.NewRequestWithContext(ctx, http.MethodHead, config.URL, http.NoBody)
	if err != nil {
		return err
	}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Printf("unreachable: %v\n", err)
		return err
	}
	defer func() { _ = resp.Body.Close() }()
	fmt.Printf("reachable (status %d)\n", resp.StatusCode)
	return nil
}

func printJSON(value any) error {
	encoded, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		return err
	}
	fmt.Println(string(encoded))
	return nil
}
