package wisp

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
)

// Common errors.
var (
	ErrNoWispDir   = errors.New("beads directory does not exist")
	ErrNoHook      = errors.New("no hook file found")
	ErrInvalidWisp = errors.New("invalid hook file format")
)

// EnsureDir ensures the .beads directory exists in the given root.
func EnsureDir(root string) (string, error) {
	dir := filepath.Join(root, WispDir)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return "", fmt.Errorf("create beads dir: %w", err)
	}
	return dir, nil
}

// WispPath returns the full path to a file in the beads directory.
func WispPath(root, filename string) string {
	return filepath.Join(root, WispDir, filename)
}

// HookPath returns the full path to an agent's hook file.
func HookPath(root, agent string) string {
	return WispPath(root, HookFilename(agent))
}

// WriteSlungWork writes a slung work hook to the agent's hook file.
func WriteSlungWork(root, agent string, sw *SlungWork) error {
	dir, err := EnsureDir(root)
	if err != nil {
		return err
	}

	path := filepath.Join(dir, HookFilename(agent))
	return writeJSON(path, sw)
}

// ReadHook reads the slung work from an agent's hook file.
// Returns ErrNoHook if no hook file exists.
func ReadHook(root, agent string) (*SlungWork, error) {
	path := HookPath(root, agent)

	data, err := os.ReadFile(path)
	if os.IsNotExist(err) {
		return nil, ErrNoHook
	}
	if err != nil {
		return nil, fmt.Errorf("read hook: %w", err)
	}

	var sw SlungWork
	if err := json.Unmarshal(data, &sw); err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidWisp, err)
	}

	if sw.Type != TypeSlungWork {
		return nil, fmt.Errorf("%w: expected slung-work, got %s", ErrInvalidWisp, sw.Type)
	}

	return &sw, nil
}

// BurnHook removes an agent's hook file after it has been picked up.
func BurnHook(root, agent string) error {
	path := HookPath(root, agent)
	err := os.Remove(path)
	if os.IsNotExist(err) {
		return nil // already burned
	}
	return err
}

// HasHook checks if an agent has a hook file.
func HasHook(root, agent string) bool {
	path := HookPath(root, agent)
	_, err := os.Stat(path)
	return err == nil
}

// ListHooks returns a list of agents with active hooks.
func ListHooks(root string) ([]string, error) {
	dir := filepath.Join(root, WispDir)
	entries, err := os.ReadDir(dir)
	if os.IsNotExist(err) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}

	var agents []string
	for _, e := range entries {
		if agent := AgentFromHookFilename(e.Name()); agent != "" {
			agents = append(agents, agent)
		}
	}
	return agents, nil
}

// writeJSON is a helper to write JSON files atomically.
func writeJSON(path string, v interface{}) error {
	data, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal json: %w", err)
	}

	// Write to temp file then rename for atomicity
	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, data, 0644); err != nil {
		return fmt.Errorf("write temp: %w", err)
	}

	if err := os.Rename(tmp, path); err != nil {
		os.Remove(tmp) // cleanup on failure
		return fmt.Errorf("rename: %w", err)
	}

	return nil
}
