package main

import (
	"context"
	"encoding/json"
	"net/http"
	"strconv"
	"strings"
	"time"

	"fn-registry/functions/infra"
)

// requireAdmin gates a handler so only users with users.is_admin = 1 can
// reach it. Non-admins get a 403. Anonymous callers get a 401.
func requireAdmin(db *DB, next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		uid, _ := infra.UserIDFromContext(r.Context(), userCtxKey)
		if uid == "" {
			infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "session required"})
			return
		}
		ok, err := db.IsAdmin(uid)
		if err != nil {
			serverError(w, err)
			return
		}
		if !ok {
			infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusForbidden, Code: "forbidden", Message: "admin required"})
			return
		}
		next(w, r)
	}
}

// publicModule strips secrets out of the config before responding. The
// API token is never returned to the client after it has been stored.
func publicModule(m Module) Module {
	clone := m
	if clone.Config != nil {
		cleaned := JSONValue{}
		for k, v := range clone.Config {
			if strings.Contains(strings.ToLower(k), "token") || strings.Contains(strings.ToLower(k), "password") || strings.Contains(strings.ToLower(k), "secret") {
				cleaned[k] = "***"
			} else {
				cleaned[k] = v
			}
		}
		clone.Config = cleaned
	}
	return clone
}

func handleListModules(db *DB) http.HandlerFunc {
	return requireAdmin(db, func(w http.ResponseWriter, r *http.Request) {
		mods, err := db.listModulesAll()
		if err != nil {
			serverError(w, err)
			return
		}
		out := make([]Module, 0, len(mods))
		for _, m := range mods {
			out = append(out, publicModule(m))
		}
		infra.HTTPJSONResponse(w, http.StatusOK, out)
	})
}

type modulePayload struct {
	Name        string    `json:"name"`
	Kind        string    `json:"kind"`
	Enabled     bool      `json:"enabled"`
	EventFilter []string  `json:"event_filter"`
	Config      JSONValue `json:"config"`
}

func handleCreateModule(db *DB) http.HandlerFunc {
	return requireAdmin(db, func(w http.ResponseWriter, r *http.Request) {
		var body modulePayload
		if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil {
			badRequest(w, err.Error())
			return
		}
		if body.Name == "" || body.Kind == "" {
			badRequest(w, "name and kind required")
			return
		}
		m := &Module{
			Name: body.Name, Kind: body.Kind, Enabled: body.Enabled,
			EventFilter: body.EventFilter, Config: body.Config,
		}
		if err := db.saveModule(m); err != nil {
			serverError(w, err)
			return
		}
		infra.HTTPJSONResponse(w, http.StatusCreated, publicModule(*m))
	})
}

func handleUpdateModule(db *DB) http.HandlerFunc {
	return requireAdmin(db, func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		existing, err := db.getModule(id)
		if err != nil {
			notFound(w, "module not found")
			return
		}
		// Partial body: preserve fields the client did not include. We rely
		// on a generic map to detect omitted vs explicit-null because PATCH
		// callers do not always send the full record.
		var raw map[string]json.RawMessage
		if err := infra.HTTPParseBody(r, &raw, maxBodyBytes); err != nil {
			badRequest(w, err.Error())
			return
		}
		decode := func(key string, into interface{}) {
			if v, ok := raw[key]; ok {
				_ = json.Unmarshal(v, into)
			}
		}
		decode("name", &existing.Name)
		decode("kind", &existing.Kind)
		decode("enabled", &existing.Enabled)
		if v, ok := raw["event_filter"]; ok {
			_ = json.Unmarshal(v, &existing.EventFilter)
		}
		if v, ok := raw["config"]; ok {
			var cfg JSONValue
			_ = json.Unmarshal(v, &cfg)
			// Re-inject masked fields the UI left as "***" so a partial
			// edit does not nuke stored secrets.
			merged := JSONValue{}
			for k, val := range existing.Config {
				merged[k] = val
			}
			for k, val := range cfg {
				if s, isStr := val.(string); isStr && s == "***" {
					continue
				}
				merged[k] = val
			}
			existing.Config = merged
		}
		if err := db.saveModule(existing); err != nil {
			serverError(w, err)
			return
		}
		infra.HTTPJSONResponse(w, http.StatusOK, publicModule(*existing))
	})
}

