package server

import (
	"context"
	"encoding/json"
	"errors"
	"log/slog"
	"net/http"
	"sync"
	"time"

	"github.com/coder/websocket"

	"github.com/uopi/uopi/internal/broker"
	"github.com/uopi/uopi/internal/datasource"
	"github.com/uopi/uopi/internal/metrics"
)

// ── Incoming message types ────────────────────────────────────────────────────

type inMsg struct {
	Type string `json:"type"`

	// subscribe / unsubscribe
	Signals []sigRef `json:"signals,omitempty"`

	// write
	DS    string          `json:"ds,omitempty"`
	Name  string          `json:"name,omitempty"`
	Value json.RawMessage `json:"value,omitempty"`

	// history
	Start     time.Time `json:"start"`
	End       time.Time `json:"end"`
	MaxPoints int       `json:"maxPoints,omitempty"`
}

type sigRef struct {
	DS   string `json:"ds"`
	Name string `json:"name"`
}

// ── Outgoing message types ────────────────────────────────────────────────────

type outMsg struct {
	Type string `json:"type"`

	// update
	DS      string `json:"ds,omitempty"`
	Name    string `json:"name,omitempty"`
	TS      string `json:"ts,omitempty"`
	Value   any    `json:"value,omitempty"`
	Quality string `json:"quality,omitempty"`

	// meta
	Meta *metaPayload `json:"meta,omitempty"`

	// history
	Points []histPoint `json:"points,omitempty"`

	// error
	Code    string `json:"code,omitempty"`
	Message string `json:"message,omitempty"`
}

type metaPayload struct {
	Type        string   `json:"type"`
	Unit        string   `json:"unit,omitempty"`
	Description string   `json:"description,omitempty"`
	DisplayLow  float64  `json:"displayLow"`
	DisplayHigh float64  `json:"displayHigh"`
	DriveLow    float64  `json:"driveLow,omitempty"`
	DriveHigh   float64  `json:"driveHigh,omitempty"`
	EnumStrings []string `json:"enumStrings,omitempty"`
	Writable    bool     `json:"writable"`
}

type histPoint struct {
	TS    string `json:"ts"`
	Value any    `json:"value"`
}

// ── Handler ───────────────────────────────────────────────────────────────────

type wsHandler struct {
	broker *broker.Broker
	log    *slog.Logger
}

func (h *wsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
		// Permissive origin check for LAN / SSH-tunnel use; tighten when auth lands.
		InsecureSkipVerify: true,
	})
	if err != nil {
		h.log.Error("ws accept failed", "err", err)
		return
	}
	metrics.IncWsConns()
	defer metrics.DecWsConns()

	ctx, cancel := context.WithCancel(r.Context())
	defer cancel()

	c := &wsClient{
		conn:     conn,
		broker:   h.broker,
		outCh:    make(chan []byte, 128),
		updateCh: make(chan broker.Update, 256),
		subs:     make(map[broker.SignalRef]func()),
		log:      h.log,
	}

	var wg sync.WaitGroup
	wg.Add(3)
	go func() { defer wg.Done(); c.readLoop(ctx, cancel) }()
	go func() { defer wg.Done(); c.dispatchLoop(ctx) }()
	go func() { defer wg.Done(); c.writeLoop(ctx) }()
	wg.Wait()

	// Cancel all subscriptions on disconnect.
	c.subsMu.Lock()
	for _, cancelSub := range c.subs {
		cancelSub()
	}
	c.subsMu.Unlock()

	_ = conn.Close(websocket.StatusNormalClosure, "")
}

// ── wsClient ──────────────────────────────────────────────────────────────────

type wsClient struct {
	conn   *websocket.Conn
	broker *broker.Broker
	log    *slog.Logger

	outCh    chan []byte        // serialised outgoing messages
	updateCh chan broker.Update // raw updates from the broker

	subsMu sync.Mutex
	subs   map[broker.SignalRef]func() // ref → cancel
}

// readLoop reads incoming WebSocket messages and dispatches them.
// It cancels ctx (and therefore the other goroutines) on any error.
func (c *wsClient) readLoop(ctx context.Context, cancel context.CancelFunc) {
	defer cancel()
	for {
		_, data, err := c.conn.Read(ctx)
		if err != nil {
			if !errors.Is(err, context.Canceled) {
				c.log.Debug("ws read", "err", err)
			}
			return
		}
		c.handleMessage(ctx, data)
	}
}

// dispatchLoop converts broker updates to JSON and enqueues them for writing.
func (c *wsClient) dispatchLoop(ctx context.Context) {
	for {
		select {
		case u := <-c.updateCh:
			msg := outMsg{
				Type:    "update",
				DS:      u.Ref.DS,
				Name:    u.Ref.Name,
				TS:      u.Value.Timestamp.UTC().Format(time.RFC3339Nano),
				Value:   u.Value.Data,
				Quality: u.Value.Quality.String(),
			}
			b, err := json.Marshal(msg)
			if err != nil {
				continue
			}
			select {
			case c.outCh <- b:
			case <-ctx.Done():
				return
			}
		case <-ctx.Done():
			return
		}
	}
}

// writeLoop sends queued messages over the WebSocket connection.
func (c *wsClient) writeLoop(ctx context.Context) {
	for {
		select {
		case msg := <-c.outCh:
			if err := c.conn.Write(ctx, websocket.MessageText, msg); err != nil {
				return
			}
			metrics.IncMsgOut()
		case <-ctx.Done():
			return
		}
	}
}

