package launch

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"github.com/ollama/ollama/cmd/config"
	"github.com/ollama/ollama/cmd/internal/fileutil"
	"golang.org/x/term"
)

const (
	claudeDesktopIntegrationName = "claude-desktop"
	claudeDesktopProfileName     = "Ollama"
	claudeDesktopProfileID       = "00000000-0000-4000-8000-000000000114"
	claudeDesktopGatewayBaseURL  = "https://ollama.com"
	claudeDesktopAPIKeyURL       = "https://ollama.com/settings/keys"
	claudeDesktopModelLabel      = "Ollama Cloud"
	claudeDesktopUnsupported     = "Claude Desktop is no longer supported. Existing installations can be restored with 'ollama launch claude-desktop --restore'."
	claudeDesktopSuccessMessage  = "Claude Desktop profile changed to Ollama Cloud."
	claudeDesktopRestoreMessage  = "To restore the usual Claude profile, run: ollama launch claude-desktop --restore"
	claudeDesktopRestoredMessage = "Claude Desktop restored to the usual Claude profile."
)

var (
	claudeDesktopGOOS           = runtime.GOOS
	claudeDesktopUserHome       = os.UserHomeDir
	claudeDesktopStat           = os.Stat
	claudeDesktopOpenApp        = defaultClaudeDesktopOpenApp
	claudeDesktopOpenAppPath    = defaultClaudeDesktopOpenAppPath
	claudeDesktopQuitApp        = defaultClaudeDesktopQuitApp
	claudeDesktopIsRunning      = defaultClaudeDesktopIsRunning
	claudeDesktopRunningAppPath = defaultClaudeDesktopRunningAppPath
	claudeDesktopGlob           = filepath.Glob
	claudeDesktopSleep          = time.Sleep
	claudeDesktopHTTPClient     = http.DefaultClient
	claudeDesktopPromptAPIKey   = promptClaudeDesktopAPIKey
	claudeDesktopValidateAPIKey = validateClaudeDesktopAPIKey
)

// ClaudeDesktop configures and launches Claude Desktop in third-party
// inference mode using Ollama Cloud as the gateway.
type ClaudeDesktop struct{}

func (c *ClaudeDesktop) String() string { return "Claude Desktop" }

func (c *ClaudeDesktop) Supported() error { return claudeDesktopSupported() }

func (c *ClaudeDesktop) Paths() []string {
	return nil
}

func (c *ClaudeDesktop) AutodiscoveredModel() string {
	return claudeDesktopModelLabel
}

func (c *ClaudeDesktop) ConfigureAutodiscovery() error {
	if err := claudeDesktopSupported(); err != nil {
		return err
	}

	targets, err := claudeDesktopTargetPaths()
	if err != nil {
		return err
	}

	key, err := claudeDesktopValidatedAPIKey(context.Background(), claudeDesktopTargetProfilePaths(targets))
	if err != nil {
		return err
	}

	for _, path := range targets.normalConfigs {
		if err := writeClaudeDesktopDeploymentMode(path, "3p"); err != nil {
			return err
		}
	}
	for _, target := range targets.thirdPartyProfiles {
		if err := writeClaudeDesktopDeploymentMode(target.desktopConfig, "3p"); err != nil {
			return err
		}
		if err := writeClaudeDesktopMeta(target.meta, claudeDesktopProfileID, claudeDesktopProfileName); err != nil {
			return err
		}
		if err := writeClaudeDesktopGatewayProfile(target.profile, key, true); err != nil {
			return err
		}
	}
	return nil
}

func (c *ClaudeDesktop) RestoreHint() string {
	return claudeDesktopRestoreMessage
}

func (c *ClaudeDesktop) ConfigurationSuccessMessage() string {
	return claudeDesktopSuccessMessage + "\n" + claudeDesktopRestoreMessage
}

func (c *ClaudeDesktop) RestoreSuccessMessage() string {
	return claudeDesktopRestoredMessage
}

func (c *ClaudeDesktop) AutodiscoveryConfigured() bool {
	targets, err := claudeDesktopTargetPaths()
	if err != nil {
		return false
	}
	return claudeDesktopTargetsConfigured(targets)
}

