// Package transport — dispatch.go implements the orca.v1.Dispatch
// service: a JSON-over-HTTP interface for cross-node job submission
// and status queries. Routes:
//
//	POST /orca.v1.Dispatch/Submit  ->  SubmitHandler
//	POST /orca.v1.Dispatch/Status  ->  StatusHandler
//
// mTLS is the v0.2 transport (P01). ConnectRPC is NOT used because
// it's not in go.mod (RESEARCH conclusion). The service is mounted on
// the orca daemon's mTLS listener (see internal/daemon/dispatch_handler.go).
package transport

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

// SubmitRequest is the body of POST /orca.v1.Dispatch/Submit.
type SubmitRequest struct {
	Target         string          `json:"target"` // optional explicit node id; empty = bin-pack
	Spec           json.RawMessage `json:"spec"`   // HCL/YAML job spec, opaque to the dispatch service
	IdempotencyKey string          `json:"-"`      // set from X-Orca-Idempotency-Key header, not body
}

// SubmitResponse is the body of a Submit reply.
type SubmitResponse struct {
	JobID  string `json:"job_id"`
	NodeID string `json:"node_id"` // node that actually accepted the job (local or peer)
}

// StatusRequest is the body of POST /orca.v1.Dispatch/Status.
type StatusRequest struct {
	JobID string `json:"job_id"`
}

// StatusResponse is the body of a Status reply.
type StatusResponse struct {
	JobID  string `json:"job_id"`
	NodeID string `json:"node_id"`
	State  string `json:"state"` // "pending" | "running" | "complete" | "failed" | "stopped"
}

// Dispatcher is the contract the HTTP layer uses to actually run a
// job on a node. The engine layer implements this; the HTTP layer
// translates between JSON and Dispatcher calls.
type Dispatcher interface {
	LocalSubmit(ctx context.Context, spec []byte) (jobID string, err error)
	LocalStatus(ctx context.Context, jobID string) (state string, err error)
}

// SubmitHandler is an http.Handler that runs Submit on a local Dispatcher.
// It honors X-Orca-Idempotency-Key for dedupe. Errors are returned
// as JSON with an "error" field and an HTTP status code.
type SubmitHandler struct {
	Dispatcher Dispatcher
	Dedupe     *IdempotencyStore
}

// NewSubmitHandler builds a SubmitHandler.
func NewSubmitHandler(d Dispatcher, dedupe *IdempotencyStore) *SubmitHandler {
	if dedupe == nil {
		dedupe = NewIdempotencyStore()
	}
	return &SubmitHandler{Dispatcher: d, Dedupe: dedupe}
}

// ServeHTTP implements http.Handler.
func (h *SubmitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		writeError(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}
	defer r.Body.Close()
	var req SubmitRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "decode body: "+err.Error())
		return
	}
	if len(req.Spec) == 0 {
		writeError(w, http.StatusBadRequest, "spec is required")
		return
	}
	req.IdempotencyKey = r.Header.Get(IdempotencyHeader)

	// Idempotency check.
	if req.IdempotencyKey != "" {
		if jobID, ok := h.Dedupe.Get(req.IdempotencyKey); ok {
			// Replay the previous response.
			writeJSON(w, http.StatusOK, SubmitResponse{JobID: jobID, NodeID: ""})
			return
		}
	}

	jobID, err := h.Dispatcher.LocalSubmit(r.Context(), req.Spec)
	if err != nil {
		writeError(w, http.StatusInternalServerError, err.Error())
		return
	}
	if req.IdempotencyKey != "" {
		h.Dedupe.Put(req.IdempotencyKey, jobID)
	}
	writeJSON(w, http.StatusOK, SubmitResponse{JobID: jobID, NodeID: "self"})
}

// StatusHandler is an http.Handler that runs Status on a local Dispatcher.
type StatusHandler struct {
	Dispatcher Dispatcher
}

// NewStatusHandler builds a StatusHandler.
func NewStatusHandler(d Dispatcher) *StatusHandler {
	return &StatusHandler{Dispatcher: d}
}

// ServeHTTP implements http.Handler.
func (h *StatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		writeError(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}
	defer r.Body.Close()
	var req StatusRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "decode body: "+err.Error())
		return
	}
	if req.JobID == "" {
		writeError(w, http.StatusBadRequest, "job_id is required")
		return
	}
	state, err := h.Dispatcher.LocalStatus(r.Context(), req.JobID)
	if err != nil {
		writeError(w, http.StatusNotFound, err.Error())
		return
	}
	writeJSON(w, http.StatusOK, StatusResponse{JobID: req.JobID, NodeID: "self", State: state})
}