// handleMessage parses and routes a single incoming client message.
func (c *wsClient) handleMessage(ctx context.Context, data []byte) {
	metrics.IncMsgIn()
	var msg inMsg
	if err := json.Unmarshal(data, &msg); err != nil {
		c.sendError(ctx, "PARSE_ERROR", "invalid JSON: "+err.Error())
		return
	}

	switch msg.Type {
	case "subscribe":
		c.handleSubscribe(ctx, msg.Signals)
	case "unsubscribe":
		c.handleUnsubscribe(msg.Signals)
	case "write":
		c.handleWrite(ctx, msg)
	case "history":
		c.handleHistory(ctx, msg)
	default:
		c.sendError(ctx, "UNKNOWN_TYPE", "unknown message type: "+msg.Type)
	}
}

func (c *wsClient) handleSubscribe(ctx context.Context, refs []sigRef) {
	for _, r := range refs {
		ref := broker.SignalRef{DS: r.DS, Name: r.Name}

		c.subsMu.Lock()
		_, already := c.subs[ref]
		c.subsMu.Unlock()
		if already {
			continue
		}

		cancelSub, err := c.broker.Subscribe(ref, c.updateCh)
		if err != nil {
			c.sendError(ctx, "SUBSCRIBE_ERROR", err.Error())
			continue
		}

		c.subsMu.Lock()
		c.subs[ref] = cancelSub
		c.subsMu.Unlock()

		// Send metadata once on subscribe.
		c.sendMeta(ctx, ref)
	}
}

func (c *wsClient) handleUnsubscribe(refs []sigRef) {
	for _, r := range refs {
		ref := broker.SignalRef{DS: r.DS, Name: r.Name}
		c.subsMu.Lock()
		if cancel, ok := c.subs[ref]; ok {
			cancel()
			delete(c.subs, ref)
		}
		c.subsMu.Unlock()
	}
}

func (c *wsClient) handleWrite(ctx context.Context, msg inMsg) {
	ds, ok := c.broker.Source(msg.DS)
	if !ok {
		c.sendError(ctx, "NOT_FOUND", "unknown data source: "+msg.DS)
		return
	}

	// Enforce write permission before touching the value payload.
	meta, err := ds.GetMetadata(ctx, msg.Name)
	if err != nil {
		c.sendError(ctx, "NOT_FOUND", "unknown signal: "+msg.Name)
		return
	}
	if !meta.Writable {
		c.sendError(ctx, "NOT_WRITABLE", "signal is read-only: "+msg.Name)
		return
	}

	// Decode the JSON value as the appropriate Go type.
	var value any
	if err := json.Unmarshal(msg.Value, &value); err != nil {
		c.sendError(ctx, "PARSE_ERROR", "invalid value: "+err.Error())
		return
	}

	metrics.IncWrites()
	if err := ds.Write(ctx, msg.Name, value); err != nil {
		c.sendError(ctx, "WRITE_ERROR", err.Error())
	}
}

func (c *wsClient) handleHistory(ctx context.Context, msg inMsg) {
	metrics.IncHistoryReqs()
	ds, ok := c.broker.Source(msg.DS)
	if !ok {
		c.sendError(ctx, "NOT_FOUND", "unknown data source: "+msg.DS)
		return
	}

	maxPts := msg.MaxPoints
	if maxPts <= 0 {
		maxPts = 5000
	}

	vals, err := ds.History(ctx, msg.Name, msg.Start, msg.End, maxPts)
	if err != nil {
		c.sendError(ctx, "HISTORY_ERROR", err.Error())
		return
	}

	pts := make([]histPoint, len(vals))
	for i, v := range vals {
		pts[i] = histPoint{
			TS:    v.Timestamp.UTC().Format(time.RFC3339Nano),
			Value: v.Data,
		}
	}

	resp := outMsg{
		Type:   "history",
		DS:     msg.DS,
		Name:   msg.Name,
		Points: pts,
	}
	b, _ := json.Marshal(resp)
	select {
	case c.outCh <- b:
	default:
	}
}

func (c *wsClient) sendMeta(ctx context.Context, ref broker.SignalRef) {
	ds, ok := c.broker.Source(ref.DS)
	if !ok {
		return
	}
	meta, err := ds.GetMetadata(ctx, ref.Name)
	if err != nil {
		return
	}

	msg := outMsg{
		Type: "meta",
		DS:   ref.DS,
		Name: ref.Name,
		Meta: metadataToPayload(meta),
	}
	b, _ := json.Marshal(msg)
	select {
	case c.outCh <- b:
	default:
	}
}

func (c *wsClient) sendError(ctx context.Context, code, message string) {
	msg := outMsg{Type: "error", Code: code, Message: message}
	b, _ := json.Marshal(msg)
	select {
	case c.outCh <- b:
	case <-ctx.Done():
	}
}

// ── Helpers ───────────────────────────────────────────────────────────────────

func metadataToPayload(m datasource.Metadata) *metaPayload {
	return &metaPayload{
		Type:        dataTypeName(m.Type),
		Unit:        m.Unit,
		Description: m.Description,
		DisplayLow:  m.DisplayLow,
		DisplayHigh: m.DisplayHigh,
		DriveLow:    m.DriveLow,
		DriveHigh:   m.DriveHigh,
		EnumStrings: m.EnumStrings,
		Writable:    m.Writable,
	}
}

func dataTypeName(t datasource.DataType) string {
	switch t {
	case datasource.TypeFloat64:
		return "float64"
	case datasource.TypeFloat64Array:
		return "float64[]"
	case datasource.TypeString:
		return "string"
	case datasource.TypeInt64:
		return "int64"
	case datasource.TypeBool:
		return "bool"
	case datasource.TypeEnum:
		return "enum"
	default:
		return "unknown"
	}
}
