// Package main provides the repository-local go run launcher behind `make run`.
package main

import (
	"bufio"
	"context"
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"
)

const stopTimeout = 8 * time.Second

type serviceSpec struct {
	Name  string
	Dir   string
	Args  []string
	Ports []int
	Color string
}

type serviceRunner struct {
	spec serviceSpec

	mu       sync.Mutex
	cmd      *exec.Cmd
	done     chan struct{}
	stopping bool
}

var logMu sync.Mutex

func main() {
	serviceList := flag.String("services", "all", "comma-separated services to run, or all")
	flag.Parse()

	root, err := os.Getwd()
	if err != nil {
		fatal(err)
	}
	specs, err := selectServices(*serviceList)
	if err != nil {
		fatal(err)
	}
	if len(specs) == 0 {
		fatal(errors.New("no services selected"))
	}

	ctx, stop := signalContext()
	defer stop()

	runners := make(map[string]*serviceRunner, len(specs))
	for _, spec := range specs {
		runner := &serviceRunner{spec: spec}
		runners[spec.Name] = runner
		killPortOwners(spec)
		if err := runner.start(root); err != nil {
			logf(os.Stderr, spec, "start failed: %v", err)
		}
	}

	printPlain(os.Stdout, "dev-run started %d service(s): %s", len(specs), serviceNames(specs))
	printPlain(os.Stdout, "press Ctrl-C to stop all services")

	<-ctx.Done()
	stopAll(runners)
}

func allServices() []serviceSpec {
	// 启动顺序按低层依赖到入口服务排列；gRPC dial 当前不阻塞，但这样能让日志更接近真实依赖链路。
	return []serviceSpec{
		appService("user-service", []int{13005, 13105}, "36"),
		appService("activity-service", []int{13006, 13106}, "35"),
		appService("wallet-service", []int{13004, 13104}, "33"),
		appService("lucky-gift-service", []int{13013, 13113}, "31"),
		appService("robot-service", []int{13011, 13111}, "93"),
		appService("game-service", []int{13008, 13108}, "32"),
		appService("room-service", []int{13001, 13101}, "34"),
		appService("notice-service", []int{13009, 13109}, "96"),
		appService("cron-service", []int{13007, 13107}, "95"),
		appService("gateway-service", []int{13000}, "92"),
		appService("statistics-service", []int{13010, 13110}, "94"),
		{
			Name:  "luck-gateway",
			Dir:   "server/luck-gateway",
			Args:  []string{"run", "./cmd/server"},
			Ports: []int{13014},
			Color: "36",
		},
		{
			Name:  "admin",
			Dir:   "server/admin",
			Args:  []string{"run", "./cmd/server", "-config", "configs/config.yaml"},
			Ports: []int{13100},
			Color: "91",
		},
	}
}

func appService(name string, ports []int, color string) serviceSpec {
	args := []string{"run", "./services/" + name + "/cmd/server", "-config", ""}
	if devRunMQEnabled() {
		args = []string{"run", "./services/" + name + "/cmd/server", "-config", "services/" + name + "/configs/config.yaml"}
	}
	return serviceSpec{
		Name:  name,
		Dir:   ".",
		Args:  args,
		Ports: ports,
		Color: color,
	}
}

func devRunMQEnabled() bool {
	value := strings.ToLower(strings.TrimSpace(os.Getenv("DEV_RUN_MQ")))
	return value == "1" || value == "true" || value == "yes"
}

func selectServices(raw string) ([]serviceSpec, error) {
	all := allServices()
	if strings.TrimSpace(raw) == "" || strings.TrimSpace(raw) == "all" {
		return all, nil
	}
	byName := make(map[string]serviceSpec, len(all))
	for _, spec := range all {
		byName[spec.Name] = spec
	}
	aliases := map[string]string{
		"gateway":    "gateway-service",
		"gs":         "gateway-service",
		"room":       "room-service",
		"rs":         "room-service",
		"wallet":     "wallet-service",
		"ws":         "wallet-service",
		"user":       "user-service",
		"us":         "user-service",
		"activity":   "activity-service",
		"as":         "activity-service",
		"lucky":      "lucky-gift-service",
		"lucky-gift": "lucky-gift-service",
		"lgs":        "lucky-gift-service",
		"luckygift":  "lucky-gift-service",
		"lg":         "luck-gateway",
		"cron":       "cron-service",
		"cs":         "cron-service",
		"robot":      "robot-service",
		"robots":     "robot-service",
		"rb":         "robot-service",
		"game":       "game-service",
		"games":      "game-service",
		"notice":     "notice-service",
		"ns":         "notice-service",
		"stats":      "statistics-service",
		"statistics": "statistics-service",
	}
	var selected []serviceSpec
	seen := map[string]bool{}
	for _, token := range strings.Split(raw, ",") {
		token = strings.TrimSpace(token)
		if token == "" {
			continue
		}
		if canonical, ok := aliases[token]; ok {
			token = canonical
		}
		spec, ok := byName[token]
		if !ok {
			return nil, fmt.Errorf("unknown service %q", token)
		}
		if !seen[spec.Name] {
			selected = append(selected, spec)
			seen[spec.Name] = true
		}
	}
	return selected, nil
}

