/*
 * ChatCLI - Command Line Interface for LLM interaction
 * Copyright (c) 2024 Edilson Freitas
 * License: Apache-2.0
 */
package remote

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/diillson/chatcli/models"
	pb "github.com/diillson/chatcli/proto/chatcli/v1"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/keepalive"
	"google.golang.org/grpc/metadata"
)

// Client implements llm/client.LLMClient by delegating to a remote ChatCLI gRPC server.
type Client struct {
	conn           *grpc.ClientConn
	grpcClient     pb.ChatCLIServiceClient
	token          string            // server auth token
	clientAPIKey   string            // client's own LLM provider API key (forwarded to server)
	overProvider   string            // client-requested provider override (sent in every request)
	overModel      string            // client-requested model override (sent in every request)
	providerConfig map[string]string // provider-specific config (StackSpot realm/agent_id, Ollama base_url, etc.)
	model          string            // effective model name (for display)
	provider       string            // effective provider name (for display)
	logger         *zap.Logger
}

// Config holds remote client configuration.
type Config struct {
	Address        string            // server address (host:port)
	Token          string            // auth token for gRPC server authentication
	TLS            bool              // use TLS
	CertFile       string            // CA certificate file for TLS (optional)
	ClientAPIKey   string            // optional: client's own LLM API key/OAuth token (forwarded to server)
	Provider       string            // optional: override server's default provider (e.g., "GOOGLEAI")
	Model          string            // optional: override server's default model (e.g., "gemini-2.0-flash")
	ProviderConfig map[string]string // optional: provider-specific config (StackSpot, Ollama, etc.)
}

