package run

import (
	"bufio"
	"context"
	"crypto/md5"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/urfave/cli/v2"
	"go-micro.dev/v5/cmd"
	"go-micro.dev/v5/cmd/micro/cli/generate"
	"go-micro.dev/v5/cmd/micro/run/config"
	"go-micro.dev/v5/cmd/micro/run/watcher"
	"go-micro.dev/v5/cmd/micro/server"
)

// Color codes for log output
var colors = []string{
	"\033[31m", // red
	"\033[32m", // green
	"\033[33m", // yellow
	"\033[34m", // blue
	"\033[35m", // magenta
	"\033[36m", // cyan
}

const colorReset = "\033[0m"

func colorFor(idx int) string {
	return colors[idx%len(colors)]
}

// serviceProcess tracks a running service
type serviceProcess struct {
	name       string
	dir        string
	binPath    string
	pidFile    string
	logFile    string
	cmd        *exec.Cmd
	pipeWriter *io.PipeWriter
	color      string
	port       int
	env        []string

	mu      sync.Mutex
	running bool
}

func (s *serviceProcess) start(logDir string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.running {
		return nil
	}

	// Build
	buildCmd := exec.Command("go", "build", "-o", s.binPath, ".")
	buildCmd.Dir = s.dir
	buildOut, buildErr := buildCmd.CombinedOutput()
	if buildErr != nil {
		return fmt.Errorf("build failed: %s\n%s", buildErr, string(buildOut))
	}

	// Open log file
	logFile, err := os.OpenFile(s.logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		return fmt.Errorf("failed to open log file: %w", err)
	}

	// Start process
	s.cmd = exec.Command(s.binPath)
	s.cmd.Dir = s.dir
	s.cmd.Env = append(os.Environ(), s.env...)

	pr, pw := io.Pipe()
	s.pipeWriter = pw
	s.cmd.Stdout = pw
	s.cmd.Stderr = pw

	// Stream output
	go func(name string, color string, pr *io.PipeReader, logFile *os.File) {
		defer logFile.Close()
		scanner := bufio.NewScanner(pr)
		for scanner.Scan() {
			line := scanner.Text()
			fmt.Printf("%s[%s]%s %s\n", color, name, colorReset, line)
			logFile.WriteString("[" + name + "] " + line + "\n")
		}
	}(s.name, s.color, pr, logFile)

	if err := s.cmd.Start(); err != nil {
		pw.Close()
		return fmt.Errorf("failed to start: %w", err)
	}

	// Write PID file
	os.WriteFile(s.pidFile, []byte(fmt.Sprintf("%d\n%s\n%s\n%s\n",
		s.cmd.Process.Pid, s.dir, s.name, time.Now().Format(time.RFC3339))), 0644)

	s.running = true
	fmt.Printf("%s[%s]%s started (pid %d)\n", s.color, s.name, colorReset, s.cmd.Process.Pid)

	return nil
}

func (s *serviceProcess) stop() {
	s.mu.Lock()
	defer s.mu.Unlock()

	if !s.running || s.cmd == nil || s.cmd.Process == nil {
		return
	}

	fmt.Printf("%s[%s]%s stopping...\n", s.color, s.name, colorReset)

	// Graceful shutdown
	s.cmd.Process.Signal(syscall.SIGTERM)

	// Wait with timeout
	done := make(chan error, 1)
	go func() {
		done <- s.cmd.Wait()
	}()

	select {
	case <-done:
	case <-time.After(5 * time.Second):
		s.cmd.Process.Kill()
		<-done
	}

	if s.pipeWriter != nil {
		s.pipeWriter.Close()
	}

	os.Remove(s.pidFile)
	s.running = false
}

func (s *serviceProcess) restart(logDir string) error {
	s.stop()
	return s.start(logDir)
}

// waitForHealth waits for a service's health endpoint to respond
func waitForHealth(port int, timeout time.Duration) bool {
	if port == 0 {
		return true // No port configured, assume ready
	}

	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", port))
		if err == nil {
			resp.Body.Close()
			if resp.StatusCode == 200 {
				return true
			}
		}
		time.Sleep(100 * time.Millisecond)
	}
	return false
}

