package edgecmd

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"

	"github.com/spf13/cobra"
	"gopkg.in/yaml.v3"

	"iop/apps/edge/internal/node"
	"iop/packages/go/config"
	"iop/packages/go/hostsetup"
)

func LoadEdgeConfig(cmd *cobra.Command) (*config.EdgeConfig, string, error) {
	path, err := ResolveConfigPath(cmd)
	if err != nil {
		return nil, "", err
	}
	cfg, err := config.LoadEdge(path)
	if err != nil {
		return nil, "", fmt.Errorf("load config: %w", err)
	}
	return cfg, path, nil
}

func configCmd() *cobra.Command {
	c := &cobra.Command{
		Use:   "config",
		Short: "Create, inspect, and validate edge config",
		Long: `Create, inspect, and validate the bundle-local edge.yaml configuration.

Typical local flow:
  1. iop-edge config init
  2. iop-edge env
  3. iop-edge node register <node-id>
  4. iop-edge config check
  5. iop-edge serve`,
		Example: `  iop-edge config init
  iop-edge env
  iop-edge node register node-silicon-ollama --adapter ollama
  iop-edge config check`,
	}
	c.AddCommand(configInitCmd(), configPrintCmd(), configCheckCmd())
	return c
}

func configInitCmd() *cobra.Command {
	output := "edge.yaml"
	var force bool
	var stdout bool
	c := &cobra.Command{
		Use:   "init",
		Short: "Write an edge.yaml template",
		Long: "Write a deployable edge.yaml template to the current bundle directory or stdout.\n" +
			"Run this beside a local iop-edge binary, then run `iop-edge config check`.",
		Example: "  iop-edge config init\n" +
			"  iop-edge config init --stdout\n" +
			"  iop-edge config init --output edge.yaml\n" +
			"  iop-edge config check",
		RunE: func(cmd *cobra.Command, _ []string) error {
			template := []byte(hostsetup.EdgeBundleConfigTemplate())
			if stdout || output == "-" {
				_, err := cmd.OutOrStdout().Write(template)
				return err
			}
			if !force {
				if _, err := os.Stat(output); err == nil {
					return fmt.Errorf("%s already exists (use --force to overwrite)", output)
				} else if !errors.Is(err, os.ErrNotExist) {
					return fmt.Errorf("stat output: %w", err)
				}
			}
			if dir := filepath.Dir(output); dir != "" && dir != "." {
				if err := os.MkdirAll(dir, 0o755); err != nil {
					return fmt.Errorf("mkdir %s: %w", dir, err)
				}
			}
			if err := os.WriteFile(output, template, 0o600); err != nil {
				return fmt.Errorf("write %s: %w", output, err)
			}
			fmt.Fprintf(cmd.OutOrStdout(), "wrote %s\n", output)
			return nil
		},
	}
	c.Flags().StringVarP(&output, "output", "o", output, "output path")
	c.Flags().BoolVar(&stdout, "stdout", false, "print template to stdout instead of writing a file")
	c.Flags().BoolVar(&force, "force", false, "overwrite an existing output file")
	return c
}

func configPrintCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "print",
		Short: "Print effective edge config as YAML",
		RunE: func(cmd *cobra.Command, _ []string) error {
			cfg, _, err := LoadEdgeConfig(cmd)
			if err != nil {
				return err
			}
			enc := yaml.NewEncoder(cmd.OutOrStdout())
			defer enc.Close()
			return enc.Encode(cfg)
		},
	}
}

func configCheckCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "check",
		Short: "Validate edge config can be loaded",
		RunE: func(cmd *cobra.Command, _ []string) error {
			cfg, path, err := LoadEdgeConfig(cmd)
			if err != nil {
				return err
			}
			if err := validateEdgeConfig(cfg); err != nil {
				return fmt.Errorf("validate config: %w", err)
			}
			fmt.Fprintf(cmd.OutOrStdout(), "OK %s\n", path)
			return nil
		},
	}
}

func anyOllamaEnabled(a config.AdaptersConf) bool {
	if a.Ollama.Enabled {
		return true
	}
	for _, inst := range a.OllamaInstances {
		if inst.Enabled {
			return true
		}
	}
	return false
}

func anyVllmEnabled(a config.AdaptersConf) bool {
	if a.Vllm.Enabled {
		return true
	}
	for _, inst := range a.VllmInstances {
		if inst.Enabled {
			return true
		}
	}
	return false
}

func validateEdgeConfig(cfg *config.EdgeConfig) error {
	_, err := node.LoadFromConfig(cfg.Nodes)
	if err != nil {
		return err
	}

	for i, n := range cfg.Nodes {
		name := n.ID
		if name == "" {
			name = n.Alias
		}
		if name == "" {
			name = fmt.Sprintf("index %d", i)
		}

		hasOllama := anyOllamaEnabled(n.Adapters)
		hasVllm := anyVllmEnabled(n.Adapters)
		if !hasOllama && !n.Adapters.CLI.Enabled && !hasVllm {
			return fmt.Errorf("node %q: at least one adapter must be enabled", name)
		}
		if n.Adapters.Ollama.Enabled && n.Adapters.Ollama.BaseURL == "" {
			return fmt.Errorf("node %q: ollama adapter base_url must not be empty", name)
		}
		for j, inst := range n.Adapters.OllamaInstances {
			if inst.Enabled && inst.BaseURL == "" {
				return fmt.Errorf("node %q: ollama_instances[%d] %q: base_url must not be empty", name, j, inst.Name)
			}
		}
		if n.Adapters.Vllm.Enabled && n.Adapters.Vllm.Endpoint == "" {
			return fmt.Errorf("node %q: vllm adapter endpoint must not be empty", name)
		}
		for j, inst := range n.Adapters.VllmInstances {
			if inst.Enabled && inst.Endpoint == "" {
				return fmt.Errorf("node %q: vllm_instances[%d] %q: endpoint must not be empty", name, j, inst.Name)
			}
		}
		if n.Runtime.Concurrency < 0 {
			return fmt.Errorf("node %q: runtime concurrency must be non-negative", name)
		}
	}
	return nil
}
