// Package cli implements the user-facing ao command. It stays thin: commands
// discover the local daemon, call its loopback HTTP API, and format output.
package cli

import (
	"context"
	"errors"
	"io"
	"net/http"
	"os"
	"os/exec"
	"time"

	"github.com/spf13/cobra"

	"github.com/aoagents/agent-orchestrator/backend/internal/daemon"
	"github.com/aoagents/agent-orchestrator/backend/internal/processalive"
)

// Execute runs the ao CLI with process stdio.
func Execute() error {
	return NewRootCommand(DefaultDeps()).Execute()
}

// usageError marks a command-line misuse (bad flag, wrong arg count). It lets
// the process entrypoint return exit code 2 for usage errors versus 1 for
// runtime failures, matching the convention CLIs are scripted against.
type usageError struct{ err error }

func (e usageError) Error() string { return e.err.Error() }
func (e usageError) Unwrap() error { return e.err }

// ExitCode maps a CLI error to a process exit code: 2 for usage errors, 1 for
// any other failure, 0 for success.
func ExitCode(err error) int {
	if err == nil {
		return 0
	}
	var ue usageError
	if errors.As(err, &ue) {
		return 2
	}
	return 1
}

// Deps holds the small set of side effects the CLI needs. Tests replace these
// functions without reaching into process-global state.
type Deps struct {
	In  io.Reader
	Out io.Writer
	Err io.Writer

	HTTPClient         *http.Client
	Executable         func() (string, error)
	StartProcess       func(processStartConfig) error
	ProcessAlive       func(pid int) bool
	LookPath           func(file string) (string, error)
	CommandOutput      func(ctx context.Context, name string, args ...string) ([]byte, error)
	CommandOutputInDir func(ctx context.Context, dir, name string, args ...string) ([]byte, error)
	// DoctorGitHubRESTBase lets tests point the doctor GitHub token probe at
	// httptest without mutating package-global state.
	DoctorGitHubRESTBase string
	Now                  func() time.Time
	Sleep                func(time.Duration)
}

// DefaultDeps returns production dependencies.
func DefaultDeps() Deps {
	return Deps{
		In:                   os.Stdin,
		Out:                  os.Stdout,
		Err:                  os.Stderr,
		HTTPClient:           &http.Client{Timeout: 2 * time.Second},
		Executable:           os.Executable,
		StartProcess:         startProcess,
		ProcessAlive:         processalive.Alive,
		LookPath:             exec.LookPath,
		CommandOutput:        commandOutput,
		CommandOutputInDir:   commandOutputInDir,
		DoctorGitHubRESTBase: defaultDoctorGitHubRESTBase,
		Now:                  time.Now,
		Sleep:                time.Sleep,
	}
}

func commandOutput(ctx context.Context, name string, args ...string) ([]byte, error) {
	return exec.CommandContext(ctx, name, args...).CombinedOutput()
}

func commandOutputInDir(ctx context.Context, dir, name string, args ...string) ([]byte, error) {
	cmd := exec.CommandContext(ctx, name, args...)
	cmd.Dir = dir
	return cmd.CombinedOutput()
}

func (d Deps) withDefaults() Deps {
	def := DefaultDeps()
	if d.In == nil {
		d.In = def.In
	}
	if d.Out == nil {
		d.Out = def.Out
	}
	if d.Err == nil {
		d.Err = def.Err
	}
	if d.HTTPClient == nil {
		d.HTTPClient = def.HTTPClient
	}
	if d.Executable == nil {
		d.Executable = def.Executable
	}
	if d.StartProcess == nil {
		d.StartProcess = def.StartProcess
	}
	if d.ProcessAlive == nil {
		d.ProcessAlive = def.ProcessAlive
	}
	if d.LookPath == nil {
		d.LookPath = def.LookPath
	}
	if d.CommandOutput == nil {
		d.CommandOutput = def.CommandOutput
	}
	if d.CommandOutputInDir == nil {
		d.CommandOutputInDir = def.CommandOutputInDir
	}
	if d.DoctorGitHubRESTBase == "" {
		d.DoctorGitHubRESTBase = def.DoctorGitHubRESTBase
	}
	if d.Now == nil {
		d.Now = def.Now
	}
	if d.Sleep == nil {
		d.Sleep = def.Sleep
	}
	return d
}

// NewRootCommand builds a testable root command.
func NewRootCommand(deps Deps) *cobra.Command {
	deps = deps.withDefaults()
	ctx := &commandContext{deps: deps}

	root := &cobra.Command{
		Use:           "ao",
		Short:         "Agent Orchestrator",
		Long:          "Agent Orchestrator manages the local daemon that supervises parallel coding-agent sessions.",
		Version:       VersionString(),
		SilenceUsage:  true,
		SilenceErrors: true,
	}
	root.SetIn(deps.In)
	root.SetOut(deps.Out)
	root.SetErr(deps.Err)
	root.CompletionOptions.DisableDefaultCmd = true
	// Tag flag-parse failures as usage errors so the entrypoint can exit 2 for
	// misuse versus 1 for runtime failures. Subcommands inherit this func.
	root.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
		return usageError{err}
	})

	root.AddCommand(newDaemonCommand())
	root.AddCommand(newStartCommand(ctx))
	root.AddCommand(newStopCommand(ctx))
	root.AddCommand(newStatusCommand(ctx))
	root.AddCommand(newDoctorCommand(ctx))
	root.AddCommand(newSpawnCommand(ctx))
	root.AddCommand(newSendCommand(ctx))
	root.AddCommand(newHooksCommand(ctx))
	root.AddCommand(newProjectCommand(ctx))
	root.AddCommand(newSessionCommand(ctx))
	root.AddCommand(newOrchestratorCommand(ctx))
	root.AddCommand(newReviewCommand(ctx))
	root.AddCommand(newCompletionCommand())
	root.AddCommand(newVersionCommand())

	return root
}

type commandContext struct {
	deps Deps
}

func noArgs(cmd *cobra.Command, args []string) error {
	if err := cobra.ExactArgs(0)(cmd, args); err != nil {
		return usageError{err}
	}
	return nil
}

func newDaemonCommand() *cobra.Command {
	return &cobra.Command{
		Use:    "daemon",
		Short:  "Run the AO backend daemon",
		Hidden: true,
		Args:   noArgs,
		RunE: func(cmd *cobra.Command, args []string) error {
			return daemon.Run()
		},
	}
}