func Run(c *cli.Context) error {
	// Handle --prompt: generate services first, then run them
	if prompt := c.String("prompt"); prompt != "" {
		return runWithPrompt(c, prompt)
	}

	dir := c.Args().Get(0)
	if dir == "" {
		dir = "."
	}

	// Handle git URLs
	if strings.HasPrefix(dir, "github.com/") || strings.HasPrefix(dir, "https://github.com/") {
		repo := strings.TrimPrefix(dir, "https://")
		tmp, err := os.MkdirTemp("", "micro-run-")
		if err != nil {
			return fmt.Errorf("failed to create temp dir: %w", err)
		}
		defer os.RemoveAll(tmp)

		cloneURL := "https://" + repo
		cloneCmd := exec.Command("git", "clone", "--depth", "1", cloneURL, tmp)
		cloneCmd.Stdout = os.Stdout
		cloneCmd.Stderr = os.Stderr
		if err := cloneCmd.Run(); err != nil {
			return fmt.Errorf("failed to clone %s: %w", cloneURL, err)
		}
		dir = tmp
	}

	absDir, err := filepath.Abs(dir)
	if err != nil {
		return fmt.Errorf("failed to get absolute path: %w", err)
	}

	// Setup directories
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return fmt.Errorf("failed to get home dir: %w", err)
	}
	logsDir := filepath.Join(homeDir, "micro", "logs")
	runDir := filepath.Join(homeDir, "micro", "run")
	binDir := filepath.Join(homeDir, "micro", "bin")

	for _, d := range []string{logsDir, runDir, binDir} {
		if err := os.MkdirAll(d, 0755); err != nil {
			return fmt.Errorf("failed to create %s: %w", d, err)
		}
	}

	// Load configuration
	cfg, err := config.Load(absDir)
	if err != nil {
		return fmt.Errorf("failed to load config: %w", err)
	}

	// Get environment
	envName := c.String("env")
	if envName == "" {
		envName = os.Getenv("MICRO_ENV")
	}
	if envName == "" {
		envName = "development"
	}

	var envVars []string
	if cfg != nil {
		if envMap := cfg.GetEnv(envName); envMap != nil {
			for k, v := range envMap {
				envVars = append(envVars, k+"="+v)
			}
		}
	}

	// Discover services
	var services []*serviceProcess
	servicesByDir := make(map[string]*serviceProcess)

	if cfg != nil && len(cfg.Services) > 0 {
		// Use configured services in dependency order
		sorted, err := cfg.TopologicalSort()
		if err != nil {
			return fmt.Errorf("dependency error: %w", err)
		}

		for i, svc := range sorted {
			svcDir := filepath.Join(absDir, svc.Path)
			absSvcDir, _ := filepath.Abs(svcDir)
			hash := fmt.Sprintf("%x", md5.Sum([]byte(absSvcDir)))[:8]

			sp := &serviceProcess{
				name:    svc.Name,
				dir:     absSvcDir,
				binPath: filepath.Join(binDir, svc.Name+"-"+hash),
				pidFile: filepath.Join(runDir, svc.Name+"-"+hash+".pid"),
				logFile: filepath.Join(logsDir, svc.Name+"-"+hash+".log"),
				color:   colorFor(i),
				port:    svc.Port,
				env:     envVars,
			}
			services = append(services, sp)
			servicesByDir[absSvcDir] = sp
		}
	} else {
		// Auto-discover from main.go files
		var mainFiles []string
		filepath.Walk(absDir, func(path string, info os.FileInfo, err error) error {
			if err != nil || info.IsDir() {
				return nil
			}
			if info.Name() == "main.go" {
				mainFiles = append(mainFiles, path)
			}
			return nil
		})

		if len(mainFiles) == 0 {
			return fmt.Errorf("no main.go files found in %s", absDir)
		}

		for i, mainFile := range mainFiles {
			svcDir := filepath.Dir(mainFile)
			absSvcDir, _ := filepath.Abs(svcDir)

			var name string
			if absSvcDir == absDir {
				name = filepath.Base(absDir)
			} else {
				name = filepath.Base(svcDir)
			}

			hash := fmt.Sprintf("%x", md5.Sum([]byte(absSvcDir)))[:8]

			sp := &serviceProcess{
				name:    name,
				dir:     absSvcDir,
				binPath: filepath.Join(binDir, name+"-"+hash),
				pidFile: filepath.Join(runDir, name+"-"+hash+".pid"),
				logFile: filepath.Join(logsDir, name+"-"+hash+".log"),
				color:   colorFor(i),
				env:     envVars,
			}
			services = append(services, sp)
			servicesByDir[absSvcDir] = sp
		}
	}

	if len(services) == 0 {
		return fmt.Errorf("no services found")
	}

	// Start gateway unless disabled
	var gw *server.Gateway
	gatewayAddr := c.String("address")
	if gatewayAddr == "" {
		gatewayAddr = ":8080"
	}

	if !c.Bool("no-gateway") {
		var err error
		mcpAddr := c.String("mcp-address")
		gw, err = server.StartGateway(server.GatewayOptions{
			Address:     gatewayAddr,
			AuthEnabled: true, // Auth enabled with default admin/micro user
			Context:     context.Background(),
			MCPEnabled:  mcpAddr != "",
			MCPAddress:  mcpAddr,
		})
		if err != nil {
			return fmt.Errorf("failed to start gateway: %w", err)
		}
	}

	// Start services
	for _, svc := range services {
		if err := svc.start(logsDir); err != nil {
			fmt.Fprintf(os.Stderr, "[%s] %v\n", svc.name, err)
			continue
		}

		// Wait for health if port configured
		if svc.port > 0 {
			if !waitForHealth(svc.port, 10*time.Second) {
				fmt.Fprintf(os.Stderr, "[%s] health check timeout\n", svc.name)
			}
		}
	}

	// Print startup banner
	printBanner(services, gw, !c.Bool("no-watch"), c.String("mcp-address"))

	// Setup signal handling
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)

	// Watch mode
	watchEnabled := !c.Bool("no-watch")
	var watch *watcher.Watcher

	if watchEnabled {
		var dirs []string
		for _, svc := range services {
			dirs = append(dirs, svc.dir)
		}

		watch = watcher.New(dirs)
		watch.Start()

		go func() {
			for event := range watch.Events() {
				if svc, ok := servicesByDir[event.Dir]; ok {
					fmt.Printf("%s[%s]%s rebuilding...\n", svc.color, svc.name, colorReset)
					if err := svc.restart(logsDir); err != nil {
						fmt.Fprintf(os.Stderr, "%s[%s]%s restart failed: %v\n", svc.color, svc.name, colorReset, err)
					}
				}
			}
		}()

		// Scan for new services added by micro chat or micro new
		go func() {
			ticker := time.NewTicker(5 * time.Second)
			defer ticker.Stop()
			for {
				select {
				case <-sigCh:
					return
				case <-ticker.C:
					newSvcs := discoverNewServices(absDir, servicesByDir, binDir, runDir, logsDir, envVars, len(services))
					for _, sp := range newSvcs {
						services = append(services, sp)
						servicesByDir[sp.dir] = sp
						watch.AddDir(sp.dir)
						if err := sp.start(logsDir); err != nil {
							fmt.Fprintf(os.Stderr, "[%s] %v\n", sp.name, err)
							continue
						}
						fmt.Printf("\n  \033[32m●\033[0m %s \033[2m(new)\033[0m\n", sp.name)
					}
				}
			}
		}()
	}

	// Wait for signal
	<-sigCh
	fmt.Println("\nShutting down...")

	if watch != nil {
		watch.Stop()
	}

	if gw != nil {
		gw.Stop()
	}

	// Stop services in reverse order
	for i := len(services) - 1; i >= 0; i-- {
		services[i].stop()
	}

	return nil
}