func (c *ClaudeDesktop) Onboard() error {
	return config.MarkIntegrationOnboarded(claudeDesktopIntegrationName)
}

func (c *ClaudeDesktop) RequiresInteractiveOnboarding() bool {
	return false
}

func (c *ClaudeDesktop) SkipModelReadiness() bool {
	return true
}

func (c *ClaudeDesktop) Run(_ string, _ []LaunchModel, _ []string) error {
	return errClaudeDesktopUnsupported()
}

func (c *ClaudeDesktop) Restore() error {
	if err := claudeDesktopSupported(); err != nil {
		return err
	}
	targets, err := claudeDesktopTargetPaths()
	if err != nil {
		return err
	}

	for _, path := range targets.normalConfigs {
		if err := writeClaudeDesktopDeploymentMode(path, "1p"); err != nil {
			return err
		}
	}
	for _, target := range targets.thirdPartyProfiles {
		if err := writeClaudeDesktopDeploymentMode(target.desktopConfig, "1p"); err != nil {
			return err
		}
		if err := restoreClaudeDesktopMeta(target.meta); err != nil {
			return err
		}
		if err := restoreClaudeDesktopOllamaProfile(target.profile); err != nil {
			return err
		}
	}
	return claudeDesktopLaunchOrRestart("Restart Claude Desktop to use the usual Claude profile?")
}

func errClaudeDesktopUnsupported() error {
	return errors.New(claudeDesktopUnsupported)
}

func claudeDesktopSupported() error {
	switch claudeDesktopGOOS {
	case "darwin", "windows":
		return nil
	default:
		return fmt.Errorf("Claude Desktop launch is only supported on macOS and Windows")
	}
}

func claudeDesktopInstalled() bool {
	if claudeDesktopAppPath() != "" {
		return true
	}
	if claudeDesktopGOOS == "windows" && claudeDesktopIsRunning() {
		return true
	}
	for _, dir := range claudeDesktopProfileDirCandidates(false) {
		if _, err := claudeDesktopStat(dir); err == nil {
			return true
		}
	}
	return false
}

func claudeDesktopAppPath() string {
	if claudeDesktopGOOS != "darwin" && claudeDesktopGOOS != "windows" {
		return ""
	}
	for _, path := range claudeDesktopAppCandidates() {
		if _, err := claudeDesktopStat(path); err == nil {
			return path
		}
	}
	return ""
}

func claudeDesktopAppCandidates() []string {
	switch claudeDesktopGOOS {
	case "darwin":
		return claudeDesktopDarwinAppCandidates()
	case "windows":
		return claudeDesktopWindowsAppCandidates()
	default:
		return nil
	}
}

func claudeDesktopDarwinAppCandidates() []string {
	candidates := []string{"/Applications/Claude.app"}
	if home, err := claudeDesktopUserHome(); err == nil {
		candidates = append(candidates, filepath.Join(home, "Applications", "Claude.app"))
	}
	return candidates
}

func claudeDesktopWindowsAppCandidates() []string {
	local, err := claudeDesktopLocalAppData()
	if err != nil {
		return nil
	}

	candidates := []string{
		filepath.Join(local, "Programs", "Claude", "Claude.exe"),
		filepath.Join(local, "Programs", "Claude Desktop", "Claude.exe"),
		filepath.Join(local, "Claude", "Claude.exe"),
		filepath.Join(local, "Claude Nest", "Claude.exe"),
		filepath.Join(local, "Claude Desktop", "Claude.exe"),
		filepath.Join(local, "AnthropicClaude", "Claude.exe"),
	}
	for _, pattern := range []string{
		filepath.Join(local, "AnthropicClaude", "app-*", "Claude.exe"),
		filepath.Join(local, "Programs", "Claude", "app-*", "Claude.exe"),
		filepath.Join(local, "Programs", "Claude Desktop", "app-*", "Claude.exe"),
	} {
		matches, _ := claudeDesktopGlob(pattern)
		candidates = append(candidates, matches...)
	}
	return claudeDesktopDedupePaths(candidates)
}

func claudeDesktopDedupePaths(paths []string) []string {
	out := make([]string, 0, len(paths))
	seen := make(map[string]bool, len(paths))
	for _, path := range paths {
		if strings.TrimSpace(path) == "" {
			continue
		}
		key := strings.ToLower(path)
		if seen[key] {
			continue
		}
		seen[key] = true
		out = append(out, path)
	}
	return out
}

