package assistant

import (
	stdContext "context"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"time"

	"github.com/yaoapp/yao/agent/context"
	"github.com/yaoapp/yao/agent/i18n"
	"github.com/yaoapp/yao/agent/output/message"
	sandboxv2 "github.com/yaoapp/yao/agent/sandbox/v2"
	sandboxTypes "github.com/yaoapp/yao/agent/sandbox/v2/types"
	store "github.com/yaoapp/yao/agent/store/types"
	"github.com/yaoapp/yao/config"
	infraV2 "github.com/yaoapp/yao/sandbox/v2"
	traceTypes "github.com/yaoapp/yao/trace/types"
	"github.com/yaoapp/yao/workspace"
)

// HasSandboxV2 returns true if the assistant has a V2 sandbox configuration.
func (ast *Assistant) HasSandboxV2() bool {
	return ast.SandboxV2 != nil
}

// initSandboxV2 initializes the V2 sandbox: obtains a Computer, gets a Runner,
// runs Prepare, and returns the runner, computer, a per-request copy of the
// SandboxConfig, cleanup closure, loading message ID, and any error.
//
// A shallow copy of ast.SandboxV2 is made so that concurrent requests to the
// same assistant each get their own mutable config (Owner, ID, NodeID, etc.).
func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options) (
	sandboxTypes.Runner, infraV2.Computer, *sandboxTypes.SandboxConfig, func(), string, error,
) {
	cfgCopy := *ast.SandboxV2
	cfg := &cfgCopy
	manager := infraV2.M()

	loadingMsg := &message.Message{
		Type: message.TypeLoading,
		Props: map[string]any{
			"message": i18n.T(ctx.Locale, "sandbox.preparing"),
		},
	}
	loadingMsgID, _ := ctx.SendStream(loadingMsg)

	stdCtx := ctx.Context

	// 1. Resolve connector (before Computer so proxy env vars can be injected).
	conn, _, err := ast.GetConnector(ctx, opts)
	if err != nil && cfg.Runner.Name != "yao" {
		closeLoadingV2(ctx, loadingMsgID, "sandbox.failed")
		return nil, nil, nil, nil, "", fmt.Errorf("get connector: %w", err)
	}

	// 2. Build human-readable DisplayName from real Agent name + Workspace name.
	cfg.DisplayName = buildBoxDisplayName(ctx, ast.ID, ast.Name)

	// 2.5. Image existence check + pull (for box mode).
	if cfg.Computer.Image != "" && manager != nil {
		nodeID, kind, _ := sandboxv2.ResolveNodeID(ctx, cfg, manager)
		if kind == "box" && nodeID != "" {
			updateLoadingV2(ctx, loadingMsgID, "sandbox.starting")
			exists, existsErr := manager.ImageExists(stdCtx, nodeID, cfg.Computer.Image)
			if existsErr != nil {
				log.Printf("[sandbox/v2] image exists check failed on node %s: %v", nodeID, existsErr)
			}
			if existsErr == nil && !exists {
				updateLoadingV2(ctx, loadingMsgID, "sandbox.pulling_image")
				ch, pullErr := manager.PullImage(stdCtx, nodeID, cfg.Computer.Image, infraV2.ImagePullOptions{})
				if pullErr != nil {
					log.Printf("[sandbox/v2] image pull failed on node %s: %v (will retry in Create)", nodeID, pullErr)
				} else if ch != nil {
					for p := range ch {
						if p.Error != "" {
							log.Printf("[sandbox/v2] image pull progress error: %s", p.Error)
							break
						}
					}
				}
			}
		}
	}

	// 3. Obtain Computer.
	updateLoadingV2(ctx, loadingMsgID, "sandbox.starting")
	computer, identifier, err := sandboxv2.GetComputer(ctx, cfg, manager)
	if err != nil {
		closeLoadingV2(ctx, loadingMsgID, "sandbox.failed")
		return nil, nil, nil, nil, "", fmt.Errorf("getComputer failed: %w", err)
	}
	_ = identifier

	// 4. Get Runner.
	runner, err := sandboxv2.Get(cfg.Runner.Name)
	if err != nil {
		sandboxv2.LifecycleAction(stdCtx, cfg, computer, manager)
		closeLoadingV2(ctx, loadingMsgID, "sandbox.failed")
		return nil, nil, nil, nil, "", fmt.Errorf("get runner %q: %w", cfg.Runner.Name, err)
	}

	// 5. Resolve assistant directory and skills subdirectory.
	assistantDir := ""
	skillsDir := ""
	if ast.Path != "" {
		assistantDir = filepath.Join(config.Conf.AppSource, ast.Path)
		dir := filepath.Join(assistantDir, "skills")
		if info, e := os.Stat(dir); e == nil && info.IsDir() {
			skillsDir = dir
		}
	}

	// 6. Convert MCP servers.
	var mcpServers []sandboxTypes.MCPServer
	if ast.MCP != nil {
		for _, s := range ast.MCP.Servers {
			mcpServers = append(mcpServers, sandboxTypes.MCPServer{
				ServerID:  s.ServerID,
				Resources: s.Resources,
				Tools:     s.Tools,
			})
		}
	}

	// 7. Runner.Prepare (standard context).
	err = runner.Prepare(stdCtx, &sandboxTypes.PrepareRequest{
		Computer:     computer,
		Config:       cfg,
		Connector:    conn,
		AssistantID:  ast.ID,
		SkillsDir:    skillsDir,
		AssistantDir: assistantDir,
		MCPServers:   mcpServers,
		ConfigHash:   ast.ConfigHash,
		RunSteps:     sandboxv2.RunPrepareSteps,
	})
	if err != nil {
		runner.Cleanup(stdCtx, computer)
		sandboxv2.LifecycleAction(stdCtx, cfg, computer, manager)
		closeLoadingV2(ctx, loadingMsgID, "sandbox.failed")
		return nil, nil, nil, nil, "", fmt.Errorf("runner.Prepare: %w", err)
	}

	// Inject computer + workspace into context so Create/Next hooks
	// can access ctx.computer and ctx.workspace.
	ctx.SetComputer(computer)

	cleanup := func() {
		cleanCtx, cancel := stdContext.WithTimeout(stdContext.Background(), 5*time.Second)
		defer cancel()
		runner.Cleanup(cleanCtx, computer)
		sandboxv2.LifecycleAction(cleanCtx, cfg, computer, manager)
	}

	return runner, computer, cfg, cleanup, loadingMsgID, nil
}

