// Command agent-gateway runs on the API Gateway VM (172.18.139.186). It
// maintains a WebSocket to the control plane and deploys the gateway —
// which is a PHP project that runs in a pre-loaded php:8.3-apache
// container. The Go agent itself only orchestrates; it does not build
// the PHP code.
package main

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

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

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

// ponytail: the gateway VM holds a single PHP project at the path below.
// Real version reads a yaml config like agent-micro does.
var repos = map[string]string{
	"api-gateway": "/home/user/SDP",
}

func main() {
	cpURL := flag.String("cp", envOr("SDP_CP_URL", "ws://localhost:8080/ws/agent"), "control plane WS URL")
	nodeID := flag.String("node", envOr("SDP_NODE_ID", "gateway"), "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-gateway 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
		}
		var frame struct {
			Op   string                `json:"op"`
			Data protocol.DeployRequest `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":
			repoPath, ok := repos[frame.Data.Repository]
			if !ok {
				emit(out, protocol.Event{
					DeploymentID: frame.Data.DeploymentID,
					Kind:         "status",
					State:        "FAILED",
					At:           time.Now().UnixMilli(),
				})
				continue
			}
			d := deployer.NewPHP(cli, frame.Data.DeploymentID,
				frame.Data.Repository, repoPath,
				frame.Data.Branch, frame.Data.Env,
				gitutil.Creds{Username: frame.Data.Username, Password: frame.Data.Password},
			)
			ctx, cancel := context.WithCancel(context.Background())
			inflight[frame.Data.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()
			}
		}
	}
}

func runDeploy(d *deployer.Deployer, ctx context.Context, out chan<- []byte) {
	events := make(chan protocol.Event, 64)
	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 envOr(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}
