// SPDX-License-Identifier: GPL-3.0-or-later

// Package cli provides the command-line interface for the PrivateBackup client.
package cli

import (
	"context"
	"os"
	"strings"
	"time"

	"github.com/privatebackup/client-frontend-unix/internal/client"
	"github.com/privatebackup/client-frontend-unix/internal/config"
	"github.com/privatebackup/client-frontend-unix/internal/output"
	"github.com/privatebackup/client-frontend-unix/internal/term"
	"github.com/spf13/cobra"
)

// Version information - set at build time.
var (
	Version   = "dev"
	BuildDate = "unknown"
	Commit    = "unknown"
)

// RootOptions contains global CLI options.
type RootOptions struct {
	// Output format
	JSON    bool
	Quiet   bool
	Verbose bool
	NoColor bool

	// Backend connection
	BackendURL string
	Timeout    time.Duration

	// Config
	ConfigFile string
}

// CLI represents the command-line interface.
type CLI struct {
	rootCmd   *cobra.Command
	options   *RootOptions
	configMgr *config.Manager
	client    *client.Client
	renderer  *output.Renderer
	terminal  *term.Terminal
	prompter  *term.Prompter
}

// New creates a new CLI instance.
func New() *CLI {
	cli := &CLI{
		options:   &RootOptions{},
		configMgr: config.NewManager(),
		terminal:  term.Detect(),
		prompter:  term.NewPrompter(),
	}

	cli.rootCmd = cli.newRootCommand()

	return cli
}

// newRootCommand creates the root command.
func (c *CLI) newRootCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "privatebackup",
		Short: "Privacy-focused encrypted backup client",
		Long: `Private Archive is a privacy-focused backup client that encrypts your data
locally before uploading to the Private Archive server via Tor.

Your data is encrypted with a key only you possess. Not even the server
operators can access your files.`,
		SilenceUsage:  true,
		SilenceErrors: true,
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			return c.setup(cmd)
		},
		Version: Version,
	}

	// Global flags
	flags := cmd.PersistentFlags()
	flags.BoolVar(&c.options.JSON, "json", false, "Output in JSON format")
	flags.BoolVarP(&c.options.Quiet, "quiet", "q", false, "Suppress non-essential output")
	flags.BoolVar(&c.options.Verbose, "verbose", false, "Show detailed output")
	flags.BoolVar(&c.options.NoColor, "no-color", false, "Disable colored output")
	flags.StringVar(&c.options.BackendURL, "backend-url", "", "Backend API URL (default: http://localhost:62774)")
	flags.DurationVar(&c.options.Timeout, "timeout", 0, "API request timeout")
	flags.StringVar(&c.options.ConfigFile, "config", "", "Config file path")

	// Set version template
	cmd.SetVersionTemplate(`Private Archive CLI {{.Version}}
`)

	// Add subcommands
	cmd.AddCommand(c.newInitCommand())
	cmd.AddCommand(c.newRestoreAccountCommand())
	cmd.AddCommand(c.newExportKeyCommand())
	cmd.AddCommand(c.newBackupCommand())
	cmd.AddCommand(c.newRestoreCommand())
	cmd.AddCommand(c.newSnapshotsCommand())
	cmd.AddCommand(c.newForgetCommand())
	cmd.AddCommand(c.newCheckCommand())
	cmd.AddCommand(c.newStatusCommand())
	cmd.AddCommand(c.newAccountCommand())
	cmd.AddCommand(c.newUnlockCommand())
	cmd.AddCommand(c.newJobsCommand())
	cmd.AddCommand(c.newConfigCommand())
	cmd.AddCommand(c.newCredentialsCommand())
	cmd.AddCommand(c.newDaemonCommand())
	cmd.AddCommand(c.newServiceCommand())
	cmd.AddCommand(c.newCompletionCommand())
	cmd.AddCommand(c.newSelfUpdateCommand())
	// Simplified commands (simple mode friendly)
	cmd.AddCommand(c.newPathsCommand())
	cmd.AddCommand(c.newModeCommand())
	cmd.AddCommand(c.newPayCommand())

	return cmd
}