type claudeDesktopPaths struct {
	normalConfig  string
	desktopConfig string
	meta          string
	profile       string
}

type claudeDesktopThirdPartyPaths struct {
	desktopConfig string
	meta          string
	profile       string
}

type claudeDesktopTargets struct {
	normalConfigs      []string
	thirdPartyProfiles []claudeDesktopThirdPartyPaths
}

func claudeDesktopConfigPaths() (claudeDesktopPaths, error) {
	switch claudeDesktopGOOS {
	case "darwin":
		return claudeDesktopDarwinConfigPaths()
	case "windows":
		return claudeDesktopWindowsConfigPaths()
	default:
		return claudeDesktopPaths{}, claudeDesktopSupported()
	}
}

func claudeDesktopDarwinConfigPaths() (claudeDesktopPaths, error) {
	normalRoots, thirdPartyRoots, err := claudeDesktopDarwinProfileRoots()
	if err != nil {
		return claudeDesktopPaths{}, err
	}
	normalBase := normalRoots[0]
	thirdPartyBase := thirdPartyRoots[0]
	return claudeDesktopPaths{
		normalConfig:  filepath.Join(normalBase, "claude_desktop_config.json"),
		desktopConfig: filepath.Join(thirdPartyBase, "claude_desktop_config.json"),
		meta:          filepath.Join(thirdPartyBase, "configLibrary", "_meta.json"),
		profile:       filepath.Join(thirdPartyBase, "configLibrary", claudeDesktopProfileID+".json"),
	}, nil
}

func claudeDesktopWindowsConfigPaths() (claudeDesktopPaths, error) {
	normalBase, err := claudeDesktopProfileDir(true)
	if err != nil {
		return claudeDesktopPaths{}, err
	}
	thirdPartyBase, err := claudeDesktopProfileDir(false)
	if err != nil {
		return claudeDesktopPaths{}, err
	}
	return claudeDesktopPaths{
		normalConfig:  filepath.Join(normalBase, "claude_desktop_config.json"),
		desktopConfig: filepath.Join(thirdPartyBase, "claude_desktop_config.json"),
		meta:          filepath.Join(thirdPartyBase, "configLibrary", "_meta.json"),
		profile:       filepath.Join(thirdPartyBase, "configLibrary", claudeDesktopProfileID+".json"),
	}, nil
}

func claudeDesktopProfileDir(normal bool) (string, error) {
	candidates := claudeDesktopProfileDirCandidates(normal)
	if len(candidates) == 0 {
		return "", fmt.Errorf("Claude Desktop profile directory could not be resolved")
	}
	for _, candidate := range candidates {
		if _, err := claudeDesktopStat(candidate); err == nil {
			return candidate, nil
		}
	}
	return candidates[0], nil
}

func claudeDesktopProfileDirCandidates(normal bool) []string {
	if claudeDesktopGOOS != "windows" {
		return nil
	}
	normalRoots, thirdPartyRoots, err := claudeDesktopWindowsProfileRoots()
	if err != nil {
		return nil
	}
	if normal {
		return normalRoots
	}
	return thirdPartyRoots
}

func claudeDesktopDarwinProfileRoots() ([]string, []string, error) {
	home, err := claudeDesktopUserHome()
	if err != nil {
		return nil, nil, err
	}
	base := filepath.Join(home, "Library", "Application Support")
	return []string{filepath.Join(base, "Claude")}, []string{filepath.Join(base, "Claude-3p")}, nil
}

func claudeDesktopWindowsProfileRoots() ([]string, []string, error) {
	local, err := claudeDesktopLocalAppData()
	if err != nil {
		return nil, nil, err
	}
	normalRoots := []string{
		filepath.Join(local, "Claude"),
		filepath.Join(local, "Claude Nest"),
	}
	thirdPartyRoots := []string{
		filepath.Join(local, "Claude-3p"),
		filepath.Join(local, "Claude Nest-3p"),
	}
	return normalRoots, thirdPartyRoots, nil
}

