// Command agent-micro runs on the microservices VM (172.18.136.92). It
// maintains a WebSocket to the control plane, accepts deploy/stop/RPC
// frames, and runs the build+container pipeline locally for Go
// microservices.
package main

import (
	"context"
	"encoding/json"
	"flag"
	"log"
	"net/url"
	"os"
	"sync"
	"time"

	"github.com/gorilla/websocket"
	docker "github.com/moby/moby/client"

	"github.com/sdp/agentlib/deployer"
	"github.com/sdp/agentlib/gitutil"
	"github.com/sdp/protocol"
)

// ponytail: hand-curated from REQUIREMENTS.md. Real version reads a yaml
// config file. Adding a new service = one line.
var repos = map[string]string{
	"account":      "/home/user/AppGolang/account",
	"payment":      "/home/user/AppGolang/payment",
	"user":         "/home/user/AppGolang/user",
	"notification": "/home/user/AppGolang/notification",
}

func main() {
	cpURL := flag.String("cp", envOr("SDP_CP_URL", "ws://localhost:3452/ws/agent"), "control plane WS URL")
	nodeID := flag.String("node", envOr("SDP_NODE_ID", "micro"), "node id sent in WS query")
	flag.Parse()

	cli, err := docker.NewClientWithOpts(docker.FromEnv)
	if err != nil {
		log.Fatalf("docker client: %v", err)
	}

	u, _ := url.Parse(*cpURL)
	q := u.Query()
	q.Set("node", *nodeID)
	u.RawQuery = q.Encode()

	out := make(chan []byte, 128)
	var connMu sync.Mutex
	var conn *websocket.Conn

	go writer(&conn, &connMu, out)

	for {
		c, err := dial(u)
		if err != nil {
			log.Printf("dial: %v; retrying in 2s", err)
			time.Sleep(2 * time.Second)
			continue
		}
		connMu.Lock()
		conn = c
		connMu.Unlock()
		log.Printf("agent-micro connected as %s", *nodeID)

		readLoop(c, cli, out, &connMu, &conn)
	}
}

func dial(u *url.URL) (*websocket.Conn, error) {
	log.Printf("connecting to %s", u)
	c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
	return c, err
}

// writer pumps outbound events to whichever conn is current. If conn is
// nil (during reconnect), messages buffer until the next conn is set.
func writer(conn **websocket.Conn, mu *sync.Mutex, out <-chan []byte) {
	for msg := range out {
		mu.Lock()
		c := *conn
		mu.Unlock()
		if c == nil {
			continue
		}
		if err := c.WriteMessage(websocket.TextMessage, msg); err != nil {
			log.Printf("write: %v", err)
		}
	}
}

type runState struct {
	deployer *deployer.Deployer
	cancel   context.CancelFunc
}

