package cli

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/config"
	"github.com/spf13/cobra"
	"go.yaml.in/yaml/v3"
)

var configCmd = &cobra.Command{
	Use:    "config",
	Short:  "Manage pentestswarm configuration",
	Hidden: true, // 4.8.2: 'init' covers the common path; config.show is power-user
}

var configInitCmd = &cobra.Command{
	Use:     "init",
	Short:   "Interactive setup wizard — creates config.yaml",
	Example: "  pentestswarm config init",
	RunE: func(cmd *cobra.Command, args []string) error {
		reader := bufio.NewReader(os.Stdin)

		fmt.Println(colorCyan("\n  Pentest Swarm AI — Configuration Wizard\n"))
		fmt.Println(colorDim("  This will create a config.yaml file in the current directory.\n"))

		// Provider
		fmt.Printf("  %s\n", colorBold("LLM Provider"))
		fmt.Println("  1) Claude (Anthropic API — best quality, recommended)")
		fmt.Println("  2) Ollama (local models — full privacy, needs GPU)")
		fmt.Println("  3) LM Studio (local, OpenAI-compatible)")
		fmt.Println("  4) OrcaRouter (one endpoint, many frontier models — Claude, GPT, etc.)")
		fmt.Println("  5) OpenRouter (400+ models on one key; per-role mixture — DeepSeek V4.1 Flash + GLM-5.3-Flash)")
		fmt.Println()
		provider := prompt(reader, "  Choose provider [1/2/3/4/5]", "1")

		var providerName, apiKey, endpoint, model string

		switch provider {
		case "1", "claude":
			providerName = "claude"
			model = "claude-sonnet-4-6"
			apiKey = prompt(reader, "  Claude API key (sk-ant-...)", "")
			if apiKey == "" {
				fmt.Println(colorYellow("  No API key provided. Set PENTESTSWARM_ORCHESTRATOR_API_KEY env var later."))
			}
		case "2", "ollama":
			providerName = "ollama"
			endpoint = prompt(reader, "  Ollama endpoint", "http://localhost:11434")
			model = prompt(reader, "  Model name", "llama3.1:8b")
		case "3", "lmstudio":
			providerName = "lmstudio"
			endpoint = prompt(reader, "  LM Studio endpoint", "http://localhost:1234")
			model = prompt(reader, "  Model name", "")
		case "4", "orcarouter":
			providerName = "orcarouter"
			model = prompt(reader, "  Model (use openai/gpt-5.5 unless you need a specific one)", "openai/gpt-5.5")
			apiKey = prompt(reader, "  OrcaRouter API key (sk-orca-...)", "")
			endpoint = prompt(reader, "  OrcaRouter endpoint", "https://api.orcarouter.ai/v1")
			if apiKey == "" {
				fmt.Println(colorYellow("  No API key provided. Set PENTESTSWARM_ORCHESTRATOR_API_KEY env var later."))
			}
		case "5", "openrouter":
			providerName = "openrouter"
			model = prompt(reader, "  Model (any OpenRouter slug; blank routes DeepSeek V4.1 Flash + GLM-5.3-Flash per role)", "deepseek/deepseek-v4.1-flash")
			apiKey = prompt(reader, "  OpenRouter API key (sk-or-...)", "")
			endpoint = prompt(reader, "  OpenRouter endpoint", "https://openrouter.ai/api/v1")
			if apiKey == "" {
				fmt.Println(colorYellow("  No API key provided. Set PENTESTSWARM_ORCHESTRATOR_API_KEY env var later."))
			}
		default:
			providerName = "claude"
			model = "claude-sonnet-4-6"
		}

		fmt.Println()

		// Output format
		format := prompt(reader, "  Default report format [md/html/json/all]", "md")

		// Write config
		cfg := fmt.Sprintf(`# pentestswarm configuration
# Generated by: pentestswarm config init

server:
  host: "0.0.0.0"
  port: 8080

orchestrator:
  provider: "%s"
  model: "%s"
  api_key: "%s"
  endpoint: "%s"
  context_window: 200000
  max_tokens: 8192
  temperature: 0.1

# All agents inherit from orchestrator by default.
# Override per-agent only if using local fine-tuned models.
agents:
  recon: {}
  classifier: {}
  exploit: {}
  report: {}

tools:
  default_timeout: 300

scope:
  enforce_strict: true

logging:
  level: "info"
  format: "console"

# Default report format
# report_format: "%s"
`, providerName, model, apiKey, endpoint, format)

		configPath := "config.yaml"
		if err := os.WriteFile(configPath, []byte(cfg), 0644); err != nil {
			return fmt.Errorf("writing config: %w", err)
		}

		fmt.Println(colorGreen(fmt.Sprintf("  Config written to %s", configPath)))
		fmt.Println()
		fmt.Println("  Next steps:")
		fmt.Printf("    %s  Run a health check\n", colorDim("pentestswarm doctor"))
		fmt.Printf("    %s  Launch your first scan\n", colorDim("pentestswarm scan target.com --scope target.com"))
		fmt.Println()

		return nil
	},
}