// NewClient creates a new remote gRPC client that implements LLMClient.
func NewClient(ctx context.Context, cfg Config, logger *zap.Logger) (*Client, error) {
	var dialOpts []grpc.DialOption

	// Security (H4): TLS configuration with TLS 1.3 minimum.
	// Use CHATCLI_TLS_CLIENT_CERT/KEY for mTLS client certificates.
	if cfg.TLS {
		tlsCfg := &tls.Config{
			MinVersion: tls.VersionTLS13,
		}

		if cfg.CertFile != "" {
			caCert, err := os.ReadFile(cfg.CertFile)
			if err != nil {
				return nil, fmt.Errorf("failed to read CA certificate: %w", err)
			}
			certPool := x509.NewCertPool()
			if !certPool.AppendCertsFromPEM(caCert) {
				return nil, fmt.Errorf("failed to parse CA certificate")
			}
			tlsCfg.RootCAs = certPool
		}

		// mTLS: load client certificate if configured
		if certPath := os.Getenv("CHATCLI_TLS_CLIENT_CERT"); certPath != "" {
			keyPath := os.Getenv("CHATCLI_TLS_CLIENT_KEY")
			if keyPath == "" {
				return nil, fmt.Errorf("CHATCLI_TLS_CLIENT_CERT set but CHATCLI_TLS_CLIENT_KEY is missing")
			}
			clientCert, err := tls.LoadX509KeyPair(certPath, keyPath)
			if err != nil {
				return nil, fmt.Errorf("failed to load client TLS cert/key: %w", err)
			}
			tlsCfg.Certificates = []tls.Certificate{clientCert}
		}

		dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)))
	} else {
		// Security: Require explicit opt-in for insecure connections
		if strings.EqualFold(os.Getenv("CHATCLI_ALLOW_INSECURE"), "true") {
			logger.Warn("SECURITY WARNING: TLS is disabled. Connection is unencrypted. Set CHATCLI_ALLOW_INSECURE=false for production.")
			dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
		} else {
			logger.Warn("TLS disabled but CHATCLI_ALLOW_INSECURE not set — using system TLS")
			dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
				MinVersion: tls.VersionTLS13,
			})))
		}
	}

	// Keepalive: detect dead connections without flooding the server.
	// Server EnforcementPolicy.MinTime is 20s, so Time must be >= 20s.
	dialOpts = append(dialOpts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
		Time:                30 * time.Second, // ping every 30s if no activity
		Timeout:             5 * time.Second,  // wait 5s for pong before considering dead
		PermitWithoutStream: true,             // ping even without active RPCs
	}))

	// Client-side round-robin load balancing for headless Services with multiple replicas
	dialOpts = append(dialOpts, grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`))

	// Use dns:/// resolver to enable address re-resolution and round-robin
	address := cfg.Address
	if !strings.Contains(address, "://") {
		address = "dns:///" + address
	}

	conn, err := grpc.NewClient(address, dialOpts...)
	if err != nil {
		return nil, fmt.Errorf("failed to connect to %s: %w", cfg.Address, err)
	}

	grpcClient := pb.NewChatCLIServiceClient(conn)

	c := &Client{
		conn:           conn,
		grpcClient:     grpcClient,
		token:          cfg.Token,
		clientAPIKey:   cfg.ClientAPIKey,
		overProvider:   cfg.Provider,
		overModel:      cfg.Model,
		providerConfig: cfg.ProviderConfig,
		logger:         logger,
	}

	// Fetch server info to get the default model/provider
	authCtx := c.withAuth(ctx)
	info, err := grpcClient.GetServerInfo(authCtx, &pb.GetServerInfoRequest{})
	if err != nil {
		logger.Warn("Failed to fetch server info, using defaults", zap.Error(err))
		c.model = "remote"
		c.provider = "remote"
	} else {
		c.model = info.Model
		c.provider = info.Provider
		logger.Info("Connected to remote ChatCLI server",
			zap.String("version", info.Version),
			zap.String("provider", info.Provider),
			zap.String("model", info.Model),
		)
	}

	// Client-side overrides take precedence over server defaults
	if c.overProvider != "" {
		c.provider = c.overProvider
	}
	if c.overModel != "" {
		c.model = c.overModel
	}

	return c, nil
}

// withAuth adds the authorization metadata to the context.
func (c *Client) withAuth(ctx context.Context) context.Context {
	if c.token == "" {
		return ctx
	}
	md := metadata.Pairs("authorization", "Bearer "+c.token)
	return metadata.NewOutgoingContext(ctx, md)
}

// GetModelName returns the remote server's model name.
// This satisfies the LLMClient interface.
func (c *Client) GetModelName() string {
	return c.model
}

// SendPrompt sends a prompt to the remote server and returns the response.
// This satisfies the LLMClient interface.
func (c *Client) SendPrompt(ctx context.Context, prompt string, history []models.Message, maxTokens int) (string, error) {
	ctx = c.withAuth(ctx)

	protoHistory := make([]*pb.ChatMessage, 0, len(history))
	for _, msg := range history {
		protoHistory = append(protoHistory, &pb.ChatMessage{
			Role:    msg.Role,
			Content: msg.Content,
		})
	}

	resp, err := c.grpcClient.SendPrompt(ctx, &pb.SendPromptRequest{
		Prompt:         prompt,
		History:        protoHistory,
		MaxTokens:      int32(maxTokens), //#nosec G115 -- value bounded by domain (counts/versions/fd)
		Provider:       c.overProvider,
		Model:          c.overModel,
		ClientApiKey:   c.clientAPIKey,
		ProviderConfig: c.providerConfig,
	})
	if err != nil {
		return "", fmt.Errorf("remote SendPrompt failed: %w", err)
	}

	// Update model/provider from server response when not explicitly set by the client.
	// This allows the display to show the actual model being used on the server.
	if resp.Model != "" && c.overModel == "" {
		c.model = resp.Model
	}
	if resp.Provider != "" && c.overProvider == "" {
		c.provider = resp.Provider
	}

	return resp.Response, nil
}

// GetProvider returns the remote server's provider.
func (c *Client) GetProvider() string {
	return c.provider
}

// ListSessions lists sessions from the remote server.
func (c *Client) ListSessions(ctx context.Context) ([]string, error) {
	ctx = c.withAuth(ctx)
	resp, err := c.grpcClient.ListSessions(ctx, &pb.ListSessionsRequest{})
	if err != nil {
		return nil, fmt.Errorf("remote ListSessions failed: %w", err)
	}
	return resp.Sessions, nil
}

// LoadSession loads a session from the remote server.
func (c *Client) LoadSession(ctx context.Context, name string) ([]models.Message, error) {
	ctx = c.withAuth(ctx)
	resp, err := c.grpcClient.LoadSession(ctx, &pb.LoadSessionRequest{Name: name})
	if err != nil {
		return nil, fmt.Errorf("remote LoadSession failed: %w", err)
	}

	messages := make([]models.Message, 0, len(resp.Messages))
	for _, m := range resp.Messages {
		messages = append(messages, models.Message{
			Role:    m.Role,
			Content: m.Content,
		})
	}
	return messages, nil
}

// SaveSession saves a session to the remote server.
func (c *Client) SaveSession(ctx context.Context, name string, history []models.Message) error {
	ctx = c.withAuth(ctx)
	protoMsgs := make([]*pb.ChatMessage, 0, len(history))
	for _, msg := range history {
		protoMsgs = append(protoMsgs, &pb.ChatMessage{
			Role:    msg.Role,
			Content: msg.Content,
		})
	}
	_, err := c.grpcClient.SaveSession(ctx, &pb.SaveSessionRequest{
		Name:     name,
		Messages: protoMsgs,
	})
	if err != nil {
		return fmt.Errorf("remote SaveSession failed: %w", err)
	}
	return nil
}

// SaveSessionV2 saves a full v2 session (with scoped histories) to the remote server.
// It also populates the v1 messages field for backward compatibility with older servers.
func (c *Client) SaveSessionV2(ctx context.Context, name string, sd *models.SessionData) error {
	ctx = c.withAuth(ctx)
	_, err := c.grpcClient.SaveSession(ctx, &pb.SaveSessionRequest{
		Name:        name,
		Messages:    modelsToProtoV2(sd.ChatHistory), // v1 compat
		SessionData: modelsSessionDataToProto(sd),    // v2 full data
	})
	if err != nil {
		return fmt.Errorf("remote SaveSessionV2 failed: %w", err)
	}
	return nil
}

// LoadSessionV2 loads a full v2 session from the remote server.
// Falls back to v1 (chat history only) if the server doesn't support v2.
func (c *Client) LoadSessionV2(ctx context.Context, name string) (*models.SessionData, error) {
	ctx = c.withAuth(ctx)
	resp, err := c.grpcClient.LoadSession(ctx, &pb.LoadSessionRequest{Name: name})
	if err != nil {
		return nil, fmt.Errorf("remote LoadSessionV2 failed: %w", err)
	}

	// Prefer v2 if server returned session_data
	if resp.SessionData != nil {
		return protoSessionDataToModels(resp.SessionData), nil
	}

	// Fallback to v1
	messages := protoToModelsV2(resp.Messages)
	return &models.SessionData{Version: 2, ChatHistory: messages}, nil
}

// --- V2 session proto ↔ models converters ---

func modelMessageToProto(m models.Message) *pb.ChatMessage {
	cm := &pb.ChatMessage{
		Role:    m.Role,
		Content: m.Content,
	}
	if m.Meta != nil {
		cm.Meta = &pb.MessageMeta{
			IsSummary: m.Meta.IsSummary,
			SummaryOf: int32(m.Meta.SummaryOf), //#nosec G115 -- value bounded by domain (counts/versions/fd)
			Mode:      m.Meta.Mode,
		}
	}
	return cm
}

func protoMessageToModel(cm *pb.ChatMessage) models.Message {
	msg := models.Message{
		Role:    cm.Role,
		Content: cm.Content,
	}
	if cm.Meta != nil {
		msg.Meta = &models.MessageMeta{
			IsSummary: cm.Meta.IsSummary,
			SummaryOf: int(cm.Meta.SummaryOf),
			Mode:      cm.Meta.Mode,
		}
	}
	return msg
}

func modelsToProtoV2(msgs []models.Message) []*pb.ChatMessage {
	out := make([]*pb.ChatMessage, 0, len(msgs))
	for _, m := range msgs {
		out = append(out, modelMessageToProto(m))
	}
	return out
}

func protoToModelsV2(msgs []*pb.ChatMessage) []models.Message {
	out := make([]models.Message, 0, len(msgs))
	for _, m := range msgs {
		out = append(out, protoMessageToModel(m))
	}
	return out
}

func modelsSessionDataToProto(sd *models.SessionData) *pb.SessionDataV2 {
	return &pb.SessionDataV2{
		Version:      int32(sd.Version), //#nosec G115 -- value bounded by domain (counts/versions/fd)
		ChatHistory:  modelsToProtoV2(sd.ChatHistory),
		AgentHistory: modelsToProtoV2(sd.AgentHistory),
		CoderHistory: modelsToProtoV2(sd.CoderHistory),
		SharedMemory: modelsToProtoV2(sd.SharedMemory),
	}
}

func protoSessionDataToModels(psd *pb.SessionDataV2) *models.SessionData {
	return &models.SessionData{
		Version:      int(psd.Version),
		ChatHistory:  protoToModelsV2(psd.ChatHistory),
		AgentHistory: protoToModelsV2(psd.AgentHistory),
		CoderHistory: protoToModelsV2(psd.CoderHistory),
		SharedMemory: protoToModelsV2(psd.SharedMemory),
	}
}

// DeleteSession deletes a session from the remote server.
func (c *Client) DeleteSession(ctx context.Context, name string) error {
	ctx = c.withAuth(ctx)
	_, err := c.grpcClient.DeleteSession(ctx, &pb.DeleteSessionRequest{Name: name})
	if err != nil {
		return fmt.Errorf("remote DeleteSession failed: %w", err)
	}
	return nil
}

// Health checks if the remote server is healthy.
func (c *Client) Health(ctx context.Context) (bool, string, error) {
	ctx = c.withAuth(ctx)
	resp, err := c.grpcClient.Health(ctx, &pb.HealthRequest{})
	if err != nil {
		return false, "", fmt.Errorf("health check failed: %w", err)
	}
	return resp.Status == pb.HealthResponse_SERVING, resp.Version, nil
}

// ServerInfo holds server metadata returned by GetServerInfo.
type ServerInfo struct {
	Version            string
	Provider           string
	Model              string
	AvailableProviders []string
	WatcherActive      bool
	WatcherTarget      string
	PluginCount        int32
	AgentCount         int32
	SkillCount         int32
}

// GetServerInfo fetches server metadata including watcher status.
func (c *Client) GetServerInfo(ctx context.Context) (*ServerInfo, error) {
	ctx = c.withAuth(ctx)
	resp, err := c.grpcClient.GetServerInfo(ctx, &pb.GetServerInfoRequest{})
	if err != nil {
		return nil, fmt.Errorf("GetServerInfo failed: %w", err)
	}
	return &ServerInfo{
		Version:            resp.Version,
		Provider:           resp.Provider,
		Model:              resp.Model,
		AvailableProviders: resp.AvailableProviders,
		WatcherActive:      resp.WatcherActive,
		WatcherTarget:      resp.WatcherTarget,
		PluginCount:        resp.PluginCount,
		AgentCount:         resp.AgentCount,
		SkillCount:         resp.SkillCount,
	}, nil
}

// WatcherStatus holds the K8s watcher status from the server.
type WatcherStatus struct {
	Active        bool
	Deployment    string
	Namespace     string
	StatusSummary string
	AlertCount    int
	SnapshotCount int
	PodCount      int
}

// GetWatcherStatus fetches the K8s watcher status from the server.
func (c *Client) GetWatcherStatus(ctx context.Context) (*WatcherStatus, error) {
	ctx = c.withAuth(ctx)
	resp, err := c.grpcClient.GetWatcherStatus(ctx, &pb.GetWatcherStatusRequest{})
	if err != nil {
		return nil, fmt.Errorf("GetWatcherStatus failed: %w", err)
	}
	return &WatcherStatus{
		Active:        resp.Active,
		Deployment:    resp.Deployment,
		Namespace:     resp.Namespace,
		StatusSummary: resp.StatusSummary,
		AlertCount:    int(resp.AlertCount),
		SnapshotCount: int(resp.SnapshotCount),
		PodCount:      int(resp.PodCount),
	}, nil
}

// Close closes the gRPC connection.
func (c *Client) Close() error {
	if c.conn != nil {
		return c.conn.Close()
	}
	return nil
}