// setup initializes CLI components based on flags.
func (c *CLI) setup(cmd *cobra.Command) error {
	// Set config file if specified
	if c.options.ConfigFile != "" {
		c.configMgr.SetConfigFile(c.options.ConfigFile)
	}

	// Load configuration
	cfg, err := c.configMgr.Load()
	if err != nil {
		// Configuration load failure is not fatal for most commands
		cfg = &config.Config{}
	}

	// Determine output format
	format := output.FormatHuman
	if c.options.JSON {
		format = output.FormatJSON
	}

	// Create renderer
	opts := []output.Option{
		output.WithFormat(format),
		output.WithTerminal(c.terminal),
		output.WithQuiet(c.options.Quiet),
		output.WithVerbose(c.options.Verbose),
	}
	if c.options.NoColor || cfg.Output.Color == "never" || (cfg.Output.Color == "auto" && !c.terminal.HasColors) {
		opts = append(opts, output.WithNoColor())
	}
	c.renderer = output.NewRenderer(opts...)

	// Determine backend URL
	backendURL := c.options.BackendURL
	if backendURL == "" {
		backendURL = cfg.Backend.URL
	}
	if backendURL == "" {
		backendURL = config.DefaultBackendURL
	}

	// Determine timeout
	timeout := c.options.Timeout
	if timeout == 0 {
		timeout = cfg.Backend.Timeout
	}
	if timeout == 0 {
		timeout = config.DefaultTimeout
	}

	// Create backend client
	c.client = client.New(&client.Options{
		BaseURL: backendURL,
		Timeout: timeout,
	})

	return nil
}

// Execute runs the CLI.
func (c *CLI) Execute() int {
	if err := c.rootCmd.Execute(); err != nil {
		if c.renderer != nil {
			c.renderer.Error(err.Error())
		} else {
			// Renderer not initialized, use stderr
			os.Stderr.WriteString("Error: " + err.Error() + "\n")
		}
		return 1
	}
	return 0
}

// newCompletionCommand creates the completion command for shell auto-completion.
func (c *CLI) newCompletionCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "completion [bash|zsh|fish|powershell]",
		Short: "Generate shell completion scripts",
		Long: `Generate shell completion scripts for privatebackup.

To load completions:

Bash:
  $ source <(privatebackup completion bash)

  # To load completions for each session, execute once:
  # Linux:
  $ privatebackup completion bash > /etc/bash_completion.d/privatebackup

  # macOS:
  $ privatebackup completion bash > $(brew --prefix)/etc/bash_completion.d/privatebackup

Zsh:
  $ source <(privatebackup completion zsh)

  # To load completions for each session, execute once:
  $ privatebackup completion zsh > "${fpath[1]}/_privatebackup"

Fish:
  $ privatebackup completion fish | source

  # To load completions for each session, execute once:
  $ privatebackup completion fish > ~/.config/fish/completions/privatebackup.fish

PowerShell:
  PS> privatebackup completion powershell | Out-String | Invoke-Expression

  # To load completions for each session, execute once:
  PS> privatebackup completion powershell > privatebackup.ps1
  # and source this file from your PowerShell profile.
`,
		DisableFlagsInUseLine: true,
		ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
		Args:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
		RunE: func(cmd *cobra.Command, args []string) error {
			switch args[0] {
			case "bash":
				return c.rootCmd.GenBashCompletion(os.Stdout)
			case "zsh":
				return c.rootCmd.GenZshCompletion(os.Stdout)
			case "fish":
				return c.rootCmd.GenFishCompletion(os.Stdout, true)
			case "powershell":
				return c.rootCmd.GenPowerShellCompletionWithDesc(os.Stdout)
			}
			return nil
		},
	}

	return cmd
}

// GetContext returns a context with timeout from options.
func (c *CLI) GetContext() (context.Context, context.CancelFunc) {
	timeout := c.options.Timeout
	if timeout == 0 {
		timeout = config.DefaultTimeout
	}
	return context.WithTimeout(context.Background(), timeout)
}

// checkBackendConnection checks if the backend is available.
func (c *CLI) checkBackendConnection() error {
	ctx, cancel := c.GetContext()
	defer cancel()

	_, err := c.client.Health(ctx)
	if err != nil {
		if isConnectionError(err) {
			return &BackendNotRunningError{}
		}
		return err
	}

	return nil
}

// BackendNotRunningError indicates the backend daemon is not running.
type BackendNotRunningError struct{}

func (e *BackendNotRunningError) Error() string {
	return "cannot connect to backend daemon"
}

// IsBackendNotRunning checks if the error is a backend not running error.
func IsBackendNotRunning(err error) bool {
	_, ok := err.(*BackendNotRunningError)
	return ok
}

// isConnectionError checks if an error is a connection error.
func isConnectionError(err error) bool {
	if err == nil {
		return false
	}
	// Check for common connection error strings
	errStr := err.Error()
	return contains(errStr, "connection refused") ||
		contains(errStr, "no such host") ||
		contains(errStr, "network is unreachable") ||
		contains(errStr, "dial tcp")
}

// contains checks if s contains substr using the standard library.
func contains(s, substr string) bool {
	return strings.Contains(s, substr)
}