// sandboxV2StreamParams groups arguments for executeSandboxV2Stream.
type sandboxV2StreamParams struct {
	Messages     []context.Message
	AgentNode    traceTypes.Node
	Handler      message.StreamFunc
	Runner       sandboxTypes.Runner
	Computer     infraV2.Computer
	Config       *sandboxTypes.SandboxConfig
	LoadingMsgID string
	Options      *context.Options
}

// executeSandboxV2Stream calls the V2 Runner.Stream and wraps it in the
// standard completion response.
func (ast *Assistant) executeSandboxV2Stream(
	ctx *context.Context, p *sandboxV2StreamParams,
) (*context.CompletionResponse, error) {
	_ = p.AgentNode

	cfg := p.Config
	manager := infraV2.M()

	// Build system prompt (parse $CTX variables the same way as buildSystemPrompts).
	var systemPrompt string
	if len(ast.Prompts) > 0 {
		ctxVars := ast.buildContextVariables(ctx)
		parsed := store.Prompts(ast.Prompts).Parse(ctxVars)
		for _, pr := range parsed {
			if pr.Role == "system" && pr.Content != "" {
				systemPrompt = pr.Content
				break
			}
		}
	}

	// Resolve connector for Stream (respects user-selected connector via opts).
	conn, _, _ := ast.GetConnector(ctx, p.Options)

	var tok *sandboxTypes.SandboxToken
	if ctx.Authorized != nil {
		var err error
		tok, err = sandboxv2.IssueSandboxToken(ctx.Authorized.TeamID, ctx.Authorized.UserID)
		if err != nil {
			return nil, fmt.Errorf("issue sandbox token: %w", err)
		}
	}

	streamReq := &sandboxTypes.StreamRequest{
		Computer:     p.Computer,
		Config:       cfg,
		Connector:    conn,
		AssistantID:  ast.ID,
		Messages:     p.Messages,
		SystemPrompt: systemPrompt,
		ChatID:       ctx.ChatID,
		Token:        tok,
		Logger:       ctx.Logger,
		UserExplicit: p.Options != nil && p.Options.Connector != "",
	}

	execReq := &sandboxv2.ExecuteRequest{
		Computer:     p.Computer,
		Runner:       p.Runner,
		Config:       cfg,
		StreamReq:    streamReq,
		Manager:      manager,
		LoadingMsgID: p.LoadingMsgID,
	}

	return sandboxv2.ExecuteSandboxStream(ctx, execReq, p.Handler)
}