// Helper functions
func parsePid(pidStr string) int {
	pid, _ := strconv.Atoi(pidStr)
	return pid
}

func processRunning(pidStr string) bool {
	pid := parsePid(pidStr)
	if pid <= 0 {
		return false
	}
	proc, err := os.FindProcess(pid)
	if err != nil {
		return false
	}
	return proc.Signal(syscall.Signal(0)) == nil
}

func discoverNewServices(baseDir string, known map[string]*serviceProcess, binDir, runDir, logsDir string, envVars []string, colorOffset int) []*serviceProcess {
	var newSvcs []*serviceProcess
	entries, err := os.ReadDir(baseDir)
	if err != nil {
		return nil
	}
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		svcDir := filepath.Join(baseDir, e.Name())
		absSvcDir, _ := filepath.Abs(svcDir)
		if _, exists := known[absSvcDir]; exists {
			continue
		}
		mainFile := filepath.Join(svcDir, "main.go")
		if _, err := os.Stat(mainFile); err != nil {
			continue
		}
		name := e.Name()
		hash := fmt.Sprintf("%x", md5.Sum([]byte(absSvcDir)))[:8]
		sp := &serviceProcess{
			name:    name,
			dir:     absSvcDir,
			binPath: filepath.Join(binDir, name+"-"+hash),
			pidFile: filepath.Join(runDir, name+"-"+hash+".pid"),
			logFile: filepath.Join(logsDir, name+"-"+hash+".log"),
			color:   colorFor(colorOffset + len(newSvcs)),
			env:     envVars,
		}
		newSvcs = append(newSvcs, sp)
	}
	return newSvcs
}

