package agent

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/internal/core/rewindcaps"
)

// DetachedRecoveryConfig holds the parameters for restoring a recovery point
// without an already-running Process.
type DetachedRecoveryConfig struct {
	AgentID         string
	Runtime         string
	Binary          string
	AuthToken       string
	ServerURL       string
	SessionID       string
	WorkDir         string
	RuntimeStateDir string
	PermissionMode  string
	Model           string
	Provider        *drivercontract.ProviderConfig
	// RuntimeDebugEnabled keeps detached recovery lifecycle evidence in the
	// same local diagnostic stream as the primary runtime launch.
	RuntimeDebugEnabled bool
	Env                 map[string]string
}

// RecoveryArtifact identifies a single recovery artifact to garbage-collect.
type RecoveryArtifact struct {
	Strategy    string
	RecoveryRef string
}

// ErrAgentBusy is returned when a recovery restore is attempted while the
// agent process is not idle.
var ErrAgentBusy = fmt.Errorf("agent is busy")

type recoveryTargetAheadError struct {
	CurrentTurnCount int
	TargetTurnCount  int
}

func (err *recoveryTargetAheadError) Error() string {
	return fmt.Sprintf("codex current turn count %d is behind recovery target %d", err.CurrentTurnCount, err.TargetTurnCount)
}

func isRecoveryTargetAheadError(err error) bool {
	var targetErr *recoveryTargetAheadError
	return errors.As(err, &targetErr)
}

// ValidateRecoveryCapability checks that the given runtime supports the
// requested recovery strategy.
func ValidateRecoveryCapability(runtime string, strategy string) (rewindcaps.Capability, error) {
	trimmedRuntime := strings.TrimSpace(runtime)
	if trimmedRuntime == "" {
		return rewindcaps.Capability{}, fmt.Errorf("runtime is required")
	}
	trimmedStrategy := strings.TrimSpace(strategy)
	if trimmedStrategy == "" {
		return rewindcaps.Capability{}, fmt.Errorf("recovery strategy is required")
	}

	capability := rewindcaps.ForRuntime(trimmedRuntime)
	if strings.TrimSpace(capability.Strategy) == "" || (!capability.SupportsDetachedRestore && !capability.SupportsLiveRestore) {
		return capability, fmt.Errorf("runtime %q does not support recovery restore", trimmedRuntime)
	}
	if trimmedStrategy != capability.Strategy {
		return capability, fmt.Errorf("runtime %q requires strategy %q", trimmedRuntime, capability.Strategy)
	}
	return capability, nil
}

// RestoreRecoveryPointDetached restores one recovery point without requiring an
// already-running Process.
func RestoreRecoveryPointDetached(ctx context.Context, cfg DetachedRecoveryConfig, strategy string, recoveryRef string) error {
	capability, err := ValidateRecoveryCapability(cfg.Runtime, strategy)
	if err != nil {
		return err
	}
	if !capability.SupportsDetachedRestore {
		return fmt.Errorf("runtime %q does not support detached recovery", capability.Runtime)
	}
	runtimeType := runtimeTypeFromName(capability.Runtime)

	workDir := strings.TrimSpace(cfg.WorkDir)
	if workDir == "" {
		return fmt.Errorf("workdir is required")
	}
	if err := os.MkdirAll(workDir, 0o755); err != nil {
		return fmt.Errorf("mkdir workdir: %w", err)
	}

	driver, err := NewDriver(runtimeType)
	if err != nil {
		return err
	}
	detached, ok := driver.(drivercontract.DetachedRecoveryDriver)
	if !ok {
		return fmt.Errorf("runtime does not support detached recovery")
	}

	cfg.WorkDir = workDir
	return detached.RestoreRecoveryPointDetached(ctx, detachedRecoveryLaunchConfig(cfg, runtimeType, true), capability.Strategy, strings.TrimSpace(recoveryRef))
}

func detachedRecoveryLaunchConfig(cfg DetachedRecoveryConfig, runtimeType runtimeconfig.RuntimeType, includeAgentTeamMCP bool) drivercontract.LaunchConfig {
	launch := drivercontract.LaunchConfig{
		AgentID:             strings.TrimSpace(cfg.AgentID),
		RuntimeType:         runtimeType,
		SessionID:           strings.TrimSpace(cfg.SessionID),
		WorkDir:             strings.TrimSpace(cfg.WorkDir),
		RuntimeStateDir:     strings.TrimSpace(cfg.RuntimeStateDir),
		Binary:              strings.TrimSpace(cfg.Binary),
		PermissionMode:      strings.TrimSpace(cfg.PermissionMode),
		Model:               strings.TrimSpace(cfg.Model),
		Provider:            cloneProviderConfig(cfg.Provider),
		RuntimeDebugEnabled: cfg.RuntimeDebugEnabled,
		Env:                 runtimeconfig.CloneStringMap(cfg.Env),
	}
	if includeAgentTeamMCP {
		launch.MCPs = []types.NamedMCPConfig{{Name: "agentteam", URL: cfg.ServerURL, Headers: map[string]string{}, Required: true}}
	}
	return launch
}

func cloneProviderConfig(prov *drivercontract.ProviderConfig) *drivercontract.ProviderConfig {
	if prov == nil {
		return nil
	}
	cloned := *prov
	if prov.Headers != nil {
		cloned.Headers = make(map[string]string, len(prov.Headers))
		for key, value := range prov.Headers {
			cloned.Headers[key] = value
		}
	}
	if prov.WireAPIs != nil {
		cloned.WireAPIs = append([]string(nil), prov.WireAPIs...)
	}
	return &cloned
}