// initStandaloneWorkspace loads the workspace FS into context when no sandbox
// is configured but the user selected a workspace (metadata["workspace_id"]).
func (ast *Assistant) initStandaloneWorkspace(ctx *context.Context) {
	if ctx.Metadata == nil {
		return
	}
	wsID, _ := ctx.Metadata["workspace_id"].(string)
	if wsID == "" {
		return
	}

	stdCtx := ctx.Context
	wsFS, err := workspace.M().FS(stdCtx, wsID)
	if err != nil {
		log.Printf("[assistant] initStandaloneWorkspace: failed to load workspace %s: %v", wsID, err)
		return
	}
	ctx.SetWorkspace(wsFS)
}

// buildBoxDisplayName constructs a human-readable display name for a Box
// using the locale-resolved Agent name and Workspace name (matching the UI list pages).
func buildBoxDisplayName(ctx *context.Context, assistantID, rawName string) string {
	agentName := i18n.Tr(assistantID, ctx.Locale, rawName)

	wsName := ""
	if ctx.Metadata != nil {
		if wsID, ok := ctx.Metadata["workspace_id"].(string); ok && wsID != "" {
			if wsm := workspace.M(); wsm != nil {
				if ws, err := wsm.Get(ctx.Context, wsID); err == nil && ws != nil {
					wsName = ws.Name
				}
			}
		}
	}

	if agentName != "" && wsName != "" {
		return agentName + " / " + wsName
	}
	if agentName != "" {
		return agentName
	}
	if wsName != "" {
		return wsName
	}
	return ""
}

func updateLoadingV2(ctx *context.Context, loadingMsgID, msgKey string) {
	if loadingMsgID == "" || ctx == nil || msgKey == "" {
		return
	}
	msg := &message.Message{
		MessageID:   loadingMsgID,
		Delta:       true,
		DeltaAction: message.DeltaReplace,
		Type:        message.TypeLoading,
		Props: map[string]any{
			"message": i18n.T(ctx.Locale, msgKey),
		},
	}
	ctx.Send(msg)
}

func closeLoadingV2(ctx *context.Context, loadingMsgID, msgKey string) {
	if loadingMsgID == "" || ctx == nil {
		return
	}
	props := map[string]any{"done": true}
	if msgKey != "" {
		props["message"] = i18n.T(ctx.Locale, msgKey)
	} else {
		props["message"] = ""
	}
	doneMsg := &message.Message{
		MessageID:   loadingMsgID,
		Delta:       true,
		DeltaAction: message.DeltaReplace,
		Type:        message.TypeLoading,
		Props:       props,
	}
	ctx.Send(doneMsg)
}