func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool, mcpAddr string) {
	fmt.Println()
	fmt.Println("  \033[1mMicro\033[0m")
	fmt.Println()

	if gw != nil {
		fmt.Printf("  Dashboard   \033[36mhttp://localhost%s\033[0m\n", gw.Addr())
		fmt.Printf("  API         \033[36mhttp://localhost%s/api/{service}/{method}\033[0m\n", gw.Addr())
		fmt.Printf("  Agent       \033[36mhttp://localhost%s/agent\033[0m\n", gw.Addr())
		fmt.Printf("  Health      \033[36mhttp://localhost%s/health\033[0m\n", gw.Addr())
		if mcpAddr != "" {
			fmt.Printf("  MCP         \033[36mhttp://localhost%s\033[0m\n", mcpAddr)
			fmt.Printf("  MCP Tools   \033[36mhttp://localhost%s/mcp/tools\033[0m\n", mcpAddr)
			fmt.Printf("  WebSocket   \033[36mws://localhost%s/mcp/ws\033[0m\n", mcpAddr)
		}
	}

	var agents, svcs []*serviceProcess
	for _, s := range services {
		if s.name == "agent" {
			agents = append(agents, s)
		} else {
			svcs = append(svcs, s)
		}
	}

	fmt.Println()
	fmt.Println("  Services:")
	for _, svc := range svcs {
		status := "\033[32m●\033[0m"
		if !svc.running {
			status = "\033[31m●\033[0m"
		}
		fmt.Printf("    %s %s\n", status, svc.name)
	}

	if len(agents) > 0 {
		fmt.Println()
		fmt.Println("  Agents:")
		for _, a := range agents {
			status := "\033[35m◆\033[0m"
			if !a.running {
				status = "\033[31m◆\033[0m"
			}
			fmt.Printf("    %s %s\n", status, a.name)
		}
	}

	fmt.Println()
	fmt.Println("  Auth: \033[32menabled\033[0m (admin / micro)")

	if watching {
		fmt.Println("  \033[33mWatching for changes...\033[0m")
	}

	fmt.Println()
	fmt.Println("  \033[2mmicro chat --provider anthropic    # talk to your services\033[0m")

	fmt.Println()
}

