// Package synap provides a production-ready Go client for the Synap cognitive memory system.
//
// Synap is a biologically-inspired memory database with spreading activation, episodic memory,
// and automatic consolidation. This client is designed for use in chat systems to provide
// context-aware AI responses with memory continuity.
//
// Usage Example:
//
//	client, err := synap.NewClient("http://localhost:7432", &synap.Config{
//	    Timeout:       10 * time.Second,
//	    MaxRetries:    3,
//	    DefaultSpace:  "conversation_abc123",
//	})
//	if err != nil {
//	    log.Fatal(err)
//	}
//
//	// Store a chat message as episodic memory
//	episode := &synap.Episode{
//	    What:       "User asked about their favorite hobbies",
//	    When:       time.Now(),
//	    Who:        []string{"user_alice", "agent_sarah"},
//	    Where:      "chat",
//	    Confidence: 0.85,
//	}
//	memoryID, err := client.RememberEpisode(ctx, episode)
//
//	// Recall relevant memories for context
//	memories, err := client.Recall(ctx, &synap.RecallRequest{
//	    Query:      "hobbies photography travel",
//	    Mode:       synap.RecallModeHybrid,
//	    MaxResults: 10,
//	    Threshold:  0.5,
//	})
package synap

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"strings"
	"time"

	"{{GO_MODULE}}/pkg/httpclient"
)

// Client provides access to the Synap memory system.
type Client struct {
	baseURL    string
	httpClient *httpclient.Client
	logger     *slog.Logger
	config     *Config
}

// Config holds configuration for the Synap client.
type Config struct {
	// Timeout for HTTP requests (default: 10s)
	Timeout time.Duration

	// MaxRetries for failed requests (default: 3)
	MaxRetries int

	// DefaultSpace for multi-tenant isolation (optional)
	// If set, all requests will use this space unless overridden
	DefaultSpace string

	// APIKey for authentication (optional)
	// If set, requests will include "Authorization: Bearer <key>" header
	APIKey string

	// Logger for structured logging (optional)
	Logger *slog.Logger
}

// NewClient creates a new Synap client.
//
// baseURL should be the Synap server URL (e.g., "http://localhost:7432")
// config is optional - pass nil to use defaults
//
// Returns nil and an error if baseURL is invalid.
func NewClient(baseURL string, config *Config) (*Client, error) {
	if baseURL == "" {
		return nil, fmt.Errorf("baseURL is required")
	}

	// Validate baseURL format
	parsedURL, err := url.Parse(baseURL)
	if err != nil {
		return nil, fmt.Errorf("invalid baseURL: %w", err)
	}

	if parsedURL.Scheme == "" {
		return nil, fmt.Errorf("baseURL must include scheme (http:// or https://)")
	}

	if parsedURL.Host == "" {
		return nil, fmt.Errorf("baseURL must include host")
	}

	if config == nil {
		config = &Config{}
	}

	if config.Timeout == 0 {
		config.Timeout = 10 * time.Second
	}

	if config.MaxRetries == 0 {
		config.MaxRetries = 3
	}

	if config.Logger == nil {
		config.Logger = slog.Default()
	}

	return &Client{
		baseURL: baseURL,
		httpClient: httpclient.New(httpclient.Config{
			Timeout:    config.Timeout,
			MaxRetries: config.MaxRetries,
			Logger:     config.Logger,
		}),
		logger: config.Logger,
		config: config,
	}, nil
}

// Episode represents an episodic memory (what/when/where/who/why/how).
//
// Episodic memories are rich contextual memories that capture events with
// multiple dimensions. They consolidate over time into semantic patterns.
type Episode struct {
	// What happened (required)
	What string `json:"what"`

	// When it happened (required for temporal context)
	When time.Time `json:"when"`

	// Where it happened (optional, e.g., "chat", "email", "phone")
	Where string `json:"where,omitempty"`

	// Who was involved (optional, e.g., ["user_alice", "agent_sarah"])
	Who []string `json:"who,omitempty"`

	// Why it happened (optional, context/motivation)
	Why string `json:"why,omitempty"`

	// How it happened (optional, method/process)
	How string `json:"how,omitempty"`

	// Confidence in memory accuracy (0.0-1.0, default: 0.7)
	// Higher confidence = stronger memory encoding
	Confidence float64 `json:"confidence"`

	// Tags for categorization (optional)
	Tags []string `json:"tags,omitempty"`
}

