package api

import (
	"context"
	"encoding/json"
	"net/http"
	"strings"
)

// Role names as stored in the PocketBase users.role select field. A missing or
// empty value is treated as roleUser.
const (
	roleUser       = "user"
	roleAdmin      = "admin"
	roleSuperadmin = "superadmin"
)

// publicPaths bypass authentication. Everything else under /api/ requires a
// valid PocketBase token.
var publicPaths = map[string]bool{
	"/api/health":        true,
	"/api/status":        true,
	"/api/auth/login":    true,
	"/api/auth/validate": true,
	"/healthz":           true,
}

type ctxKey int

const ctxCaller ctxKey = iota

// callerIdentity is who the request token belongs to. It is resolved from
// PocketBase on each request, so a role change or a deletion takes effect
// immediately rather than lingering until a token expires.
type callerIdentity struct {
	ID    string
	Email string
	Name  string
	Role  string
	OrgID string // organization record id ("" when the user belongs to no org)
}

func (c *callerIdentity) isSuperadmin() bool { return c != nil && c.Role == roleSuperadmin }
func (c *callerIdentity) isManager() bool {
	return c != nil && (c.Role == roleAdmin || c.Role == roleSuperadmin)
}

// caller returns the identity stashed on the request context by withAuth.
func caller(r *http.Request) *callerIdentity {
	if v, ok := r.Context().Value(ctxCaller).(*callerIdentity); ok {
		return v
	}
	return nil
}

// currentUserID returns the authenticated user's id. Handlers on non-public
// paths can treat "" as "not authenticated" — withAuth already rejected those.
func (s *Server) currentUserID(r *http.Request) string {
	if who := caller(r); who != nil {
		return who.ID
	}
	return ""
}

// bearerToken extracts the caller's token, accepting both "Bearer <token>" and
// a raw token (PocketBase's own SDKs send the latter).
func bearerToken(r *http.Request) string {
	h := r.Header.Get("Authorization")
	if h == "" {
		return ""
	}
	if after, ok := strings.CutPrefix(h, "Bearer "); ok {
		return strings.TrimSpace(after)
	}
	return strings.TrimSpace(h)
}

// identify resolves the caller's id/email/name/role/org from their PocketBase
// token by asking PocketBase to refresh it. A non-200 status means the token is
// invalid or expired.
func (s *Server) identify(ctx context.Context, token string) (*callerIdentity, int, error) {
	raw, status, err := s.pb.AuthRefresh(ctx, s.usersCollection(), token)
	if err != nil {
		return nil, 0, err
	}
	if status != http.StatusOK {
		return nil, status, nil
	}

	var out struct {
		Record struct {
			ID           string `json:"id"`
			Email        string `json:"email"`
			Name         string `json:"name"`
			Role         string `json:"role"`
			Organization string `json:"organization"`
		} `json:"record"`
	}
	if err := json.Unmarshal(raw, &out); err != nil {
		return nil, status, err
	}
	role := out.Record.Role
	if role == "" {
		role = roleUser
	}
	return &callerIdentity{
		ID:    out.Record.ID,
		Email: out.Record.Email,
		Name:  out.Record.Name,
		Role:  role,
		OrgID: out.Record.Organization,
	}, http.StatusOK, nil
}

// withAuth rejects requests to non-public /api/ paths that lack a valid
// PocketBase token, and stashes the resolved identity on the request context.
func (s *Server) withAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Public API paths, plus everything outside /api/ (the embedded web
		// panel and its static assets), bypass authentication.
		if publicPaths[r.URL.Path] || !strings.HasPrefix(r.URL.Path, "/api/") {
			next.ServeHTTP(w, r)
			return
		}
		who, ok := s.authenticate(w, r)
		if !ok {
			return
		}
		next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxCaller, who)))
	})
}

// authenticate resolves and validates the caller, writing the error response
// itself and returning ok=false when the request should not proceed.
func (s *Server) authenticate(w http.ResponseWriter, r *http.Request) (*callerIdentity, bool) {
	token := bearerToken(r)
	if token == "" {
		writeError(w, http.StatusUnauthorized, "missing bearer token")
		return nil, false
	}
	who, status, err := s.identify(r.Context(), token)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach PocketBase", "detail": err.Error()})
		return nil, false
	}
	if status != http.StatusOK || who == nil {
		writeError(w, http.StatusUnauthorized, "invalid or expired token")
		return nil, false
	}
	return who, true
}