// DispatchClient is the client-side wrapper that calls Submit/Status
// on a remote peer. It uses mTLS (REQ-011) and the retry helper
// (REQ-037).
type DispatchClient struct {
	HTTP     *MTLSClient
	PeerAddr string // http://host:port or https://host:port
}

// NewDispatchClient builds a DispatchClient for a peer.
func NewDispatchClient(caPath, serverName, peerAddr string) (*DispatchClient, error) {
	c, err := NewMTLSClient(caPath, serverName, "", "")
	if err != nil {
		return nil, fmt.Errorf("NewDispatchClient: %w", err)
	}
	return &DispatchClient{HTTP: c, PeerAddr: peerAddr}, nil
}

// Submit calls POST /orca.v1.Dispatch/Submit on the peer with the
// given spec and idempotency key. Retries per the default policy.
func (c *DispatchClient) Submit(ctx context.Context, spec []byte, idempotencyKey string) (*SubmitResponse, error) {
	if idempotencyKey != "" {
		ctx = WithIdempotencyKey(ctx, idempotencyKey)
	}
	body, _ := json.Marshal(SubmitRequest{Spec: spec})
	policy := DefaultRetryPolicy()
	for attempt := 1; attempt <= policy.MaxAttempts; attempt++ {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.PeerAddr+"/orca.v1.Dispatch/Submit", bytesReader(body))
		req.Header.Set("Content-Type", "application/json")
		if k := IdempotencyKeyFromContext(ctx); k != "" {
			req.Header.Set(IdempotencyHeader, k)
		}
		r, err := c.HTTP.Do(req)
		if err == nil {
			defer r.Body.Close()
			if r.StatusCode == http.StatusOK {
				var resp SubmitResponse
				if derr := json.NewDecoder(r.Body).Decode(&resp); derr == nil {
					return &resp, nil
				} else {
					return nil, fmt.Errorf("DispatchClient.Submit: decode: %w", derr)
				}
			}
			err = fmt.Errorf("status %d", r.StatusCode)
			err = fmt.Errorf("%w: %v", ErrTransient, err)
		} else {
			err = fmt.Errorf("%w: %v", ErrTransient, err)
		}
		// No key, not idempotent: bail on first transient error.
		if IdempotencyKeyFromContext(ctx) == "" {
			return nil, err
		}
		if attempt == policy.MaxAttempts {
			return nil, err
		}
		// Wait with backoff, respecting ctx.
		wait := backoff(policy.Initial, policy.Max, attempt)
		t := time.NewTimer(wait)
		select {
		case <-ctx.Done():
			t.Stop()
			return nil, ctx.Err()
		case <-t.C:
		}
	}
	return nil, fmt.Errorf("DispatchClient.Submit: exhausted attempts")
}

// Status calls POST /orca.v1.Dispatch/Status on the peer. Status is
// idempotent at the verb level, so retries are always safe.
func (c *DispatchClient) Status(ctx context.Context, jobID string) (*StatusResponse, error) {
	body, _ := json.Marshal(StatusRequest{JobID: jobID})
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.PeerAddr+"/orca.v1.Dispatch/Status", bytesReader(body))
	req.Header.Set("Content-Type", "application/json")
	r, err := c.HTTP.Do(req)
	if err != nil {
		return nil, fmt.Errorf("DispatchClient.Status: %w", err)
	}
	defer r.Body.Close()
	if r.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("DispatchClient.Status: status %d", r.StatusCode)
	}
	var resp StatusResponse
	if err := json.NewDecoder(r.Body).Decode(&resp); err != nil {
		return nil, fmt.Errorf("DispatchClient.Status: decode: %w", err)
	}
	return &resp, nil
}

// writeJSON encodes v as JSON and writes it with the given status.
func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

// writeError writes a JSON error response.
func writeError(w http.ResponseWriter, status int, msg string) {
	writeJSON(w, status, map[string]string{"error": msg})
}

// bytesReader is a small helper to keep this file self-contained.
type bytesReadCloser struct {
	b   []byte
	pos int
}

func bytesReader(b []byte) *bytesReadCloser { return &bytesReadCloser{b: b} }

func (r *bytesReadCloser) Read(p []byte) (int, error) {
	if r.pos >= len(r.b) {
		return 0, io.EOF
	}
	n := copy(p, r.b[r.pos:])
	r.pos += n
	return n, nil
}

func (r *bytesReadCloser) Close() error { return nil }
