// Package sync owns all conversations with the cloud LabCore API: register,
// heartbeat, push results, push events, pull worklist, manifest reconcile.
package sync

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

// Device is one analyzer the cloud has registered against this agent.
type Device struct {
	ID             int64                  `json:"id"`
	Vendor         string                 `json:"vendor"`
	Model          string                 `json:"model"`
	Name           string                 `json:"name"`
	ConnectionType string                 `json:"connection_type"`
	CommProtocol   string                 `json:"comm_protocol"`
	Host           string                 `json:"host"`
	Port           int                    `json:"port"`
	ComPort        string                 `json:"com_port"`
	Baud           int                    `json:"baud"`
	Parity         string                 `json:"parity"`
	DataBits       int                    `json:"data_bits"`
	StopBits       int                    `json:"stop_bits"`
	Status         string                 `json:"status"`
	Settings       map[string]interface{} `json:"settings"`
}

// RegisterResponse comes back from /api/lab/agent/register.
type RegisterResponse struct {
	Agent struct {
		ID   int64  `json:"id"`
		Name string `json:"name"`
		Code string `json:"code"`
	} `json:"agent"`
	Devices []Device `json:"devices"`
}

// Client is the HTTP client to LabCore. Reuses an http.Client with sensible
// timeouts so a wedged cloud never blocks the agent forever.
type Client struct {
	base       string
	token      string
	version    string
	http       *http.Client
}

// New constructs a client. cloudURL is the cloud ERP base (e.g.
// "https://erp.example.com" or "http://localhost/static-purchase/public").
func New(cloudURL, token, version string) *Client {
	return &Client{
		base:    strings.TrimRight(cloudURL, "/"),
		token:   token,
		version: version,
		http: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// Register tells the cloud the agent is starting and pulls the device list.
func (c *Client) Register() (*RegisterResponse, error) {
	body := map[string]string{
		"agent_version": c.version,
	}
	var out RegisterResponse
	if err := c.post("/api/lab/agent/register", body, &out); err != nil {
		return nil, fmt.Errorf("register: %w", err)
	}
	return &out, nil
}

// Heartbeat reports the agent's queue depth + liveness.
func (c *Client) Heartbeat(queueDepth int) error {
	body := map[string]interface{}{
		"agent_version": c.version,
		"queue_depth":   queueDepth,
	}
	return c.post("/api/lab/agent/heartbeat", body, nil)
}

// PushResult posts one raw device message to the cloud. The cloud parses,
// stores, and returns {status, session_id, results}. Idempotent on uuid.
type PushResultResponse struct {
	Status    string `json:"status"`
	SessionID int64  `json:"session_id"`
	Results   int    `json:"results"`
	Reason    string `json:"reason"`
}

func (c *Client) PushResult(uuid, vendor, raw string, deviceID *int64, receivedAt time.Time) (*PushResultResponse, error) {
	body := map[string]interface{}{
		"uuid":        uuid,
		"vendor":      vendor,
		"raw_message": raw,
		"received_at": receivedAt.UTC().Format(time.RFC3339),
	}
	if deviceID != nil {
		body["device_id"] = *deviceID
	}
	var out PushResultResponse
	if err := c.post("/api/lab/results", body, &out); err != nil {
		return nil, err
	}
	return &out, nil
}

// PushEvents batches log/error events to the cloud.
func (c *Client) PushEvents(events []map[string]interface{}) error {
	body := map[string]interface{}{"events": events}
	return c.post("/api/lab/events", body, nil)
}

// PullWorklist gets pending orders for this agent's devices.
func (c *Client) PullWorklist() ([]map[string]interface{}, error) {
	var out struct {
		Worklist []map[string]interface{} `json:"worklist"`
	}
	if err := c.get("/api/lab/worklist", &out); err != nil {
		return nil, err
	}
	return out.Worklist, nil
}

// SyncManifest sends the UUIDs we hold; cloud responds with which it's missing.
func (c *Client) SyncManifest(uuids []string) ([]string, error) {
	body := map[string]interface{}{"uuids": uuids}
	var out struct {
		Missing []string `json:"missing"`
	}
	if err := c.post("/api/lab/sync/manifest", body, &out); err != nil {
		return nil, err
	}
	return out.Missing, nil
}

// -----------------------------------------------------------------------

func (c *Client) post(path string, body interface{}, out interface{}) error {
	raw, err := json.Marshal(body)
	if err != nil {
		return err
	}
	req, err := http.NewRequest("POST", c.base+path, bytes.NewReader(raw))
	if err != nil {
		return err
	}
	c.setHeaders(req)
	return c.do(req, out)
}

func (c *Client) get(path string, out interface{}) error {
	req, err := http.NewRequest("GET", c.base+path, nil)
	if err != nil {
		return err
	}
	c.setHeaders(req)
	return c.do(req, out)
}

func (c *Client) setHeaders(req *http.Request) {
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+c.token)
	req.Header.Set("User-Agent", "LabBridgeAgent/"+c.version)
}

func (c *Client) do(req *http.Request, out interface{}) error {
	resp, err := c.http.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 400 {
		return fmt.Errorf("HTTP %d %s: %s", resp.StatusCode, resp.Status, strings.TrimSpace(string(body)))
	}
	if out == nil || len(body) == 0 {
		return nil
	}
	return json.Unmarshal(body, out)
}
