// Package api implements the REST API handlers for uopi.
package api

import (
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"net/http"
	"strings"

	"github.com/uopi/uopi/internal/broker"
	"github.com/uopi/uopi/internal/datasource"
	"github.com/uopi/uopi/internal/datasource/synthetic"
	"github.com/uopi/uopi/internal/storage"
)

// Handler holds the dependencies shared across all API endpoints.
type Handler struct {
	broker           *broker.Broker
	synthetic        *synthetic.Synthetic // nil if not enabled
	store            *storage.Store
	channelFinderURL string // empty if not configured
	log              *slog.Logger
}

// New creates an API Handler. synth may be nil if the synthetic DS is disabled.
func New(b *broker.Broker, synth *synthetic.Synthetic, store *storage.Store, channelFinderURL string, log *slog.Logger) *Handler {
	return &Handler{broker: b, synthetic: synth, store: store, channelFinderURL: channelFinderURL, log: log}
}

// Register mounts all API routes onto mux under the given prefix.
// Requires Go 1.22+ for method+path routing.
func (h *Handler) Register(mux *http.ServeMux, prefix string) {
	mux.HandleFunc("GET "+prefix+"/datasources", h.listDataSources)
	mux.HandleFunc("GET "+prefix+"/signals", h.listSignals)
	mux.HandleFunc("GET "+prefix+"/signals/search", h.searchSignals)
	mux.HandleFunc("GET "+prefix+"/channel-finder", h.channelFinder)
	mux.HandleFunc("GET "+prefix+"/interfaces", h.listInterfaces)
	mux.HandleFunc("POST "+prefix+"/interfaces", h.createInterface)
	mux.HandleFunc("GET "+prefix+"/interfaces/{id}", h.getInterface)
	mux.HandleFunc("PUT "+prefix+"/interfaces/{id}", h.updateInterface)
	mux.HandleFunc("DELETE "+prefix+"/interfaces/{id}", h.deleteInterface)
	mux.HandleFunc("POST "+prefix+"/interfaces/{id}/clone", h.cloneInterface)
	// Synthetic signal CRUD
	mux.HandleFunc("GET "+prefix+"/synthetic", h.listSynthetic)
	mux.HandleFunc("POST "+prefix+"/synthetic", h.createSynthetic)
	mux.HandleFunc("DELETE "+prefix+"/synthetic/{name}", h.deleteSynthetic)
}

// ── /datasources ──────────────────────────────────────────────────────────────

type dsInfo struct {
	Name   string `json:"name"`
	Status string `json:"status"`
}

func (h *Handler) listDataSources(w http.ResponseWriter, r *http.Request) {
	sources := h.broker.DataSources()
	out := make([]dsInfo, len(sources))
	for i, ds := range sources {
		out[i] = dsInfo{Name: ds.Name(), Status: "connected"}
	}
	jsonOK(w, out)
}

// ── /signals ──────────────────────────────────────────────────────────────────

type signalInfo struct {
	Name        string   `json:"name"`
	Type        string   `json:"type"`
	Unit        string   `json:"unit,omitempty"`
	Description string   `json:"description,omitempty"`
	DisplayLow  float64  `json:"displayLow"`
	DisplayHigh float64  `json:"displayHigh"`
	Writable    bool     `json:"writable"`
	EnumStrings []string `json:"enumStrings,omitempty"`
}

func (h *Handler) listSignals(w http.ResponseWriter, r *http.Request) {
	dsName := r.URL.Query().Get("ds")
	if dsName == "" {
		jsonError(w, http.StatusBadRequest, "query parameter 'ds' is required")
		return
	}

	ds, ok := h.broker.Source(dsName)
	if !ok {
		jsonError(w, http.StatusNotFound, "unknown data source: "+dsName)
		return
	}

	metas, err := ds.ListSignals(r.Context())
	if err != nil {
		h.log.Error("list signals", "ds", dsName, "err", err)
		jsonError(w, http.StatusInternalServerError, err.Error())
		return
	}

	out := make([]signalInfo, len(metas))
	for i, m := range metas {
		out[i] = metaToSignalInfo(m)
	}
	jsonOK(w, out)
}

// ── /signals/search ───────────────────────────────────────────────────────────

func (h *Handler) searchSignals(w http.ResponseWriter, r *http.Request) {
	q := strings.ToLower(r.URL.Query().Get("q"))
	dsName := r.URL.Query().Get("ds")

	var sources []datasource.DataSource
	if dsName != "" {
		ds, ok := h.broker.Source(dsName)
		if !ok {
			jsonError(w, http.StatusNotFound, "unknown data source: "+dsName)
			return
		}
		sources = []datasource.DataSource{ds}
	} else {
		sources = h.broker.DataSources()
	}

	var out []signalInfo
	for _, ds := range sources {
		metas, err := ds.ListSignals(r.Context())
		if err != nil {
			continue
		}
		for _, m := range metas {
			if q == "" || strings.Contains(strings.ToLower(m.Name), q) || strings.Contains(strings.ToLower(m.Description), q) {
				si := metaToSignalInfo(m)
				out = append(out, si)
			}
		}
	}
	if out == nil {
		out = []signalInfo{}
	}
	jsonOK(w, out)
}

// ── /channel-finder ───────────────────────────────────────────────────────────