func init() {
	cmd.Register(&cli.Command{
		Name:  "run",
		Usage: "Development mode: run services with hot reload and API gateway",
		Description: `Run discovers and runs services in a directory (development mode).

Starts an HTTP gateway on :8080 providing:
  - Web dashboard at /
  - Agent playground at /agent (AI chat with MCP tools)
  - API explorer at /api
  - API proxy at /api/{service}/{endpoint}
  - MCP tools at /mcp/tools
  - Health checks at /health

With a micro.mu or micro.json config file, services start in dependency order.
Without config, all main.go files are discovered and run.

Examples:
  micro run                    # Run with gateway on :8080
  micro run --address :3000    # Gateway on custom port
  micro run --no-gateway       # Services only, no HTTP gateway
  micro run --no-watch         # Disable hot reload
  micro run --env production   # Use production environment
  micro run --mcp-address :3000  # Enable MCP protocol gateway
  micro run --prompt "an order system for dropshipping"  # Generate and run`,
		Action: Run,
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:    "address",
				Aliases: []string{"a"},
				Usage:   "Gateway address (default :8080)",
				Value:   ":8080",
			},
			&cli.BoolFlag{
				Name:  "no-gateway",
				Usage: "Disable HTTP gateway",
			},
			&cli.BoolFlag{
				Name:  "no-watch",
				Usage: "Disable hot reload (file watching)",
			},
			&cli.StringFlag{
				Name:    "env",
				Aliases: []string{"e"},
				Usage:   "Environment to use (default: development)",
				EnvVars: []string{"MICRO_ENV"},
			},
			&cli.StringFlag{
				Name:    "mcp-address",
				Usage:   "MCP gateway address (e.g., :3000). Enables MCP protocol for AI tools.",
				EnvVars: []string{"MICRO_MCP_ADDRESS"},
			},
			&cli.StringFlag{
				Name:    "prompt",
				Usage:   "Describe a system to generate and run (AI designs, builds, and starts services)",
				EnvVars: []string{"MICRO_RUN_PROMPT"},
			},
			&cli.StringFlag{
				Name:    "provider",
				Usage:   "AI provider for --prompt (anthropic, openai, gemini, atlascloud, groq, mistral, together)",
				EnvVars: []string{"MICRO_AI_PROVIDER"},
			},
			&cli.StringFlag{
				Name:    "api_key",
				Usage:   "API key for --prompt (or set ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)",
				EnvVars: []string{"MICRO_AI_API_KEY"},
			},
		},
	})
}

func runWithPrompt(c *cli.Context, prompt string) error {
	provider := c.String("provider")
	apiKey := c.String("api_key")
	if apiKey == "" {
		for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY",
			"ATLASCLOUD_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "TOGETHER_API_KEY", "MICRO_AI_API_KEY"} {
			if v := os.Getenv(env); v != "" {
				apiKey = v
				break
			}
		}
	}
	if apiKey == "" {
		return fmt.Errorf("--api_key or a provider API key env var is required for --prompt")
	}

	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer cancel()

	fmt.Println()
	fmt.Println("  \033[1mmicro run --prompt\033[0m")
	fmt.Println()
	fmt.Printf("  \033[2mDesigning services for:\033[0m %s\n\n", prompt)

	design, err := generate.Design(ctx, provider, apiKey, "", ".", prompt)
	if err != nil {
		return fmt.Errorf("design failed: %w", err)
	}

	fmt.Println("  Services:")
	for _, svc := range design.Services {
		fmt.Printf("    \033[32m●\033[0m \033[36m%s\033[0m — %s\n", svc.Name, svc.Description)
		for _, ep := range svc.Endpoints {
			fmt.Printf("      %s: %s\n", ep.Name, ep.Description)
		}
	}
	fmt.Println()

	if !confirmGenerate() {
		fmt.Println("  Cancelled.")
		return nil
	}

	fmt.Println("  Generating code...")
	if err := generate.Generate(ctx, ".", design, provider, apiKey, ""); err != nil {
		return fmt.Errorf("generate failed: %w", err)
	}
	for _, svc := range design.Services {
		fmt.Printf("    \033[32m✓\033[0m %s/\n", svc.Name)
	}
	fmt.Println()

	// Set env vars so the agent process can pick them up
	if provider != "" {
		os.Setenv("MICRO_AI_PROVIDER", provider)
	}
	os.Setenv("MICRO_AI_API_KEY", apiKey)

	// Now run normally — micro run discovers the generated services + agent
	fmt.Println("  Starting services...")
	fmt.Println()

	cancel()
	c.Set("prompt", "")
	return Run(c)
}

func confirmGenerate() bool {
	fmt.Print("  Generate? [Y/n] ")
	scanner := bufio.NewScanner(os.Stdin)
	if !scanner.Scan() {
		return false
	}
	answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
	return answer == "" || answer == "y" || answer == "yes"
}
