// Package checkpoint provides session checkpointing for crash recovery.
// When a polecat session dies (context limit, crash, timeout), checkpoints
// allow the next session to recover state and resume work.
package checkpoint

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/steveyegge/gastown/internal/runtime"
)

// Filename is the checkpoint file name within the polecat directory.
const Filename = ".polecat-checkpoint.json"

// Checkpoint represents a session recovery checkpoint.
type Checkpoint struct {
	// MoleculeID is the current molecule being worked.
	MoleculeID string `json:"molecule_id,omitempty"`

	// CurrentStep is the step ID currently in progress.
	CurrentStep string `json:"current_step,omitempty"`

	// StepTitle is the human-readable title of the current step.
	StepTitle string `json:"step_title,omitempty"`

	// ModifiedFiles lists files modified since the last commit.
	ModifiedFiles []string `json:"modified_files,omitempty"`

	// LastCommit is the SHA of the last commit.
	LastCommit string `json:"last_commit,omitempty"`

	// Branch is the current git branch.
	Branch string `json:"branch,omitempty"`

	// HookedBead is the bead ID on the agent's hook.
	HookedBead string `json:"hooked_bead,omitempty"`

	// Timestamp is when the checkpoint was written.
	Timestamp time.Time `json:"timestamp"`

	// SessionID identifies the session that wrote the checkpoint.
	SessionID string `json:"session_id,omitempty"`

	// Notes contains optional context from the session.
	Notes string `json:"notes,omitempty"`
}

// Path returns the checkpoint file path for a given polecat directory.
func Path(polecatDir string) string {
	return filepath.Join(polecatDir, Filename)
}

// Read loads a checkpoint from the polecat directory.
// Returns nil, nil if no checkpoint exists.
func Read(polecatDir string) (*Checkpoint, error) {
	path := Path(polecatDir)

	data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed from trusted polecatDir
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("reading checkpoint: %w", err)
	}

	var cp Checkpoint
	if err := json.Unmarshal(data, &cp); err != nil {
		return nil, fmt.Errorf("parsing checkpoint: %w", err)
	}

	return &cp, nil
}

// Write saves a checkpoint to the polecat directory.
func Write(polecatDir string, cp *Checkpoint) error {
	// Set timestamp if not already set
	if cp.Timestamp.IsZero() {
		cp.Timestamp = time.Now()
	}

	// Set session ID from environment if available
	if cp.SessionID == "" {
		cp.SessionID = runtime.SessionIDFromEnv()
		if cp.SessionID == "" {
			cp.SessionID = fmt.Sprintf("pid-%d", os.Getpid())
		}
	}

	data, err := json.MarshalIndent(cp, "", "  ")
	if err != nil {
		return fmt.Errorf("marshaling checkpoint: %w", err)
	}

	path := Path(polecatDir)
	if err := os.WriteFile(path, data, 0600); err != nil {
		return fmt.Errorf("writing checkpoint: %w", err)
	}

	return nil
}

// Remove deletes the checkpoint file.
func Remove(polecatDir string) error {
	path := Path(polecatDir)
	if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("removing checkpoint: %w", err)
	}
	return nil
}

// Capture creates a checkpoint by capturing current git and work state.
func Capture(polecatDir string) (*Checkpoint, error) {
	cp := &Checkpoint{
		Timestamp: time.Now(),
	}

	// Get modified files from git status
	cmd := exec.Command("git", "status", "--porcelain")
	cmd.Dir = polecatDir
	output, err := cmd.Output()
	if err == nil {
		lines := strings.Split(strings.TrimSpace(string(output)), "\n")
		for _, line := range lines {
			if len(line) > 3 {
				// Format: XY filename
				file := strings.TrimSpace(line[3:])
				if file != "" {
					cp.ModifiedFiles = append(cp.ModifiedFiles, file)
				}
			}
		}
	}

	// Get last commit SHA
	cmd = exec.Command("git", "rev-parse", "HEAD")
	cmd.Dir = polecatDir
	output, err = cmd.Output()
	if err == nil {
		cp.LastCommit = strings.TrimSpace(string(output))
	}

	// Get current branch
	cmd = exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
	cmd.Dir = polecatDir
	output, err = cmd.Output()
	if err == nil {
		cp.Branch = strings.TrimSpace(string(output))
	}

	return cp, nil
}

// WithMolecule adds molecule context to a checkpoint.
func (cp *Checkpoint) WithMolecule(moleculeID, stepID, stepTitle string) *Checkpoint {
	cp.MoleculeID = moleculeID
	cp.CurrentStep = stepID
	cp.StepTitle = stepTitle
	return cp
}

// WithHookedBead adds hooked bead context to a checkpoint.
func (cp *Checkpoint) WithHookedBead(beadID string) *Checkpoint {
	cp.HookedBead = beadID
	return cp
}

// WithNotes adds context notes to a checkpoint.
func (cp *Checkpoint) WithNotes(notes string) *Checkpoint {
	cp.Notes = notes
	return cp
}

// Age returns how long ago the checkpoint was written.
func (cp *Checkpoint) Age() time.Duration {
	return time.Since(cp.Timestamp)
}

// IsStale returns true if the checkpoint is older than the threshold.
func (cp *Checkpoint) IsStale(threshold time.Duration) bool {
	return cp.Age() > threshold
}

// Summary returns a concise summary of the checkpoint.
func (cp *Checkpoint) Summary() string {
	var parts []string

	if cp.MoleculeID != "" {
		if cp.CurrentStep != "" {
			parts = append(parts, fmt.Sprintf("molecule %s, step %s", cp.MoleculeID, cp.CurrentStep))
		} else {
			parts = append(parts, fmt.Sprintf("molecule %s", cp.MoleculeID))
		}
	}

	if cp.HookedBead != "" {
		parts = append(parts, fmt.Sprintf("hooked: %s", cp.HookedBead))
	}

	if len(cp.ModifiedFiles) > 0 {
		parts = append(parts, fmt.Sprintf("%d modified files", len(cp.ModifiedFiles)))
	}

	if cp.Branch != "" {
		parts = append(parts, fmt.Sprintf("branch: %s", cp.Branch))
	}

	if len(parts) == 0 {
		return "no significant state"
	}

	return strings.Join(parts, ", ")
}