// RememberEpisodeResponse contains the result of storing an episode.
type RememberEpisodeResponse struct {
	// MemoryID is the unique identifier for this episode
	MemoryID string `json:"memory_id"`

	// StorageConfidence reflects Synap's assessment of storage quality
	StorageConfidence ConfidenceScore `json:"storage_confidence"`

	// ConsolidationState indicates memory age ("Recent", "Consolidating", "Semantic")
	ConsolidationState string `json:"consolidation_state"`

	// ObservedAt is when the event originally occurred
	ObservedAt time.Time `json:"observed_at"`

	// StoredAt is when Synap stored the memory
	StoredAt time.Time `json:"stored_at"`

	// SystemMessage provides feedback about storage
	SystemMessage string `json:"system_message"`
}

// ConfidenceScore represents Synap's confidence in a memory or recall.
type ConfidenceScore struct {
	// Value is the numeric confidence (0.0-1.0)
	Value float64 `json:"value"`

	// Category is the human-readable category ("Low", "Medium", "High", "Very High")
	Category string `json:"category"`

	// Reasoning explains how confidence was calculated (optional)
	Reasoning string `json:"reasoning,omitempty"`
}

// RememberResponse contains the result of storing a simple semantic memory.
type RememberResponse struct {
	// MemoryID is the unique identifier for this memory
	MemoryID string `json:"memory_id"`

	// ObservedAt is when the memory was created
	ObservedAt time.Time `json:"observed_at"`

	// StoredAt is when Synap stored the memory
	StoredAt time.Time `json:"stored_at"`

	// StorageConfidence reflects Synap's assessment of storage quality
	StorageConfidence ConfidenceScore `json:"storage_confidence"`
}

// Remember stores a simple semantic memory in Synap.
//
// This is a convenience method for storing straightforward facts or observations
// without the full episodic context (what/when/where/who/why/how).
// For richer contextual memories, use RememberEpisode instead.
//
// The memory will be stored in the space specified in the client config,
// or you can override it with WithSpace in the context.
//
// Example:
//
//	memoryID, err := client.Remember(ctx, "User loves hiking and photography", 0.85)
func (c *Client) Remember(ctx context.Context, content string, confidence float64) (*RememberResponse, error) {
	if content == "" {
		return nil, fmt.Errorf("content is required")
	}

	if confidence == 0 {
		confidence = 0.7 // Default confidence
	}

	space := getSpace(ctx, c.config.DefaultSpace)
	endpoint := c.buildURL("/api/v1/memories/remember", map[string]string{
		"space": space,
	})

	reqBody := map[string]interface{}{
		"content":    content,
		"confidence": confidence,
	}

	body, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	if c.config.APIKey != "" {
		req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	}

	c.logger.Debug("storing semantic memory in synap",
		"content_length", len(content),
		"space", space,
		"confidence", confidence)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("execute request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			return nil, fmt.Errorf("synap API error (HTTP %d): failed to read error body: %w", resp.StatusCode, readErr)
		}
		return nil, fmt.Errorf("synap API error (HTTP %d): %s", resp.StatusCode, bodyBytes)
	}

	var result RememberResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	c.logger.Info("semantic memory stored in synap",
		"memory_id", result.MemoryID,
		"confidence", result.StorageConfidence.Value,
		"space", space)

	return &result, nil
}

// RememberEpisode stores an episodic memory in Synap.
//
// The memory will be stored in the space specified in the client config,
// or you can override it with WithSpace in the context.
//
// Example:
//
//	episode := &synap.Episode{
//	    What:       "User mentioned they love hiking and photography",
//	    When:       time.Now(),
//	    Who:        []string{"user_alice"},
//	    Confidence: 0.85,
//	}
//	memoryID, err := client.RememberEpisode(ctx, episode)
func (c *Client) RememberEpisode(ctx context.Context, episode *Episode) (*RememberEpisodeResponse, error) {
	if episode.What == "" {
		return nil, fmt.Errorf("episode.What is required")
	}

	if episode.When.IsZero() {
		return nil, fmt.Errorf("episode.When is required")
	}

	if episode.Confidence == 0 {
		episode.Confidence = 0.7 // Default confidence
	}

	space := getSpace(ctx, c.config.DefaultSpace)
	endpoint := c.buildURL("/api/v1/episodes/remember", map[string]string{
		"space": space,
	})

	body, err := json.Marshal(episode)
	if err != nil {
		return nil, fmt.Errorf("marshal episode: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	if c.config.APIKey != "" {
		req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	}

	c.logger.Debug("storing episode in synap",
		"what", episode.What,
		"space", space,
		"confidence", episode.Confidence)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("execute request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			return nil, fmt.Errorf("synap API error (HTTP %d): failed to read error body: %w", resp.StatusCode, readErr)
		}
		return nil, fmt.Errorf("synap API error (HTTP %d): %s", resp.StatusCode, bodyBytes)
	}

	var result RememberEpisodeResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	c.logger.Info("episode stored in synap",
		"memory_id", result.MemoryID,
		"confidence", result.StorageConfidence.Value,
		"space", space)

	return &result, nil
}