func claudeDesktopTargetPaths() (claudeDesktopTargets, error) {
	var (
		normalRoots     []string
		thirdPartyRoots []string
		err             error
	)

	switch claudeDesktopGOOS {
	case "darwin":
		normalRoots, thirdPartyRoots, err = claudeDesktopDarwinProfileRoots()
	case "windows":
		normalRoots, thirdPartyRoots, err = claudeDesktopWindowsProfileRoots()
	default:
		err = claudeDesktopSupported()
	}
	if err != nil {
		return claudeDesktopTargets{}, err
	}

	return newClaudeDesktopTargets(normalRoots, thirdPartyRoots), nil
}

func newClaudeDesktopTargets(normalRoots, thirdPartyRoots []string) claudeDesktopTargets {
	targets := claudeDesktopTargets{}
	for _, root := range claudeDesktopDedupePaths(normalRoots) {
		targets.normalConfigs = append(targets.normalConfigs, filepath.Join(root, "claude_desktop_config.json"))
	}
	for _, root := range claudeDesktopDedupePaths(thirdPartyRoots) {
		targets.thirdPartyProfiles = append(targets.thirdPartyProfiles, claudeDesktopThirdPartyPaths{
			desktopConfig: filepath.Join(root, "claude_desktop_config.json"),
			meta:          filepath.Join(root, "configLibrary", "_meta.json"),
			profile:       filepath.Join(root, "configLibrary", claudeDesktopProfileID+".json"),
		})
	}
	return targets
}

func claudeDesktopTargetProfilePaths(targets claudeDesktopTargets) []string {
	paths := make([]string, 0, len(targets.thirdPartyProfiles))
	for _, target := range targets.thirdPartyProfiles {
		paths = append(paths, target.profile)
	}
	return paths
}

func claudeDesktopLocalAppData() (string, error) {
	if local := strings.TrimSpace(os.Getenv("LOCALAPPDATA")); local != "" {
		return local, nil
	}
	if home := strings.TrimSpace(os.Getenv("USERPROFILE")); home != "" {
		return filepath.Join(home, "AppData", "Local"), nil
	}
	home, err := claudeDesktopUserHome()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, "AppData", "Local"), nil
}

type claudeDesktopAPIKeySource int

const (
	claudeDesktopAPIKeySourceNone claudeDesktopAPIKeySource = iota
	claudeDesktopAPIKeySourceEnv
	claudeDesktopAPIKeySourceProfile
)

func claudeDesktopValidatedAPIKey(ctx context.Context, profilePaths []string) (string, error) {
	key, source, err := claudeDesktopAPIKey(profilePaths)
	if err != nil {
		return "", err
	}
	if err := claudeDesktopValidateAPIKey(ctx, key); err == nil {
		return key, nil
	} else if source != claudeDesktopAPIKeySourceProfile || !canPromptClaudeDesktopAPIKey() {
		return "", err
	}
	return promptValidClaudeDesktopAPIKey(ctx)
}

func claudeDesktopAPIKey(profilePaths []string) (string, claudeDesktopAPIKeySource, error) {
	if key := strings.TrimSpace(os.Getenv("OLLAMA_API_KEY")); key != "" {
		return key, claudeDesktopAPIKeySourceEnv, nil
	}
	for _, profilePath := range profilePaths {
		if key := readClaudeDesktopGatewayAPIKey(profilePath); key != "" {
			return key, claudeDesktopAPIKeySourceProfile, nil
		}
	}
	key, err := promptClaudeDesktopAPIKeyValue()
	return key, claudeDesktopAPIKeySourceNone, err
}

func canPromptClaudeDesktopAPIKey() bool {
	return isInteractiveSession() && !currentLaunchConfirmPolicy.requireYesMessage
}

func promptValidClaudeDesktopAPIKey(ctx context.Context) (string, error) {
	key, err := promptClaudeDesktopAPIKeyValue()
	if err != nil {
		return "", err
	}
	if err := claudeDesktopValidateAPIKey(ctx, key); err != nil {
		return "", err
	}
	return key, nil
}