var configShowCmd = &cobra.Command{
	Use:     "show",
	Short:   "Print the effective configuration (secrets redacted)",
	Example: "  pentestswarm config show\n  pentestswarm config show --json",
	RunE: func(cmd *cobra.Command, args []string) error {
		settings, err := config.LoadSettings(cfgFile)
		if err != nil {
			return fmt.Errorf("loading config: %w", err)
		}
		redacted := redactSecrets(settings)

		if OutputIsJSON() {
			enc := json.NewEncoder(os.Stdout)
			enc.SetIndent("", "  ")
			return enc.Encode(redacted)
		}

		out, err := yaml.Marshal(redacted)
		if err != nil {
			return fmt.Errorf("rendering config: %w", err)
		}
		fmt.Print(string(out))
		return nil
	},
}

var configPathCmd = &cobra.Command{
	Use:   "path",
	Short: "Print the resolved config file path",
	Long: `Prints the config file pentestswarm would load right now — honouring
--config and any active workspace ('pentestswarm workspace current'),
falling back to the default search order (./config.yaml,
~/.pentestswarm/config.yaml, /etc/pentestswarm/config.yaml).`,
	Example: "  pentestswarm config path",
	RunE: func(cmd *cobra.Command, args []string) error {
		path, exists := resolveConfigPath()
		if abs, err := filepath.Abs(path); err == nil {
			path = abs
		}

		if OutputIsJSON() {
			enc := json.NewEncoder(os.Stdout)
			enc.SetIndent("", "  ")
			return enc.Encode(map[string]any{"path": path, "exists": exists})
		}

		fmt.Println(path)
		if !exists {
			fmt.Println(colorDim("  (file does not exist yet — defaults + env vars are in effect)"))
		}
		return nil
	},
}

var configGetCmd = &cobra.Command{
	Use:     "get <key>",
	Short:   "Print the effective value of a dotted config key",
	Args:    cobra.ExactArgs(1),
	Example: "  pentestswarm config get orchestrator.provider\n  pentestswarm config get orchestrator.model",
	RunE: func(cmd *cobra.Command, args []string) error {
		settings, err := config.LoadSettings(cfgFile)
		if err != nil {
			return fmt.Errorf("loading config: %w", err)
		}

		val, ok := lookupSetting(settings, args[0])
		if !ok {
			return fmt.Errorf("key %q not found in effective config", args[0])
		}

		if OutputIsJSON() {
			enc := json.NewEncoder(os.Stdout)
			enc.SetIndent("", "  ")
			return enc.Encode(map[string]any{"key": args[0], "value": val})
		}

		fmt.Printf("%v\n", val)
		return nil
	},
}