// RecallMode specifies how memories should be retrieved.
type RecallMode string

const (
	// RecallModeSimilarity uses vector similarity search only
	RecallModeSimilarity RecallMode = "similarity"

	// RecallModeSpreading uses spreading activation through memory graph
	RecallModeSpreading RecallMode = "spreading"

	// RecallModeHybrid combines similarity and spreading (recommended for chat)
	RecallModeHybrid RecallMode = "hybrid"
)

// RecallRequest specifies parameters for memory recall.
type RecallRequest struct {
	// Query is the natural language search query (required)
	Query string

	// Mode specifies recall strategy (default: hybrid)
	Mode RecallMode

	// MaxResults limits the number of memories returned (default: 10)
	MaxResults int

	// Threshold is the minimum confidence for results (0.0-1.0, default: 0.5)
	Threshold float64

	// FromTime filters memories after this time (optional)
	FromTime *time.Time

	// ToTime filters memories before this time (optional)
	ToTime *time.Time

	// RequiredTags filters to memories with these tags (optional)
	RequiredTags []string

	// ExcludedTags filters out memories with these tags (optional)
	ExcludedTags []string

	// TraceActivation returns activation spreading details (for debugging)
	TraceActivation bool
}

// Memory represents a recalled memory from Synap.
type Memory struct {
	// ID is the unique memory identifier
	ID string `json:"id"`

	// Content is the memory content (for semantic memories)
	// For episodes, use Episode field instead
	Content string `json:"content,omitempty"`

	// Episode contains episodic memory details (what/when/where/who/why/how)
	Episode *EpisodeDetails `json:"episode,omitempty"`

	// Confidence in the memory accuracy
	Confidence ConfidenceScore `json:"confidence"`

	// ActivationLevel shows how "awake" this memory is (0.0-1.0)
	// Higher activation = more recently/frequently accessed
	ActivationLevel float64 `json:"activation_level"`

	// SimilarityScore shows relevance to the query (0.0-1.0)
	SimilarityScore float64 `json:"similarity_score"`

	// ObservedAt is when the original event occurred
	ObservedAt time.Time `json:"observed_at,omitempty"`

	// Tags categorize the memory
	Tags []string `json:"tags,omitempty"`
}

// EpisodeDetails contains the rich context of an episodic memory.
type EpisodeDetails struct {
	What  string    `json:"what"`
	When  time.Time `json:"when"`
	Where string    `json:"where,omitempty"`
	Who   []string  `json:"who,omitempty"`
	Why   string    `json:"why,omitempty"`
	How   string    `json:"how,omitempty"`
}

// RecallResponse contains memories recalled from Synap.
type RecallResponse struct {
	// Memories organized by vividness
	Memories MemoryCategories `json:"memories"`

	// RecallConfidence indicates overall recall quality
	RecallConfidence ConfidenceScore `json:"recall_confidence"`

	// QueryAnalysis provides insights into the search
	QueryAnalysis QueryAnalysis `json:"query_analysis"`

	// SystemMessage provides feedback
	SystemMessage string `json:"system_message"`
}

// MemoryCategories groups memories by their vividness/confidence.
type MemoryCategories struct {
	// Vivid memories are high-confidence, directly relevant
	Vivid []Memory `json:"vivid"`

	// Associated memories are related through spreading activation
	Associated []Memory `json:"associated"`

	// Reconstructed memories are pattern-completed from partial information
	Reconstructed []Memory `json:"reconstructed"`
}

// QueryAnalysis provides insights into how the query was processed.
type QueryAnalysis struct {
	// UnderstoodIntent is Synap's interpretation of the query
	UnderstoodIntent string `json:"understood_intent"`

	// SearchStrategy describes the retrieval approach
	SearchStrategy string `json:"search_strategy"`

	// CognitiveLoad indicates query complexity ("Low", "Medium", "High")
	CognitiveLoad string `json:"cognitive_load"`

	// Suggestions for improving recall (optional)
	Suggestions []string `json:"suggestions,omitempty"`
}