func promptClaudeDesktopAPIKeyValue() (string, error) {
	if !canPromptClaudeDesktopAPIKey() {
		return "", missingClaudeDesktopAPIKeyError()
	}
	key, err := claudeDesktopPromptAPIKey()
	if err != nil {
		return "", err
	}
	key = strings.TrimSpace(key)
	if key == "" {
		return "", missingClaudeDesktopAPIKeyError()
	}
	return key, nil
}

func missingClaudeDesktopAPIKeyError() error {
	return fmt.Errorf("OLLAMA_API_KEY is required for Claude Desktop. Create an API key at %s, then re-run with OLLAMA_API_KEY set", claudeDesktopAPIKeyURL)
}

func promptClaudeDesktopAPIKey() (string, error) {
	fmt.Fprint(os.Stderr, claudeDesktopAPIKeyPrompt())
	key, err := term.ReadPassword(int(os.Stdin.Fd()))
	fmt.Fprintln(os.Stderr)
	if err != nil {
		return "", err
	}
	return string(key), nil
}

func claudeDesktopAPIKeyPrompt() string {
	return fmt.Sprintf("Create an Ollama API key at %s\nEnter Ollama API key (input hidden): ", claudeDesktopAPIKeyURL)
}

func readClaudeDesktopGatewayAPIKey(path string) string {
	cfg, err := readClaudeDesktopJSON(path)
	if err != nil {
		return ""
	}
	key, _ := cfg["inferenceGatewayApiKey"].(string)
	return strings.TrimSpace(key)
}

func validateClaudeDesktopAPIKey(ctx context.Context, key string) error {
	ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
	defer cancel()

	if claudeDesktopAPIKeyHasInvalidHeaderChars(key) {
		return claudeDesktopAPIKeyVerificationError()
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, claudeDesktopGatewayBaseURL+"/v1/models", nil)
	if err != nil {
		return claudeDesktopAPIKeyVerificationError()
	}
	req.Header.Set("Authorization", "Bearer "+key)
	req.Header.Set("Accept", "application/json")

	resp, err := claudeDesktopHTTPClient.Do(req)
	if err != nil {
		return claudeDesktopAPIKeyVerificationError()
	}
	defer resp.Body.Close()
	_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4<<10))

	switch {
	case resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden:
		return fmt.Errorf("Ollama API key was rejected; create a valid key at %s", claudeDesktopAPIKeyURL)
	case resp.StatusCode >= 200 && resp.StatusCode < 300:
		return nil
	default:
		return fmt.Errorf("could not verify Ollama API key; ollama.com returned status %d, try again later", resp.StatusCode)
	}
}

func claudeDesktopAPIKeyHasInvalidHeaderChars(key string) bool {
	return strings.ContainsFunc(key, func(r rune) bool {
		return r < ' ' || r == 0x7f
	})
}

func claudeDesktopAPIKeyVerificationError() error {
	return fmt.Errorf("could not verify Ollama API key; copy a key from %s and try again", claudeDesktopAPIKeyURL)
}

func writeClaudeDesktopDeploymentMode(path, mode string) error {
	cfg, err := readClaudeDesktopJSONAllowMissing(path)
	if err != nil {
		return fmt.Errorf("parse Claude Desktop config: %w", err)
	}
	cfg["deploymentMode"] = mode
	return writeClaudeDesktopJSON(path, cfg)
}

func writeClaudeDesktopMeta(path, id, name string) error {
	meta, err := readClaudeDesktopJSONAllowMissing(path)
	if err != nil {
		return fmt.Errorf("parse Claude Desktop config metadata: %w", err)
	}

	meta["appliedId"] = id
	entries := make([]any, 0)
	for _, entry := range claudeDesktopAnySlice(meta["entries"]) {
		entryMap, _ := entry.(map[string]any)
		if entryMap == nil {
			entries = append(entries, entry)
			continue
		}
		if entryID, _ := entryMap["id"].(string); entryID == id {
			continue
		}
		entries = append(entries, entryMap)
	}
	entries = append(entries, map[string]any{
		"id":   id,
		"name": name,
	})
	meta["entries"] = entries
	return writeClaudeDesktopJSON(path, meta)
}