func (r *serviceRunner) start(repoRoot string) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	cmd := exec.Command("go", r.spec.Args...)
	cmd.Dir = filepath.Join(repoRoot, r.spec.Dir)
	cmd.Env = append(os.Environ(), "TZ=UTC")
	// go run 会再托管一个编译后的子进程；独立进程组保证重启时能一起收到 SIGINT。
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return err
	}
	stderr, err := cmd.StderrPipe()
	if err != nil {
		return err
	}
	if err := cmd.Start(); err != nil {
		return err
	}
	r.cmd = cmd
	r.done = make(chan struct{})
	r.stopping = false

	go prefixPipe(r.spec, stdout)
	go prefixPipe(r.spec, stderr)
	go r.wait(cmd, r.done)

	logf(os.Stdout, r.spec, "started: go %s", strings.Join(r.spec.Args, " "))
	return nil
}

func (r *serviceRunner) stop() {
	r.mu.Lock()
	cmd := r.cmd
	done := r.done
	r.stopping = true
	r.mu.Unlock()
	if cmd == nil || cmd.Process == nil || done == nil {
		return
	}

	_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT)
	select {
	case <-done:
	case <-time.After(stopTimeout):
		_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
		<-done
	}
}

func (r *serviceRunner) wait(cmd *exec.Cmd, done chan struct{}) {
	err := cmd.Wait()
	close(done)

	r.mu.Lock()
	stopping := r.stopping
	if r.cmd == cmd {
		r.cmd = nil
		r.done = nil
		r.stopping = false
	}
	r.mu.Unlock()

	if err != nil {
		if stopping {
			logf(os.Stdout, r.spec, "stopped")
			return
		}
		logf(os.Stderr, r.spec, "exited: %v", err)
		return
	}
	logf(os.Stdout, r.spec, "exited")
}

func stopAll(runners map[string]*serviceRunner) {
	names := make([]string, 0, len(runners))
	for name := range runners {
		names = append(names, name)
	}
	sort.Strings(names)
	var wg sync.WaitGroup
	for _, name := range names {
		wg.Add(1)
		go func(runner *serviceRunner) {
			defer wg.Done()
			runner.stop()
		}(runners[name])
	}
	wg.Wait()
}

func killPortOwners(spec serviceSpec) {
	seen := map[int]bool{}
	for _, port := range spec.Ports {
		if port <= 0 || seen[port] {
			continue
		}
		seen[port] = true
		pids, err := listeningPIDs(port)
		if err != nil {
			logf(os.Stderr, spec, "port %d lookup failed: %v", port, err)
			continue
		}
		if len(pids) == 0 {
			continue
		}
		for _, pid := range pids {
			// 本地开发启动以抢占端口为准：旧进程可能是 go run 托管的子进程，直接杀监听 PID 最可靠。
			if err := syscall.Kill(pid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {
				logf(os.Stderr, spec, "kill pid %d on port %d failed: %v", pid, port, err)
				continue
			}
			logf(os.Stdout, spec, "killed pid %d occupying port %d", pid, port)
		}
	}
}

func listeningPIDs(port int) ([]int, error) {
	output, err := exec.Command("lsof", "-nP", "-tiTCP:"+strconv.Itoa(port), "-sTCP:LISTEN").Output()
	if err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) && len(output) == 0 {
			return nil, nil
		}
		return nil, err
	}
	lines := strings.Fields(string(output))
	pids := make([]int, 0, len(lines))
	for _, line := range lines {
		pid, err := strconv.Atoi(strings.TrimSpace(line))
		if err != nil {
			continue
		}
		pids = append(pids, pid)
	}
	return pids, nil
}

func prefixPipe(spec serviceSpec, reader io.Reader) {
	scanner := bufio.NewScanner(reader)
	buf := make([]byte, 0, 64*1024)
	scanner.Buffer(buf, 1024*1024)
	for scanner.Scan() {
		logf(os.Stdout, spec, "%s", scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		logf(os.Stderr, spec, "log pipe failed: %v", err)
	}
}

func logf(w io.Writer, spec serviceSpec, format string, args ...any) {
	line := fmt.Sprintf(format, args...)
	prefix := "[" + spec.Name + "]"
	if colorEnabled() && spec.Color != "" {
		prefix = "\x1b[" + spec.Color + "m" + prefix + "\x1b[0m"
	}
	printPlain(w, "%s %s", prefix, line)
}

func printPlain(w io.Writer, format string, args ...any) {
	logMu.Lock()
	defer logMu.Unlock()
	fmt.Fprintf(w, format+"\n", args...)
}

func colorEnabled() bool {
	if _, ok := os.LookupEnv("NO_COLOR"); ok {
		return false
	}
	return os.Getenv("TERM") != "dumb"
}

func serviceNames(specs []serviceSpec) string {
	names := make([]string, 0, len(specs))
	for _, spec := range specs {
		names = append(names, spec.Name)
	}
	return strings.Join(names, ", ")
}

func signalContext() (context.Context, context.CancelFunc) {
	ctx, cancel := context.WithCancel(context.Background())
	signals := make(chan os.Signal, 1)
	signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-signals
		cancel()
	}()
	return ctx, cancel
}

func fatal(err error) {
	fmt.Fprintln(os.Stderr, err)
	os.Exit(1)
}
