package service

import (
	"encoding/json"
	"errors"
	"html/template"
	"net/http"
	"strings"
)

type userContext struct {
	Name   string
	Groups []string
}

type pageData struct {
	State          PageState
	AllowedGroups  []string
	DefaultMessage string
	BootJSON       template.JS
}

// Handler returns the Metis HTTP handler.
func (a *App) Handler() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", a.handleHealth)
	mux.HandleFunc("/metrics", a.handleMetrics)
	mux.HandleFunc("/internal/sentinel/snapshot", a.handleInternalSnapshot)
	mux.HandleFunc("/internal/sentinel/watch", a.handleInternalWatch)
	mux.HandleFunc("/api/state", a.withUIAuth(a.handleState))
	mux.HandleFunc("/api/devices", a.withUIAuth(a.handleDevices))
	mux.HandleFunc("/api/jobs/build", a.withUIAuth(a.handleBuild))
	mux.HandleFunc("/api/jobs/flash", a.withUIAuth(a.handleFlash))
	mux.HandleFunc("/api/jobs/replace", a.withUIAuth(a.handleReplace))
	mux.HandleFunc("/api/sentinel/watch", a.withUIAuth(a.handleWatch))
	mux.HandleFunc("/", a.withUIAuth(a.handleIndex))
	return mux
}

func (a *App) handleHealth(w http.ResponseWriter, _ *http.Request) {
	writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "service": "metis"})
}

func (a *App) handleMetrics(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
	a.metrics.Render(w)
}

func (a *App) handleInternalSnapshot(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	var record SnapshotRecord
	if err := json.NewDecoder(r.Body).Decode(&record); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := a.StoreSnapshot(record); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
}

func (a *App) handleInternalWatch(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	event, err := a.WatchSentinel()
	if err != nil {
		a.metrics.RecordWatch("error")
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	writeJSON(w, http.StatusOK, event)
}

func (a *App) handleState(w http.ResponseWriter, r *http.Request) {
	host := r.URL.Query().Get("host")
	writeJSON(w, http.StatusOK, a.State(host))
}

func (a *App) handleDevices(w http.ResponseWriter, r *http.Request) {
	host := r.URL.Query().Get("host")
	devices, err := a.RefreshDevices(host)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"devices": devices})
}

func (a *App) handleBuild(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	values := requestValues(r)
	node := values["node"]
	job, err := a.Build(node)
	if err != nil {
		http.Error(w, err.Error(), statusForJobError(err))
		return
	}
	writeJSON(w, http.StatusAccepted, job)
}

func (a *App) handleFlash(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	values := requestValues(r)
	node := values["node"]
	host := values["host"]
	device := values["device"]
	job, err := a.Flash(node, host, device)
	if err != nil {
		http.Error(w, err.Error(), statusForJobError(err))
		return
	}
	writeJSON(w, http.StatusAccepted, job)
}

func (a *App) handleReplace(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	values := requestValues(r)
	node := values["node"]
	host := values["host"]
	device := values["device"]
	job, err := a.Replace(node, host, device)
	if err != nil {
		http.Error(w, err.Error(), statusForJobError(err))
		return
	}
	writeJSON(w, http.StatusAccepted, job)
}

func statusForJobError(err error) int {
	var conflict *activeNodeJobError
	if errors.As(err, &conflict) {
		return http.StatusConflict
	}
	return http.StatusBadRequest
}

func (a *App) handleWatch(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	event, err := a.WatchSentinel()
	if err != nil {
		a.metrics.RecordWatch("error")
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	writeJSON(w, http.StatusOK, event)
}

func (a *App) handleIndex(w http.ResponseWriter, r *http.Request) {
	state := a.State(a.settings.DefaultFlashHost)
	payload, _ := json.Marshal(state)
	data := pageData{
		State:         state,
		AllowedGroups: append([]string{}, a.settings.AllowedGroups...),
		BootJSON:      template.JS(payload),
	}
	_ = metisPage.Execute(w, data)
}

func (a *App) withUIAuth(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		user, ok := a.authorize(r)
		if !ok {
			http.Error(w, "forbidden", http.StatusForbidden)
			return
		}
		if user.Name != "" {
			w.Header().Set("X-Metis-User", user.Name)
		}
		next(w, r)
	}
}

func (a *App) authorize(r *http.Request) (userContext, bool) {
	user := firstNonEmptyHeader(r, "X-Auth-Request-User", "X-Forwarded-User", "X-Auth-Request-Email", "X-Forwarded-Email")
	groups := splitHeaderList(firstNonEmptyHeader(r, "X-Auth-Request-Groups", "X-Forwarded-Groups"))
	if len(groups) == 0 {
		return userContext{Name: user, Groups: groups}, false
	}
	for _, group := range groups {
		for _, allowed := range a.settings.AllowedGroups {
			if normalizeGroupValue(group) == normalizeGroupValue(allowed) {
				return userContext{Name: user, Groups: groups}, true
			}
		}
	}
	return userContext{Name: user, Groups: groups}, false
}

func firstNonEmptyHeader(r *http.Request, keys ...string) string {
	for _, key := range keys {
		if value := strings.TrimSpace(r.Header.Get(key)); value != "" {
			return value
		}
	}
	return ""
}

func splitHeaderList(raw string) []string {
	if strings.TrimSpace(raw) == "" {
		return nil
	}
	parts := strings.Split(raw, ",")
	out := make([]string, 0, len(parts))
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part != "" {
			out = append(out, part)
		}
	}
	return out
}

func normalizeGroupValue(raw string) string {
	value := strings.ToLower(strings.TrimSpace(raw))
	return strings.TrimPrefix(value, "/")
}

func requestValues(r *http.Request) map[string]string {
	values := map[string]string{}
	if err := r.ParseForm(); err == nil {
		for key, rawValues := range r.Form {
			for _, raw := range rawValues {
				if value := strings.TrimSpace(raw); value != "" {
					values[key] = value
					break
				}
			}
		}
	}
	var payload map[string]any
	if err := json.NewDecoder(r.Body).Decode(&payload); err == nil {
		for key, raw := range payload {
			if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
				values[key] = strings.TrimSpace(value)
			}
		}
	}
	return values
}

func writeJSON(w http.ResponseWriter, status int, payload any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(payload)
}
