package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

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

const claudePendingRecoveryFilename = ".daemon-claude-recovery.json"

type claudePendingRecovery struct {
	Strategy    string `json:"strategy"`
	RecoveryRef string `json:"recoveryRef"`
}

func claudePendingRecoveryPath(workDir string) string {
	return filepath.Join(strings.TrimSpace(workDir), claudePendingRecoveryFilename)
}

func writeClaudePendingRecovery(workDir string, strategy string, recoveryRef string) error {
	workDir = strings.TrimSpace(workDir)
	if workDir == "" {
		return fmt.Errorf("workdir is required")
	}
	if err := os.MkdirAll(workDir, 0o755); err != nil {
		return fmt.Errorf("mkdir workdir: %w", err)
	}
	payload, err := json.Marshal(claudePendingRecovery{
		Strategy:    strings.TrimSpace(strategy),
		RecoveryRef: strings.TrimSpace(recoveryRef),
	})
	if err != nil {
		return err
	}
	return os.WriteFile(claudePendingRecoveryPath(workDir), payload, 0o600)
}

func readClaudePendingRecovery(workDir string) (claudePendingRecovery, bool, error) {
	workDir = strings.TrimSpace(workDir)
	if workDir == "" {
		return claudePendingRecovery{}, false, nil
	}
	data, err := os.ReadFile(claudePendingRecoveryPath(workDir))
	if err != nil {
		if os.IsNotExist(err) {
			return claudePendingRecovery{}, false, nil
		}
		return claudePendingRecovery{}, false, err
	}
	var pending claudePendingRecovery
	if err := json.Unmarshal(data, &pending); err != nil {
		return claudePendingRecovery{}, false, fmt.Errorf("decode claude pending recovery: %w", err)
	}
	pending.Strategy = strings.TrimSpace(pending.Strategy)
	pending.RecoveryRef = strings.TrimSpace(pending.RecoveryRef)
	if pending.Strategy == "" || pending.RecoveryRef == "" {
		return claudePendingRecovery{}, false, nil
	}
	return pending, true, nil
}

func clearClaudePendingRecovery(workDir string) {
	if strings.TrimSpace(workDir) == "" {
		return
	}
	logIgnoredCleanupError("failed to clear claude pending recovery", os.Remove(claudePendingRecoveryPath(workDir)))
}

func claudeRecoveryRefForRestore(strategy string, recoveryRef string) (rewindcaps.ClaudeSessionResumeAtRecoveryRef, error) {
	if strings.TrimSpace(strategy) != rewindcaps.StrategyClaudeSessionResumeAt {
		return rewindcaps.ClaudeSessionResumeAtRecoveryRef{}, fmt.Errorf("unsupported recovery strategy %q", strategy)
	}
	ref, err := rewindcaps.DecodeClaudeSessionResumeAtRecoveryRef(recoveryRef)
	if err != nil {
		return rewindcaps.ClaudeSessionResumeAtRecoveryRef{}, fmt.Errorf("decode claude recovery ref: %w", err)
	}
	if ref.SessionID == "" {
		return rewindcaps.ClaudeSessionResumeAtRecoveryRef{}, fmt.Errorf("claude recovery ref is missing session id")
	}
	switch ref.BoundaryKind {
	case rewindcaps.RecoveryBoundaryResumeAt:
		if strings.TrimSpace(ref.BoundaryUUID) == "" {
			return rewindcaps.ClaudeSessionResumeAtRecoveryRef{}, fmt.Errorf("claude recovery ref is missing boundary uuid")
		}
	case rewindcaps.RecoveryBoundaryBeforeFirstInput:
		// No transcript boundary exists before the first AgentTeam input.
	default:
		return rewindcaps.ClaudeSessionResumeAtRecoveryRef{}, fmt.Errorf("unsupported claude recovery boundary %q", ref.BoundaryKind)
	}
	return ref, nil
}

func validateClaudeResumeAtBoundary(ref rewindcaps.ClaudeSessionResumeAtRecoveryRef) error {
	if ref.BoundaryKind != rewindcaps.RecoveryBoundaryResumeAt {
		return nil
	}
	if ok, err := CanResumeSession(string(runtimeconfig.TypeClaudeCode), ref.SessionID); err != nil {
		return err
	} else if !ok {
		return fmt.Errorf("claude transcript for session %q is unavailable", ref.SessionID)
	}
	if ok, err := claudeTranscriptContainsUUID(ref.SessionID, ref.BoundaryUUID); err != nil {
		return err
	} else if !ok {
		return fmt.Errorf("claude transcript for session %q does not contain boundary uuid %q", ref.SessionID, ref.BoundaryUUID)
	}
	return nil
}

// RestoreRecoveryPointDetached stages a Claude native resume-at restore for
// the next launch. Claude Code exposes rewind as a launch-time contract
// (--resume --resume-session-at) rather than a mutating daemon API, so detached
// restore records the exact native ref in the session workdir. Spawn consumes
// and clears the marker only after it has successfully launched Claude with the
// corresponding native arguments and written the next AgentTeam input.
func (d *ClaudeDriver) RestoreRecoveryPointDetached(ctx context.Context, cfg drivercontract.LaunchConfig, strategy string, recoveryRef string) error {
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}
	ref, err := claudeRecoveryRefForRestore(strategy, recoveryRef)
	if err != nil {
		return err
	}
	if strings.TrimSpace(cfg.SessionID) != "" && strings.TrimSpace(cfg.SessionID) != ref.SessionID {
		return fmt.Errorf("claude recovery ref session id %q does not match launch session id %q", ref.SessionID, strings.TrimSpace(cfg.SessionID))
	}
	if err := validateClaudeResumeAtBoundary(ref); err != nil {
		return err
	}
	return writeClaudePendingRecovery(cfg.WorkDir, strategy, recoveryRef)
}