// Recall retrieves relevant memories from Synap.
//
// Example:
//
//	memories, err := client.Recall(ctx, &synap.RecallRequest{
//	    Query:      "hobbies and interests",
//	    Mode:       synap.RecallModeHybrid,
//	    MaxResults: 10,
//	    Threshold:  0.5,
//	})
//
//	for _, memory := range memories.Memories.Vivid {
//	    fmt.Printf("Memory: %s (confidence: %.2f)\n", memory.Content, memory.Confidence.Value)
//	}
func (c *Client) Recall(ctx context.Context, req *RecallRequest) (*RecallResponse, error) {
	if req.Query == "" {
		return nil, fmt.Errorf("query is required")
	}

	if req.Mode == "" {
		req.Mode = RecallModeHybrid
	}

	if req.MaxResults == 0 {
		req.MaxResults = 10
	}

	if req.Threshold == 0 {
		req.Threshold = 0.5
	}

	space := getSpace(ctx, c.config.DefaultSpace)

	// Build query parameters
	params := map[string]string{
		"query":       req.Query,
		"mode":        string(req.Mode),
		"max_results": fmt.Sprintf("%d", req.MaxResults),
		"threshold":   fmt.Sprintf("%.2f", req.Threshold),
		"space":       space,
	}

	if req.FromTime != nil {
		params["from_time"] = req.FromTime.Format(time.RFC3339)
	}

	if req.ToTime != nil {
		params["to_time"] = req.ToTime.Format(time.RFC3339)
	}

	if len(req.RequiredTags) > 0 {
		params["required_tags"] = strings.Join(req.RequiredTags, ",")
	}

	if len(req.ExcludedTags) > 0 {
		params["excluded_tags"] = strings.Join(req.ExcludedTags, ",")
	}

	if req.TraceActivation {
		params["trace_activation"] = "true"
	}

	endpoint := c.buildURL("/api/v1/memories/recall", params)

	httpReq, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	if c.config.APIKey != "" {
		httpReq.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	}

	c.logger.Debug("recalling memories from synap",
		"query", req.Query,
		"mode", req.Mode,
		"space", space)

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("execute request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			return nil, fmt.Errorf("synap API error (HTTP %d): failed to read error body: %w", resp.StatusCode, readErr)
		}
		return nil, fmt.Errorf("synap API error (HTTP %d): %s", resp.StatusCode, bodyBytes)
	}

	var result RecallResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	totalMemories := len(result.Memories.Vivid) + len(result.Memories.Associated) + len(result.Memories.Reconstructed)
	c.logger.Info("recalled memories from synap",
		"total", totalMemories,
		"vivid", len(result.Memories.Vivid),
		"associated", len(result.Memories.Associated),
		"confidence", result.RecallConfidence.Value,
		"space", space)

	return &result, nil
}

// Health checks if the Synap server is reachable and healthy.
func (c *Client) Health(ctx context.Context) error {
	endpoint := c.baseURL + "/api/v1/system/health"

	req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}

	if c.config.APIKey != "" {
		req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("execute request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			return fmt.Errorf("synap health check failed (HTTP %d): failed to read error body: %w", resp.StatusCode, readErr)
		}
		return fmt.Errorf("synap health check failed (HTTP %d): %s", resp.StatusCode, bodyBytes)
	}

	return nil
}

// buildURL constructs a URL with query parameters.
func (c *Client) buildURL(path string, params map[string]string) string {
	u, _ := url.Parse(c.baseURL + path)

	q := u.Query()
	for k, v := range params {
		if v != "" {
			q.Set(k, v)
		}
	}

	u.RawQuery = q.Encode()
	return u.String()
}

// Context keys for per-request configuration.
type contextKey string

const spaceContextKey contextKey = "synap_space"

// WithSpace returns a context with a specific memory space override.
//
// This overrides the default space configured in the client for a single request.
//
// Example:
//
//	ctx := synap.WithSpace(ctx, "conversation_xyz")
//	client.RememberEpisode(ctx, episode) // Uses conversation_xyz space
func WithSpace(ctx context.Context, space string) context.Context {
	return context.WithValue(ctx, spaceContextKey, space)
}

// getSpace retrieves the space from context, falling back to default.
func getSpace(ctx context.Context, defaultSpace string) string {
	if space, ok := ctx.Value(spaceContextKey).(string); ok && space != "" {
		return space
	}
	return defaultSpace
}