// CleanupRecoveryArtifactsDetached garbage-collects recovery artifacts without
// requiring a running Process.
func CleanupRecoveryArtifactsDetached(ctx context.Context, cfg DetachedRecoveryConfig, requests []RecoveryArtifact) error {
	runtimeType := runtimeTypeFromName(cfg.Runtime)
	if runtimeType == runtimeconfig.TypeOpenCode || runtimeType == runtimeconfig.TypeBuiltin {
		return cleanupOpenCodeRecoveryArtifactsDetached(ctx, cfg, runtimeType, requests)
	}

	for _, req := range requests {
		strategy := strings.TrimSpace(req.Strategy)
		recoveryRef := strings.TrimSpace(req.RecoveryRef)
		if strategy == "" || recoveryRef == "" {
			continue
		}

		capability, err := ValidateRecoveryCapability(cfg.Runtime, strategy)
		if err != nil {
			slog.Debug("skipping recovery artifact cleanup for unsupported capability",
				"runtime", strings.TrimSpace(cfg.Runtime),
				"strategy", strategy,
				"error", err,
			)
			continue
		}
		if capability.Strategy == rewindcaps.StrategyCodexAppServerTurns {
			// A Codex recovery ref is only a restore coordinate (thread ID and turn
			// count); it does not own a disposable runtime artifact. Restoring it here
			// would roll back the shared Codex transcript and can interrupt a live turn.
			continue
		}
		runtimeType := runtimeTypeFromName(capability.Runtime)
		driver, err := NewDriver(runtimeType)
		if err != nil {
			slog.Warn("recovery artifact cleanup failed to create driver",
				"runtime", capability.Runtime,
				"strategy", strategy,
				"error", err,
			)
			continue
		}
		detached, ok := driver.(drivercontract.DetachedRecoveryDriver)
		if !ok {
			slog.Debug("skipping recovery artifact cleanup for runtime without detached recovery",
				"runtime", capability.Runtime,
				"strategy", strategy,
			)
			continue
		}

		if err := detached.RestoreRecoveryPointDetached(ctx, detachedRecoveryLaunchConfig(cfg, runtimeType, false), strategy, recoveryRef); err != nil {
			if isRecoveryTargetAheadError(err) {
				slog.Debug("recovery artifact cleanup skipped for unreachable target",
					"agent_id", strings.TrimSpace(cfg.AgentID),
					"runtime", capability.Runtime,
					"strategy", strategy,
					"error", err,
				)
				continue
			}
			slog.Warn("recovery artifact cleanup failed",
				"agent_id", strings.TrimSpace(cfg.AgentID),
				"runtime", capability.Runtime,
				"strategy", strategy,
				"error", err,
			)
		}
	}
	return nil
}

func cleanupOpenCodeRecoveryArtifactsDetached(ctx context.Context, cfg DetachedRecoveryConfig, runtimeType runtimeconfig.RuntimeType, requests []RecoveryArtifact) error {
	recoveryRefs := make([]string, 0, len(requests))
	strategy := ""
	for _, req := range requests {
		requestStrategy := strings.TrimSpace(req.Strategy)
		recoveryRef := strings.TrimSpace(req.RecoveryRef)
		if requestStrategy == "" || recoveryRef == "" {
			continue
		}
		capability, err := ValidateRecoveryCapability(cfg.Runtime, requestStrategy)
		if err != nil {
			slog.Debug("skipping recovery artifact cleanup for unsupported capability",
				"runtime", strings.TrimSpace(cfg.Runtime),
				"strategy", requestStrategy,
				"error", err,
			)
			continue
		}
		if strategy == "" {
			strategy = capability.Strategy
		}
		recoveryRefs = append(recoveryRefs, recoveryRef)
	}
	if len(recoveryRefs) == 0 {
		return nil
	}

	driver, err := NewDriver(runtimeType)
	if err != nil {
		slog.Warn("recovery artifact cleanup failed to create driver",
			"runtime", strings.TrimSpace(cfg.Runtime),
			"strategy", strategy,
			"error", err,
		)
		return nil
	}
	openCode, ok := driver.(*OpenCodeDriver)
	if !ok {
		slog.Warn("recovery artifact cleanup created unexpected OpenCode driver",
			"runtime", strings.TrimSpace(cfg.Runtime),
			"driver", fmt.Sprintf("%T", driver),
		)
		return nil
	}
	if err := openCode.restoreRecoveryPointsDetached(
		ctx,
		detachedRecoveryLaunchConfig(cfg, runtimeType, false),
		strategy,
		recoveryRefs,
	); err != nil {
		slog.Warn("recovery artifact cleanup batch failed",
			"agent_id", strings.TrimSpace(cfg.AgentID),
			"runtime", strings.TrimSpace(cfg.Runtime),
			"strategy", strategy,
			"artifact_count", len(recoveryRefs),
			"error", err,
		)
	}
	return nil
}

func runtimeTypeFromName(name string) runtimeconfig.RuntimeType {
	switch strings.TrimSpace(name) {
	case "claude", "claude-code":
		return runtimeconfig.TypeClaudeCode
	case "codex":
		return runtimeconfig.TypeCodex
	case "kimi":
		return runtimeconfig.TypeKimi
	case "opencode":
		return runtimeconfig.TypeOpenCode
	case "builtin":
		return runtimeconfig.TypeBuiltin
	default:
		return runtimeconfig.RuntimeType(name)
	}
}