func readLoop(c *websocket.Conn, cli *docker.Client, out chan<- []byte, mu *sync.Mutex, connPtr **websocket.Conn) {
	inflight := map[string]*runState{}
	for {
		_, raw, err := c.ReadMessage()
		if err != nil {
			log.Printf("read: %v", err)
			mu.Lock()
			if *connPtr == c {
				*connPtr = nil
			}
			mu.Unlock()
			_ = c.Close()
			return
		}
		// Inbound frame: {op, data, id}. Op is the verb. data is op-specific.
		var frame struct {
			Op   string          `json:"op"`
			Data json.RawMessage `json:"data"`
			ID   string          `json:"id"`
		}
		if err := json.Unmarshal(raw, &frame); err != nil {
			log.Printf("bad frame: %v", err)
			continue
		}
		switch frame.Op {
		case "deploy":
			var req protocol.DeployRequest
			if err := json.Unmarshal(frame.Data, &req); err != nil {
				log.Printf("bad deploy data: %v", err)
				continue
			}
			repoPath, ok := repos[req.Repository]
			if !ok {
				emit(out, protocol.Event{
					DeploymentID: req.DeploymentID,
					Kind:         "status",
					State:        "FAILED",
					At:           time.Now().UnixMilli(),
				})
				continue
			}
			d := deployer.NewGo(cli, req.DeploymentID,
				req.Repository, repoPath,
				req.Branch, req.HostPort, req.Env,
				gitutil.Creds{Username: req.Username, Password: req.Password},
			)
			ctx, cancel := context.WithCancel(context.Background())
			inflight[req.DeploymentID] = &runState{deployer: d, cancel: cancel}
			go runDeploy(d, ctx, out)
		case "stop":
			if rs, ok := inflight[frame.ID]; ok {
				_ = rs.deployer.Stop(context.Background())
				rs.cancel() // unblock StreamLogs
			}
		case "list_repos":
			handleListRepos(out, frame.ID)
		case "list_branches":
			var body struct {
				Repo string `json:"repo"`
			}
			_ = json.Unmarshal(frame.Data, &body)
			handleListBranches(out, frame.ID, body.Repo)
		case "probe":
			var body struct {
				Repo     string `json:"repo"`
				Username string `json:"username"`
				Password string `json:"password"`
			}
			_ = json.Unmarshal(frame.Data, &body)
			handleProbe(out, frame.ID, body.Repo, body.Username, body.Password)
		}
	}
}

func handleListRepos(out chan<- []byte, id string) {
	// Micro agent serves all repos except the api-gateway. The control
	// plane calls both nodes for the union; the gateway will return
	// only its own repo.
	list := make([]protocol.RepoInfo, 0, len(repos))
	for name, path := range repos {
		list = append(list, protocol.RepoInfo{Name: name, Path: path})
	}
	replyOK(out, id, map[string]any{"repos": list})
}

func handleListBranches(out chan<- []byte, id, repo string) {
	path, ok := repos[repo]
	if !ok {
		replyErr(out, id, "unknown repo: "+repo)
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	branches, err := gitutil.ListBranches(ctx, path)
	if err != nil {
		replyErr(out, id, err.Error())
		return
	}
	replyOK(out, id, map[string]any{"ok": true, "branches": branches})
}

func handleProbe(out chan<- []byte, id, repo, user, pass string) {
	path, ok := repos[repo]
	if !ok {
		replyErr(out, id, "unknown repo: "+repo)
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := gitutil.Probe(ctx, path, gitutil.Creds{Username: user, Password: pass}); err != nil {
		replyErr(out, id, err.Error())
		return
	}
	replyOK(out, id, map[string]any{"ok": true})
}

func runDeploy(d *deployer.Deployer, ctx context.Context, out chan<- []byte) {
	events := make(chan protocol.Event, 64)
	// producer: Run pipelines, then StreamLogs tails the container. Both
	// write to the same channel. We close it when both are done so the
	// drain loop below can exit.
	go func() {
		state := d.Run(ctx, events)
		if state == "RUNNING" {
			d.StreamLogs(ctx, events)
		}
		close(events)
	}()

	for e := range events {
		emit(out, e)
	}
}

func emit(out chan<- []byte, e protocol.Event) {
	b, _ := json.Marshal(e)
	select {
	case out <- b:
	default:
		// ponytail: drop on backpressure. Deploys are rare.
	}
}

func replyOK(out chan<- []byte, id string, data interface{}) {
	reply(out, id, true, "", data)
}

func replyErr(out chan<- []byte, id, errMsg string) {
	reply(out, id, false, errMsg, nil)
}

func reply(out chan<- []byte, id string, ok bool, errMsg string, data interface{}) {
	frame := map[string]any{
		"op":   "reply",
		"id":   id,
		"ok":   ok,
		"data": data,
	}
	if errMsg != "" {
		frame["error"] = errMsg
	}
	b, _ := json.Marshal(frame)
	select {
	case out <- b:
	default:
	}
}

func envOr(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}