func writeClaudeDesktopGatewayProfile(path string, apiKey string, forceChooser bool) error {
	cfg, err := readClaudeDesktopJSONAllowMissing(path)
	if err != nil {
		return fmt.Errorf("parse Claude Desktop Ollama profile: %w", err)
	}
	cfg["inferenceProvider"] = "gateway"
	cfg["inferenceGatewayBaseUrl"] = claudeDesktopGatewayBaseURL
	cfg["inferenceGatewayApiKey"] = apiKey
	cfg["inferenceGatewayAuthScheme"] = "bearer"
	delete(cfg, "inferenceModels")
	cfg["disableDeploymentModeChooser"] = forceChooser
	return writeClaudeDesktopJSON(path, cfg)
}

func restoreClaudeDesktopMeta(path string) error {
	meta, err := readClaudeDesktopJSONAllowMissing(path)
	if err != nil {
		return fmt.Errorf("parse Claude Desktop config metadata: %w", err)
	}
	if len(meta) == 0 {
		return nil
	}

	changed := false
	if appliedID, _ := meta["appliedId"].(string); appliedID == claudeDesktopProfileID {
		delete(meta, "appliedId")
		changed = true
	}

	entries := claudeDesktopAnySlice(meta["entries"])
	if entries != nil {
		filtered := make([]any, 0, len(entries))
		for _, entry := range entries {
			entryMap, _ := entry.(map[string]any)
			if entryID, _ := entryMap["id"].(string); entryID == claudeDesktopProfileID {
				changed = true
				continue
			}
			filtered = append(filtered, entry)
		}
		meta["entries"] = filtered
	}

	if !changed {
		return nil
	}
	return writeClaudeDesktopJSON(path, meta)
}

func restoreClaudeDesktopOllamaProfile(path string) error {
	cfg, err := readClaudeDesktopJSONAllowMissing(path)
	if err != nil {
		return fmt.Errorf("parse Claude Desktop Ollama profile: %w", err)
	}
	if len(cfg) == 0 {
		return nil
	}
	cfg["disableDeploymentModeChooser"] = false
	delete(cfg, "inferenceProvider")
	delete(cfg, "inferenceGatewayBaseUrl")
	delete(cfg, "inferenceGatewayAuthScheme")
	delete(cfg, "inferenceModels")
	return writeClaudeDesktopJSON(path, cfg)
}

func readClaudeDesktopAppliedID(path string) string {
	meta, err := readClaudeDesktopJSON(path)
	if err != nil {
		return ""
	}
	applied, _ := meta["appliedId"].(string)
	return applied
}

func readClaudeDesktopDeploymentMode(path string) string {
	cfg, err := readClaudeDesktopJSON(path)
	if err != nil {
		return ""
	}
	mode, _ := cfg["deploymentMode"].(string)
	return mode
}

func claudeDesktopTargetsConfigured(targets claudeDesktopTargets) bool {
	if len(targets.normalConfigs) == 0 || len(targets.thirdPartyProfiles) == 0 {
		return false
	}
	for _, path := range targets.normalConfigs {
		if readClaudeDesktopDeploymentMode(path) != "3p" {
			return false
		}
	}
	for _, target := range targets.thirdPartyProfiles {
		if readClaudeDesktopDeploymentMode(target.desktopConfig) != "3p" {
			return false
		}
		if !claudeDesktopThirdPartyProfileConfigured(target) {
			return false
		}
	}
	return true
}

func claudeDesktopThirdPartyProfileConfigured(target claudeDesktopThirdPartyPaths) bool {
	if readClaudeDesktopAppliedID(target.meta) != claudeDesktopProfileID {
		return false
	}

	cfg, err := readClaudeDesktopJSON(target.profile)
	if err != nil {
		return false
	}
	if s, _ := cfg["inferenceProvider"].(string); s != "gateway" {
		return false
	}
	if s, _ := cfg["inferenceGatewayBaseUrl"].(string); strings.TrimRight(s, "/") != claudeDesktopGatewayBaseURL {
		return false
	}
	if s, _ := cfg["inferenceGatewayApiKey"].(string); strings.TrimSpace(s) == "" {
		return false
	}
	return true
}

func readClaudeDesktopJSONAllowMissing(path string) (map[string]any, error) {
	cfg, err := readClaudeDesktopJSON(path)
	if errors.Is(err, os.ErrNotExist) {
		return map[string]any{}, nil
	}
	return cfg, err
}