var configSetCmd = &cobra.Command{
	Use:   "set <key> <value>",
	Short: "Set a dotted config key in the config file",
	Long: `Writes <key>=<value> into the config file pentestswarm would load
(honouring --config and any active workspace), preserving the rest of
the file's structure and comments. Creates the file if it doesn't
exist yet.

Common keys: orchestrator.provider, orchestrator.model,
orchestrator.api_key, orchestrator.endpoint,
orchestrator.context_window, orchestrator.max_tokens,
orchestrator.temperature, server.host, server.port, server.api_key,
logging.level, logging.format.`,
	Args: cobra.ExactArgs(2),
	Example: `  pentestswarm config set orchestrator.provider claude
  pentestswarm config set orchestrator.model claude-opus-4-7
  pentestswarm config set server.port 9090`,
	RunE: func(cmd *cobra.Command, args []string) error {
		key, value := args[0], args[1]
		path, _ := resolveConfigPath()

		if err := setConfigKey(path, key, value); err != nil {
			return err
		}

		fmt.Printf("  %s  %s = %s  (%s)\n", colorGreen("[ok]"), colorCyan(key), value, colorDim(path))
		if strings.EqualFold(key, "scope.enforce_strict") {
			fmt.Println(colorYellow("  note: scope.enforce_strict is hard-coded to true at runtime for safety — this write has no effect."))
		}
		return nil
	},
}

// resolveConfigPath returns the config file pentestswarm would load given
// the current --config flag / active workspace (cfgFile is already
// rewritten to the active workspace's path by the time any RunE sees it —
// see resolveActiveWorkspaceConfig in workspace.go), mirroring
// internal/config.Load's own search order. The second return value
// reports whether that path actually exists on disk.
func resolveConfigPath() (path string, exists bool) {
	if cfgFile != "" {
		_, err := os.Stat(cfgFile)
		return cfgFile, err == nil
	}

	candidates := []string{"config.yaml"}
	if home, err := os.UserHomeDir(); err == nil {
		candidates = append(candidates, filepath.Join(home, ".pentestswarm", "config.yaml"))
	}
	candidates = append(candidates, "/etc/pentestswarm/config.yaml")

	for _, c := range candidates {
		if _, err := os.Stat(c); err == nil {
			return c, true
		}
	}
	return candidates[0], false
}

// redactSecrets returns a deep copy of settings with any string value
// whose key looks secret (api_key, password, token, secret — matching
// the substrings the original `config show` redacted) replaced with
// "****". Non-secret values, and empty secrets, pass through unchanged.
func redactSecrets(m map[string]interface{}) map[string]interface{} {
	out := make(map[string]interface{}, len(m))
	for k, v := range m {
		if nested, ok := v.(map[string]interface{}); ok {
			out[k] = redactSecrets(nested)
			continue
		}
		if isSecretKey(k) {
			if s, ok := v.(string); ok && s != "" {
				out[k] = "****"
				continue
			}
		}
		out[k] = v
	}
	return out
}

func isSecretKey(k string) bool {
	lk := strings.ToLower(k)
	return strings.Contains(lk, "api_key") || strings.Contains(lk, "password") ||
		strings.Contains(lk, "token") || strings.Contains(lk, "secret")
}

// lookupSetting walks a dotted key ("orchestrator.provider") through a
// nested settings map as returned by config.LoadSettings.
func lookupSetting(settings map[string]interface{}, key string) (interface{}, bool) {
	var cur interface{} = settings
	for _, part := range strings.Split(key, ".") {
		m, ok := cur.(map[string]interface{})
		if !ok {
			return nil, false
		}
		v, ok := m[strings.ToLower(part)]
		if !ok {
			return nil, false
		}
		cur = v
	}
	return cur, true
}

// setConfigKey writes <key>=<value> into the YAML file at path, creating
// the file (and any parent directories) if it doesn't exist. It edits a
// yaml.Node tree rather than unmarshaling into a plain map so existing
// comments and key ordering survive the round-trip; only the target key's
// value node is touched (or added, if new).
func setConfigKey(path, key, value string) error {
	if strings.TrimSpace(key) == "" {
		return fmt.Errorf("key must not be empty")
	}
	parts := strings.Split(key, ".")
	for _, p := range parts {
		if p == "" {
			return fmt.Errorf("invalid key %q: empty path segment", key)
		}
	}

	doc, err := readOrInitYAMLDoc(path)
	if err != nil {
		return err
	}

	root := doc.Content[0]
	if root.Kind != yaml.MappingNode {
		return fmt.Errorf("%s: top level is not a mapping — cannot set keys", path)
	}
	if err := setNestedKey(root, parts, value); err != nil {
		return fmt.Errorf("%s: %w", path, err)
	}

	out, err := yaml.Marshal(doc)
	if err != nil {
		return fmt.Errorf("rendering %s: %w", path, err)
	}
	if dir := filepath.Dir(path); dir != "." && dir != "" {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("creating %s: %w", dir, err)
		}
	}
	if err := os.WriteFile(path, out, 0o644); err != nil {
		return fmt.Errorf("writing %s: %w", path, err)
	}
	return nil
}