func handleDeleteModule(db *DB) http.HandlerFunc {
	return requireAdmin(db, func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		if err := db.deleteModule(id); err != nil {
			serverError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	})
}

func handleModuleLogs(db *DB) http.HandlerFunc {
	return requireAdmin(db, func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		limit := 100
		if v := r.URL.Query().Get("limit"); v != "" {
			if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 1000 {
				limit = n
			}
		}
		out, err := db.listModuleLogs(id, limit)
		if err != nil {
			serverError(w, err)
			return
		}
		infra.HTTPJSONResponse(w, http.StatusOK, out)
	})
}

// handleTestModule executes the kind-specific test_connection probe with
// the *current stored config* (or with an incoming config payload, for
// pre-save validation). Returns {ok, status, error} regardless of outcome
// so the UI can show a useful message.
func handleTestModule(db *DB, dispatcher *Dispatcher) http.HandlerFunc {
	return requireAdmin(db, func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		var m *Module
		if id == "draft" {
			// Pre-save test path: caller supplies a full module payload.
			var body modulePayload
			if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil {
				badRequest(w, err.Error())
				return
			}
			m = &Module{Kind: body.Kind, Config: body.Config}
		} else {
			got, err := db.getModule(id)
			if err != nil {
				notFound(w, "module not found")
				return
			}
			m = got
		}
		h, ok := dispatcher.handlers[m.Kind]
		if !ok {
			infra.HTTPJSONResponse(w, http.StatusOK, map[string]interface{}{
				"ok": false, "status": 0, "error": "unknown kind: " + m.Kind,
			})
			return
		}
		ctx, cancel := context.WithTimeout(r.Context(), moduleHTTPTimeout)
		defer cancel()
		start := time.Now()
		status, err := h.TestConnection(ctx, *m)
		resp := map[string]interface{}{
			"ok":          err == nil,
			"status":      status,
			"duration_ms": int(time.Since(start).Milliseconds()),
		}
		if err != nil {
			resp["error"] = err.Error()
		}
		infra.HTTPJSONResponse(w, http.StatusOK, resp)
	})
}

// handleCardJiraSync returns the per-card Jira sync state for the indicator
// tooltip. Reads cards.jira_last_* columns + dispatcher inflight map. The
// caller does not need admin: any authenticated user can see the state of
// their cards. Returns 200 + zero-valued state when the card has no link
// yet (so the UI can show the gray indicator without a special case).
func handleCardJiraSync(db *DB, dispatcher *Dispatcher) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		uid, _ := infra.UserIDFromContext(r.Context(), userCtxKey)
		if uid == "" {
			infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "session required"})
			return
		}
		id := r.PathValue("id")
		state, err := db.readCardJiraSync(id)
		if err != nil {
			notFound(w, "card not found")
			return
		}
		state.Inflight = dispatcher.IsInflight(id)
		// Resolve issue URL by reading any enabled jira module's base_url. We
		// pick the first match because the kanban-jira link is conceptually
		// 1:1 — multiple jira modules pointing at different projects would be
		// a misconfiguration.
		if state.JiraKey != "" {
			if mods, err := db.listModulesEnabled(); err == nil {
				for _, m := range mods {
					if m.Kind != "jira" {
						continue
					}
					cfg, perr := parseJiraConfig(m)
					if perr == nil && cfg.BaseURL != "" {
						state.IssueURL = cfg.BaseURL + "/browse/" + state.JiraKey
						break
					}
				}
			}
		}
		infra.HTTPJSONResponse(w, http.StatusOK, state)
	}
}