func readClaudeDesktopJSON(path string) (map[string]any, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	var cfg map[string]any
	if err := json.Unmarshal(data, &cfg); err != nil {
		return nil, err
	}
	if cfg == nil {
		cfg = map[string]any{}
	}
	return cfg, nil
}

func writeClaudeDesktopJSON(path string, cfg any) error {
	data, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return err
	}
	data = append(data, '\n')
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	return fileutil.WriteWithBackup(path, data)
}

func claudeDesktopAnySlice(value any) []any {
	switch v := value.(type) {
	case []any:
		return v
	case nil:
		return nil
	default:
		return nil
	}
}

func claudeDesktopLaunchOrRestart(prompt string) error {
	if !claudeDesktopIsRunning() {
		return claudeDesktopOpenApp()
	}
	restartAppPath := ""
	if claudeDesktopGOOS == "windows" {
		restartAppPath = claudeDesktopRunningAppPath()
	}

	restart, err := ConfirmPrompt(prompt)
	if err != nil {
		return err
	}
	if !restart {
		fmt.Fprintln(os.Stderr, "\nQuit and reopen Claude Desktop when you're ready for the profile change to take effect.")
		return nil
	}

	if err := claudeDesktopQuitApp(); err != nil {
		return fmt.Errorf("quit Claude Desktop: %w", err)
	}
	if err := waitForClaudeDesktopExit(30 * time.Second); err != nil {
		return err
	}
	if restartAppPath != "" {
		return claudeDesktopOpenAppPath(restartAppPath)
	}
	return claudeDesktopOpenApp()
}

func waitForClaudeDesktopExit(timeout time.Duration) error {
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		if !claudeDesktopIsRunning() {
			return nil
		}
		claudeDesktopSleep(200 * time.Millisecond)
	}
	return fmt.Errorf("Claude Desktop did not quit; quit it manually and re-run the command")
}

func defaultClaudeDesktopIsRunning() bool {
	switch claudeDesktopGOOS {
	case "darwin":
		out, err := exec.Command("pgrep", "-f", "Claude.app/Contents/MacOS/Claude").Output()
		return err == nil && strings.TrimSpace(string(out)) != ""
	case "windows":
		out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", `(Get-Process claude -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1).Id`).Output()
		return err == nil && strings.TrimSpace(string(out)) != ""
	default:
		return false
	}
}

func defaultClaudeDesktopOpenApp() error {
	switch claudeDesktopGOOS {
	case "windows":
		if path := claudeDesktopAppPath(); path != "" {
			return claudeDesktopOpenAppPath(path)
		}
		if path := claudeDesktopRunningAppPath(); path != "" {
			return claudeDesktopOpenAppPath(path)
		}
		return fmt.Errorf("Claude Desktop executable was not found; open Claude Desktop manually once and re-run 'ollama launch claude-desktop --restore'")
	case "darwin":
		return openClaudeDesktopDarwin()
	default:
		return claudeDesktopSupported()
	}
}

func defaultClaudeDesktopOpenAppPath(path string) error {
	switch claudeDesktopGOOS {
	case "windows":
		return exec.Command("powershell.exe", "-NoProfile", "-Command", "Start-Process -FilePath "+quotePowerShellString(path)).Run()
	case "darwin":
		return openClaudeDesktopDarwin()
	default:
		return claudeDesktopSupported()
	}
}

func openClaudeDesktopDarwin() error {
	cmd := exec.Command("open", "-a", "Claude")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func defaultClaudeDesktopRunningAppPath() string {
	if claudeDesktopGOOS != "windows" {
		return ""
	}
	script := `(Get-Process claude -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 -and $_.Path } | Select-Object -First 1 -ExpandProperty Path)`
	out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", script).Output()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}

func defaultClaudeDesktopQuitApp() error {
	if claudeDesktopGOOS == "windows" {
		script := `Get-Process claude -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | ForEach-Object { [void]$_.CloseMainWindow() }`
		return exec.Command("powershell.exe", "-NoProfile", "-Command", script).Run()
	}
	return exec.Command("osascript", "-e", `tell application "Claude" to quit`).Run()
}

func quotePowerShellString(s string) string {
	return "'" + strings.ReplaceAll(s, "'", "''") + "'"
}