// readOrInitYAMLDoc reads path into a yaml.Node document tree, or returns
// a fresh empty-mapping document if the file doesn't exist / is empty.
func readOrInitYAMLDoc(path string) (*yaml.Node, error) {
	empty := &yaml.Node{Kind: yaml.DocumentNode, Content: []*yaml.Node{{Kind: yaml.MappingNode}}}

	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return empty, nil
		}
		return nil, fmt.Errorf("reading %s: %w", path, err)
	}

	var doc yaml.Node
	if err := yaml.Unmarshal(data, &doc); err != nil {
		return nil, fmt.Errorf("parsing %s: %w", path, err)
	}
	if doc.Kind != yaml.DocumentNode || len(doc.Content) == 0 || doc.Content[0].Kind != yaml.MappingNode {
		// Empty file, or a file that isn't a top-level mapping — start fresh
		// rather than fail; a genuinely malformed non-mapping file would
		// already have failed config.Load elsewhere.
		return empty, nil
	}
	return &doc, nil
}

// setNestedKey walks (creating as needed) mapping nodes for parts[:-1],
// then sets the scalar value for the final segment. Leaving the value
// node's Tag/Style unset lets the YAML resolver infer bool/int/float vs
// string on the next read, the same way a hand-written "port: 9090"
// would — so numeric/bool keys don't get force-quoted into strings.
func setNestedKey(mapping *yaml.Node, parts []string, value string) error {
	key := parts[0]
	idx := findMapKey(mapping, key)

	if len(parts) == 1 {
		valNode := &yaml.Node{Kind: yaml.ScalarNode, Value: value}
		if idx >= 0 {
			mapping.Content[idx+1] = valNode
		} else {
			mapping.Content = append(mapping.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, valNode)
		}
		return nil
	}

	if idx >= 0 {
		child := mapping.Content[idx+1]
		if child.Kind != yaml.MappingNode {
			return fmt.Errorf("cannot set %q: %q is not a mapping", strings.Join(parts, "."), key)
		}
		return setNestedKey(child, parts[1:], value)
	}

	child := &yaml.Node{Kind: yaml.MappingNode}
	mapping.Content = append(mapping.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, child)
	return setNestedKey(child, parts[1:], value)
}

func findMapKey(mapping *yaml.Node, key string) int {
	for i := 0; i+1 < len(mapping.Content); i += 2 {
		if mapping.Content[i].Value == key {
			return i
		}
	}
	return -1
}

var configValidateCmd = &cobra.Command{
	Use:     "validate",
	Short:   "Validate the configuration file",
	Example: "  pentestswarm config validate",
	RunE: func(cmd *cobra.Command, args []string) error {
		_, err := os.ReadFile("config.yaml")
		if err != nil {
			return fmt.Errorf("no config.yaml found — run 'pentestswarm config init'")
		}
		// TODO: load and validate with config.Validate
		fmt.Println(colorGreen("  Config is valid."))
		return nil
	},
}

func prompt(reader *bufio.Reader, question, defaultVal string) string {
	if defaultVal != "" {
		fmt.Printf("  %s [%s]: ", question, colorDim(defaultVal))
	} else {
		fmt.Printf("  %s: ", question)
	}

	input, _ := reader.ReadString('\n')
	input = strings.TrimSpace(input)

	if input == "" {
		return defaultVal
	}
	return input
}

func init() {
	configCmd.AddCommand(configInitCmd)
	configCmd.AddCommand(configShowCmd)
	configCmd.AddCommand(configValidateCmd)
	configCmd.AddCommand(configPathCmd)
	configCmd.AddCommand(configGetCmd)
	configCmd.AddCommand(configSetCmd)

	rootCmd.AddCommand(configCmd)
}
