//go:build !windows

package infra

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

// ResumableClaude describes a CLOSED Claude Code session that still has a saved
// goal and can therefore be reopened with `claude --resume <SessionID>`. The
// fleetview TUI consumes these for its "resume" picker.
type ResumableClaude struct {
	SessionID  string `json:"session_id"`
	Goal       string `json:"goal"`        // from goals/<id>.json .goal ("" if absent)
	Emojis     string `json:"emojis"`      // from goals/<id>.json .emojis ("" if absent)
	Name       string `json:"name"`        // from goals/<id>.json .rename ("" if absent)
	LastActive int64  `json:"last_active"` // mtime of the goal.json file, epoch seconds
}

// maxResumable caps the number of resumable sessions returned, keeping only the
// most recently touched ones.
const maxResumable = 40

// ListResumableClaudes scans the current user's ~/.claude directory and returns
// the closed sessions that can be reopened with `claude --resume`. It is a thin
// wrapper over ListResumableClaudesFrom resolving the home directory.
func ListResumableClaudes() ([]ResumableClaude, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return nil, fmt.Errorf("resolve home dir: %w", err)
	}
	return ListResumableClaudesFrom(filepath.Join(home, ".claude"))
}

// ListResumableClaudesFrom scans claudeDir (e.g. ~/.claude) and returns the
// sessions that have a goal (goals/<id>.json) whose process is NOT alive — i.e.
// candidates to reopen with `claude --resume <SessionID>`.
//
// A session is considered live (and thus excluded) when sessions/<PID>.json
// reports a PID whose /proc starttime matches the recorded procStart, using the
// exact same liveness criterion as ListClaudeFleetFrom (procIsAlive). Goals
// without a non-empty goal string are skipped. Results are ordered by
// LastActive descending and capped at maxResumable.
func ListResumableClaudesFrom(claudeDir string) ([]ResumableClaude, error) {
	sessionsDir := filepath.Join(claudeDir, "sessions")
	goalsDir := filepath.Join(claudeDir, "goals")

	// 1. Build the set of LIVE sessionIds from sessions/*.json.
	live := liveSessionIDs(sessionsDir)

	// 2. Scan goals/*.json.
	entries, err := os.ReadDir(goalsDir)
	if err != nil {
		if os.IsNotExist(err) {
			return []ResumableClaude{}, nil
		}
		return nil, fmt.Errorf("read goals dir %q: %w", goalsDir, err)
	}

	out := make([]ResumableClaude, 0, len(entries))
	for _, entry := range entries {
		name := entry.Name()
		if entry.IsDir() || !strings.HasSuffix(name, ".json") {
			continue
		}
		sessionID := strings.TrimSuffix(name, ".json")
		if sessionID == "" {
			continue
		}
		// Skip sessions that are alive (already in the fleet, not resumable).
		if live[sessionID] {
			continue
		}

		path := filepath.Join(goalsDir, name)
		raw, readErr := os.ReadFile(path)
		if readErr != nil {
			continue
		}
		var g goalFile
		if json.Unmarshal(raw, &g) != nil {
			continue
		}
		// No real work to resume without a goal.
		if strings.TrimSpace(g.Goal) == "" {
			continue
		}

		info, statErr := os.Stat(path)
		if statErr != nil {
			continue
		}

		out = append(out, ResumableClaude{
			SessionID:  sessionID,
			Goal:       g.Goal,
			Emojis:     g.Emojis,
			Name:       g.Rename,
			LastActive: info.ModTime().Unix(),
		})
	}

	// 3. Order by LastActive descending (most recent first).
	sort.SliceStable(out, func(i, j int) bool {
		return out[i].LastActive > out[j].LastActive
	})

	// 4. Cap at maxResumable.
	if len(out) > maxResumable {
		out = out[:maxResumable]
	}
	return out, nil
}

// liveSessionIDs scans sessionsDir (sessions/*.json) and returns the set of
// sessionIds whose process is currently alive, applying the same anti-PID-
// recycling check as ListClaudeFleetFrom (procIsAlive matches /proc starttime
// against the recorded procStart). Missing or unparseable files are ignored.
func liveSessionIDs(sessionsDir string) map[string]bool {
	live := make(map[string]bool)
	entries, err := os.ReadDir(sessionsDir)
	if err != nil {
		return live
	}
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
			continue
		}
		raw, readErr := os.ReadFile(filepath.Join(sessionsDir, entry.Name()))
		if readErr != nil {
			continue
		}
		var sess sessionFile
		if json.Unmarshal(raw, &sess) != nil {
			continue
		}
		if sess.PID == 0 || sess.SessionID == "" {
			continue
		}
		if procIsAlive(sess.PID, sess.ProcStart) {
			live[sess.SessionID] = true
		}
	}
	return live
}
