package api

import (
	"context"
	"encoding/json"
	"net/http"
	"strconv"
	"time"

	"github.com/sdp/control-plane/internal/store"
	"github.com/sdp/protocol"
)

// handleListDeployments reads deployment history from SQLite. Filterable
// by sandbox via ?sandbox=ID. Limit via ?limit=N (default 100, max 500).
func (s *Server) handleListDeployments(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "GET only", http.StatusMethodNotAllowed)
		return
	}
	sandboxID := r.URL.Query().Get("sandbox")
	limit := 100
	if l := r.URL.Query().Get("limit"); l != "" {
		if n, err := strconv.Atoi(l); err == nil {
			limit = n
		}
	}
	deps, err := s.st.ListDeployments(sandboxID, limit)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, err.Error())
		return
	}
	if deps == nil {
		deps = []store.Deployment{} // never null in JSON
	}
	writeJSON(w, http.StatusOK, deps)
}

type createDeployReq struct {
	Repository string            `json:"repository"`
	Branch     string            `json:"branch"`
	Env        map[string]string `json:"env,omitempty"`
	Username   string            `json:"username"`
	Password   string            `json:"password"`
	// SandboxID optionally ties this deploy to a sandbox. When set, the
	// sandbox's host_port for the repo is used as the container's
	// published port and the env is the union of (sandbox env) and
	// (request env).
	SandboxID string `json:"sandboxId,omitempty"`
	// HostPort overrides the auto-allocated port. Use 0 to allocate.
	HostPort int `json:"hostPort,omitempty"`
}

// handleCreateDeployment is the Slice-2-friendly deploy path. It
// resolves the repo to a node, looks up the sandbox's port (if any),
// and dispatches the deploy frame.
func (s *Server) handleCreateDeployment(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	var body createDeployReq
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		http.Error(w, "bad json", http.StatusBadRequest)
		return
	}
	if body.Repository == "" || body.Branch == "" {
		writeErr(w, http.StatusBadRequest, "repository and branch required")
		return
	}
	node, err := s.resolveNode(body.Repository, body.SandboxID)
	if err != nil {
		writeErr(w, http.StatusBadRequest, err.Error())
		return
	}
	s.agents.mu.RLock()
	connected := s.agents.conns[node]
	s.agents.mu.RUnlock()
	if !connected {
		writeErr(w, http.StatusServiceUnavailable, "agent "+node+" not connected")
		return
	}
	user := s.userFromContext(r)
	id := newID()

	hostPort := body.HostPort
	env := body.Env
	if body.SandboxID != "" {
		sb, err := s.st.GetSandbox(body.SandboxID)
		if err == nil {
			for _, svc := range sb.Services {
				if svc.Repo == body.Repository {
					if hostPort == 0 {
						hostPort = svc.HostPort
					}
					if svc.EnvID != "" {
						if e, err := s.st.GetEnvironment(svc.EnvID); err == nil {
							if env == nil {
								env = map[string]string{}
							}
							for k, v := range e.Values {
								env[k] = v
							}
						}
					}
					break
				}
			}
		}
	}
	if err := s.st.StartDeploymentInSandbox(id, body.SandboxID, body.Repository, body.Branch, user, hostPort); err != nil {
		writeErr(w, http.StatusInternalServerError, err.Error())
		return
	}

	req := protocol.DeployRequest{
		DeploymentID: id,
		SandboxID:    body.SandboxID,
		Repository:   body.Repository,
		Branch:       body.Branch,
		HostPort:     hostPort,
		Env:          env,
		Username:     body.Username,
		Password:     body.Password,
	}
	payload, _ := json.Marshal(map[string]any{"op": "deploy", "data": req})
	if !s.hub.SendToAgent(node, payload) {
		writeErr(w, http.StatusServiceUnavailable, "agent buffer full")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"id": id})
}

type stopDeployReq struct {
	ID   string `json:"id"`
	Node string `json:"node"`
}

func (s *Server) handleStopDeployment(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	var body stopDeployReq
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		http.Error(w, "bad json", http.StatusBadRequest)
		return
	}
	if body.Node == "" {
		// try to find the node from the deployment row
		if d, err := s.st.GetDeployment(body.ID); err == nil && d != nil {
			body.Node, _ = s.resolveNode(d.Repository, d.SandboxID)
		}
	}
	if body.Node == "" {
		writeErr(w, http.StatusBadRequest, "node required")
		return
	}
	payload, _ := json.Marshal(map[string]any{"op": "stop", "id": body.ID})
	if !s.hub.SendToAgent(body.Node, payload) {
		writeErr(w, http.StatusServiceUnavailable, "agent not reachable")
		return
	}
	_ = s.st.FinishDeployment(body.ID, "STOPPED")
	writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}

// resolveNode picks which agent serves a repo. The gateway agent owns
// the api-gateway repo. Everything else is the micro agent.
func (s *Server) resolveNode(repo, sandboxID string) (string, error) {
	if repo == "api-gateway" {
		return "gateway", nil
	}
	return "micro", nil
}

// callAgentPushRoutes pushes a routes table to the gateway agent.
func (s *Server) callAgentPushRoutes(ctx context.Context, sandboxID string, payload []protocol.RouteOverride) error {
	ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
	defer cancel()
	data := map[string]any{
		"sandboxId": sandboxID,
		"routes":    payload,
	}
	raw, err := s.hub.CallAgent(ctx, "gateway", "push_routes", data, 0)
	if err != nil {
		return err
	}
	var reply struct {
		OK  bool   `json:"ok"`
		Err string `json:"error,omitempty"`
	}
	if err := json.Unmarshal(raw, &reply); err != nil {
		return err
	}
	if !reply.OK {
		return wsError(reply.Err)
	}
	return nil
}