// channelFinder proxies a query to the EPICS Channel Finder service when one
// is configured, returning a flat list of PV names. Returns 501 otherwise.
func (h *Handler) channelFinder(w http.ResponseWriter, r *http.Request) {
	if h.channelFinderURL == "" {
		jsonError(w, http.StatusNotImplemented, "Channel Finder not configured (set channel_finder_url in config)")
		return
	}

	q := r.URL.Query().Get("q")
	cfURL := strings.TrimRight(h.channelFinderURL, "/") + "/resources/channels"
	if q != "" {
		cfURL += "?~name=*" + q + "*"
	}

	resp, err := http.Get(cfURL) //nolint:noctx
	if err != nil {
		jsonError(w, http.StatusBadGateway, "Channel Finder request failed: "+err.Error())
		return
	}
	defer resp.Body.Close()

	// Channel Finder returns JSON array of channel objects; extract names.
	var channels []struct {
		Name string `json:"name"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&channels); err != nil {
		jsonError(w, http.StatusBadGateway, "Channel Finder response parse error: "+err.Error())
		return
	}

	names := make([]string, 0, len(channels))
	for _, ch := range channels {
		if ch.Name != "" {
			names = append(names, ch.Name)
		}
	}
	jsonOK(w, names)
}

// ── /interfaces ───────────────────────────────────────────────────────────────

func (h *Handler) listInterfaces(w http.ResponseWriter, r *http.Request) {
	list, err := h.store.List()
	if err != nil {
		h.log.Error("list interfaces", "err", err)
		jsonError(w, http.StatusInternalServerError, err.Error())
		return
	}
	jsonOK(w, list)
}

func (h *Handler) createInterface(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
	if err != nil {
		jsonError(w, http.StatusBadRequest, "read body: "+err.Error())
		return
	}
	id, err := h.store.Create(body)
	if err != nil {
		h.log.Error("create interface", "err", err)
		jsonError(w, http.StatusBadRequest, err.Error())
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(map[string]string{"id": id})
}

func (h *Handler) getInterface(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	data, err := h.store.Get(id)
	if err != nil {
		if errors.Is(err, storage.ErrNotFound) {
			jsonError(w, http.StatusNotFound, "interface not found: "+id)
		} else {
			jsonError(w, http.StatusInternalServerError, err.Error())
		}
		return
	}
	w.Header().Set("Content-Type", "application/xml")
	_, _ = w.Write(data)
}

func (h *Handler) updateInterface(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
	if err != nil {
		jsonError(w, http.StatusBadRequest, "read body: "+err.Error())
		return
	}
	if err := h.store.Update(id, body); err != nil {
		if errors.Is(err, storage.ErrNotFound) {
			jsonError(w, http.StatusNotFound, "interface not found: "+id)
		} else {
			h.log.Error("update interface", "id", id, "err", err)
			jsonError(w, http.StatusInternalServerError, err.Error())
		}
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) deleteInterface(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	if err := h.store.Delete(id); err != nil {
		if errors.Is(err, storage.ErrNotFound) {
			jsonError(w, http.StatusNotFound, "interface not found: "+id)
		} else {
			jsonError(w, http.StatusInternalServerError, err.Error())
		}
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) cloneInterface(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	newID, err := h.store.Clone(id)
	if err != nil {
		if errors.Is(err, storage.ErrNotFound) {
			jsonError(w, http.StatusNotFound, "interface not found: "+id)
		} else {
			h.log.Error("clone interface", "id", id, "err", err)
			jsonError(w, http.StatusInternalServerError, err.Error())
		}
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(map[string]string{"id": newID})
}

// ── /synthetic ────────────────────────────────────────────────────────────────

func (h *Handler) listSynthetic(w http.ResponseWriter, _ *http.Request) {
	if h.synthetic == nil {
		jsonOK(w, []any{})
		return
	}
	jsonOK(w, h.synthetic.GetDefs())
}

func (h *Handler) createSynthetic(w http.ResponseWriter, r *http.Request) {
	if h.synthetic == nil {
		jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
		return
	}
	var def synthetic.SignalDef
	if err := json.NewDecoder(r.Body).Decode(&def); err != nil {
		jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}
	if err := h.synthetic.AddSignal(def); err != nil {
		if errors.Is(err, datasource.ErrNotFound) {
			jsonError(w, http.StatusConflict, err.Error())
		} else {
			jsonError(w, http.StatusBadRequest, err.Error())
		}
		return
	}
	w.WriteHeader(http.StatusCreated)
	jsonOK(w, def)
}

func (h *Handler) deleteSynthetic(w http.ResponseWriter, r *http.Request) {
	if h.synthetic == nil {
		jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
		return
	}
	name := r.PathValue("name")
	if err := h.synthetic.RemoveSignal(name); err != nil {
		if errors.Is(err, datasource.ErrNotFound) {
			jsonError(w, http.StatusNotFound, "synthetic signal not found: "+name)
		} else {
			jsonError(w, http.StatusInternalServerError, err.Error())
		}
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

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

func metaToSignalInfo(m datasource.Metadata) signalInfo {
	return signalInfo{
		Name:        m.Name,
		Type:        dataTypeName(m.Type),
		Unit:        m.Unit,
		Description: m.Description,
		DisplayLow:  m.DisplayLow,
		DisplayHigh: m.DisplayHigh,
		Writable:    m.Writable,
		EnumStrings: m.EnumStrings,
	}
}

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"
	}
}

func jsonOK(w http.ResponseWriter, v any) {
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(v)
}

func jsonError(w http.ResponseWriter, code int, message string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}
