package agent

import (
	"context"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"
)

const (
	agentteamLLMBridgeSessionExpirySkew    = 5 * time.Second
	agentteamLLMBridgeRuntimeReadyBackoff  = 100 * time.Millisecond
	agentteamRuntimeSessionNotReadyCode    = "agent_runtime_session_not_ready"
	agentteamRuntimeSessionNotReadyMessage = "agent runtime session is not ready"
)

// AgentTeamLLMProxySession is one MaaS runtime capability credential issued by AgentTeam.
type AgentTeamLLMProxySession struct {
	SessionID      string
	ProxyToken     string
	ExpiresAt      time.Time
	RefreshAt      *time.Time
	MaasBaseURL    string
	CredentialKind string
}

// AgentTeamLLMProxySessionIssuer issues and refreshes MaaS runtime capability credentials.
type AgentTeamLLMProxySessionIssuer interface {
	IssueLLMProxySession(ctx context.Context, selectorSessionID string) (AgentTeamLLMProxySession, error)
	RefreshLLMProxySession(ctx context.Context, selectorSessionID string) (AgentTeamLLMProxySession, error)
}

// AgentTeamMaaSLLMBridgeConfig configures the local AgentTeam MaaS LLM bridge.
type AgentTeamMaaSLLMBridgeConfig struct {
	UpstreamBaseURL         string
	Issuer                  AgentTeamLLMProxySessionIssuer
	RuntimeSessionID        string
	RuntimeSessionPublisher func(context.Context, string) error
	RuntimeDebugEnabled     bool
	AgentID                 string
	ServerAgentID           string
}

// AgentTeamMaaSLLMBridge exposes a loopback LLM endpoint for runtimes while it
// manages MaaS runtime capability credentials against the real AgentTeam server.
type AgentTeamMaaSLLMBridge struct {
	upstreamBaseURL               *url.URL
	basePath                      string
	localBaseURL                  string
	localAPIKey                   string
	issuer                        AgentTeamLLMProxySessionIssuer
	runtimeSessionID              string
	runtimeSessionPublisher       func(context.Context, string) error
	runtimeSessionNotReadyBackoff func(context.Context) error
	runtimeDebugEnabled           bool
	agentID                       string
	serverAgentID                 string
	httpClient                    *http.Client
	server                        *http.Server
	listener                      net.Listener

	mu      sync.Mutex
	session AgentTeamLLMProxySession

	closeOnce sync.Once
	closeErr  error
}

func newAgentTeamMaaSLLMBridgeHTTPClient() *http.Client {
	// MaaS LLM requests may be long-lived streams. Rely on the per-request
	// context for cancellation instead of truncating responses with Client.Timeout.
	return &http.Client{}
}

// StartAgentTeamMaaSLLMBridge starts a loopback HTTP bridge for one runtime launch.
func StartAgentTeamMaaSLLMBridge(ctx context.Context, cfg AgentTeamMaaSLLMBridgeConfig) (*AgentTeamMaaSLLMBridge, error) {
	if cfg.Issuer == nil {
		return nil, fmt.Errorf("agentteam llm bridge issuer is required")
	}
	upstreamBaseURL, err := url.Parse(strings.TrimSpace(cfg.UpstreamBaseURL))
	if err != nil {
		return nil, fmt.Errorf("parse agentteam llm bridge upstream base URL: %w", err)
	}
	if strings.TrimSpace(upstreamBaseURL.Scheme) == "" || strings.TrimSpace(upstreamBaseURL.Host) == "" {
		return nil, fmt.Errorf("agentteam llm bridge upstream base URL is invalid")
	}

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return nil, fmt.Errorf("listen agentteam llm bridge: %w", err)
	}
	localAPIKey, err := randomHex(24)
	if err != nil {
		_ = listener.Close()
		return nil, fmt.Errorf("generate agentteam llm bridge api key: %w", err)
	}

	basePath := strings.TrimRight(strings.TrimSpace(upstreamBaseURL.Path), "/")
	if basePath == "" {
		basePath = "/"
	}
	localBaseURL := (&url.URL{
		Scheme: "http",
		Host:   listener.Addr().String(),
		Path:   basePath,
	}).String()

	bridge := &AgentTeamMaaSLLMBridge{
		upstreamBaseURL:         upstreamBaseURL,
		basePath:                basePath,
		localBaseURL:            localBaseURL,
		localAPIKey:             localAPIKey,
		issuer:                  cfg.Issuer,
		runtimeSessionID:        strings.TrimSpace(cfg.RuntimeSessionID),
		runtimeSessionPublisher: cfg.RuntimeSessionPublisher,
		runtimeDebugEnabled:     cfg.RuntimeDebugEnabled,
		agentID:                 strings.TrimSpace(cfg.AgentID),
		serverAgentID:           strings.TrimSpace(cfg.ServerAgentID),
		httpClient:              newAgentTeamMaaSLLMBridgeHTTPClient(),
		listener:                listener,
	}
	mux := http.NewServeMux()
	if basePath == "/" {
		mux.HandleFunc("/", bridge.serveHTTP)
	} else {
		mux.HandleFunc(basePath, bridge.serveHTTP)
		mux.HandleFunc(basePath+"/", bridge.serveHTTP)
	}
	bridge.server = &http.Server{Handler: mux}
	bridge.debugLog("agentteam maas llm bridge starting",
		"base_url", sanitizeOpenCodeLogURL(localBaseURL),
		"upstream_base_url", sanitizeOpenCodeLogURL(upstreamBaseURL.String()),
		"runtime_session_id", bridge.runtimeSessionID,
	)
	if err := bridge.publishRuntimeSession(ctx); err != nil {
		_ = listener.Close()
		return nil, fmt.Errorf("publish agentteam runtime session before agentteam llm bridge start: %w", err)
	}

	go func() {
		<-ctx.Done()
		_ = bridge.Close()
	}()
	go func() {
		_ = bridge.server.Serve(listener)
	}()
	bridge.debugLog("agentteam maas llm bridge started",
		"base_url", sanitizeOpenCodeLogURL(localBaseURL),
		"runtime_session_id", bridge.runtimeSessionID,
	)

	return bridge, nil
}

// BaseURL returns the loopback base URL runtimes should use.
func (bridge *AgentTeamMaaSLLMBridge) BaseURL() string {
	if bridge == nil {
		return ""
	}
	return bridge.localBaseURL
}

// APIKey returns the loopback credential runtimes should use.
func (bridge *AgentTeamMaaSLLMBridge) APIKey() string {
	if bridge == nil {
		return ""
	}
	return bridge.localAPIKey
}

// Close stops the loopback bridge.
func (bridge *AgentTeamMaaSLLMBridge) Close() error {
	if bridge == nil || bridge.server == nil {
		return nil
	}
	bridge.closeOnce.Do(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		bridge.closeErr = bridge.server.Shutdown(ctx)
	})
	return bridge.closeErr
}

func (bridge *AgentTeamMaaSLLMBridge) debugLog(message string, attrs ...any) {
	if bridge == nil || !bridge.runtimeDebugEnabled {
		return
	}
	base := []any{
		"agent_id", strings.TrimSpace(bridge.agentID),
		"server_agent_id", strings.TrimSpace(bridge.serverAgentID),
	}
	slog.Info(message, scrubOpenCodeDebugLogAttrs(append(base, attrs...))...)
}