// requireRole is the shared gate for privileged handlers: it needs the service
// account (every privileged flow runs through it) and a caller satisfying ok.
// withAuth has already established the identity.
func (s *Server) requireRole(next http.HandlerFunc, ok func(*callerIdentity) bool, denied string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !s.pb.Configured() {
			writeError(w, http.StatusServiceUnavailable, "user management not configured on the server")
			return
		}
		if !ok(caller(r)) {
			writeError(w, http.StatusForbidden, denied)
			return
		}
		next(w, r)
	}
}

// requireManager wraps a handler so only managers (admin or superadmin) proceed.
func (s *Server) requireManager(next http.HandlerFunc) http.HandlerFunc {
	return s.requireRole(next, func(c *callerIdentity) bool { return c.isManager() }, "admin role required")
}

// requireSuperadmin wraps a handler so only superadmins proceed.
func (s *Server) requireSuperadmin(next http.HandlerFunc) http.HandlerFunc {
	return s.requireRole(next, func(c *callerIdentity) bool { return c.isSuperadmin() }, "superadmin role required")
}

// requireSuperadminAuth gates a handler on a superadmin caller WITHOUT requiring
// the service account to already be configured. Used by the PocketBase settings
// endpoints, whose whole purpose is to configure that service account.
func (s *Server) requireSuperadminAuth(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !caller(r).isSuperadmin() {
			writeError(w, http.StatusForbidden, "superadmin role required")
			return
		}
		next(w, r)
	}
}

// POST /api/auth/login
// Body: {"email"|"identity":"...","password":"..."}
// Proxies to the PocketBase users auth-with-password and relays its response —
// the client gets PocketBase's own token and user record. PocketBase's address
// lives only in this server and is never exposed to clients.
func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
	var body struct {
		Email    string `json:"email"`
		Identity string `json:"identity"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json")
		return
	}
	identity := body.Identity
	if identity == "" {
		identity = body.Email
	}
	if identity == "" || body.Password == "" {
		writeError(w, http.StatusBadRequest, "email and password are required")
		return
	}

	raw, status, err := s.pb.LoginWithPassword(r.Context(), s.usersCollection(), identity, body.Password)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach PocketBase", "detail": err.Error()})
		return
	}
	relay(w, status, raw)
}

// GET /api/auth/validate  (Authorization: <pb token>)
// Proxies to PocketBase auth-refresh to confirm a token is still valid.
func (s *Server) handleAuthValidate(w http.ResponseWriter, r *http.Request) {
	token := bearerToken(r)
	if token == "" {
		writeJSON(w, http.StatusUnauthorized, map[string]any{"valid": false})
		return
	}
	raw, status, err := s.pb.AuthRefresh(r.Context(), s.usersCollection(), token)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]any{"valid": false, "detail": err.Error()})
		return
	}
	relay(w, status, raw)
}

// GET /api/auth/me — the caller's identity, from their token.
func (s *Server) handleAuthMe(w http.ResponseWriter, r *http.Request) {
	who := caller(r)
	if who == nil {
		writeError(w, http.StatusUnauthorized, "not authenticated")
		return
	}
	writeJSON(w, http.StatusOK, userInfo{ID: who.ID, Email: who.Email, Name: who.Name, Role: who.Role})
}

// GET /api/identity — like /api/auth/me, plus the caller's organization. The
// panel uses this to decide which management cards to show.
func (s *Server) handleIdentity(w http.ResponseWriter, r *http.Request) {
	who := caller(r)
	if who == nil {
		writeError(w, http.StatusUnauthorized, "not authenticated")
		return
	}
	orgName := ""
	if who.OrgID != "" {
		orgName = s.orgName(r.Context(), who.OrgID)
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"id":               who.ID,
		"email":            who.Email,
		"name":             who.Name,
		"role":             who.Role,
		"organization":     who.OrgID,
		"organizationName": orgName,
	})
}

// userInfo is the compact identity shape returned by /api/auth/me.
type userInfo struct {
	ID    string `json:"id"`
	Email string `json:"email"`
	Name  string `json:"name"`
	Role  string `json:"role,omitempty"`
}

// relay copies an upstream PocketBase status + JSON body to the client.
func relay(w http.ResponseWriter, status int, body []byte) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_, _ = w.Write(body)
}
