package api

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

	"drivervault/apiserver/internal/config"
	"drivervault/apiserver/internal/pb"
)

// pbProbe is the outcome of testing a PocketBase connection: whether the base
// URL answers its health check and whether the service-account credentials
// authenticate as a superuser.
type pbProbe struct {
	Reachable  bool     `json:"reachable"`
	HTTPStatus int      `json:"httpStatus,omitempty"`
	LatencyMs  *float64 `json:"latencyMs,omitempty"`
	Superuser  bool     `json:"superuser"`
	Detail     string   `json:"detail,omitempty"`
}

// pbConfigView is the PocketBase-connection shape returned to the panel. The
// password itself is never sent back — only whether one is set.
type pbConfigView struct {
	URL             string  `json:"url"`
	AdminEmail      string  `json:"adminEmail"`
	AdminConfigured bool    `json:"adminConfigured"`
	Probe           pbProbe `json:"probe"`
}

// probePB checks a PocketBase base URL's health and, when credentials are given,
// whether they authenticate as a superuser. It uses the short-timeout
// healthClient so a hung PocketBase cannot stall the request.
func probePB(ctx context.Context, url, email, password string) pbProbe {
	h := probe(ctx, url+"/api/health")
	p := pbProbe{Reachable: h.Status == "ok", HTTPStatus: h.HTTPStatus, LatencyMs: h.LatencyMs}
	if h.Error != "" {
		p.Detail = h.Error
	}
	if email != "" && password != "" {
		st, err := pb.SuperuserAuth(ctx, healthClient, url, email, password)
		if err == nil {
			p.Superuser = true
		} else if p.Reachable {
			p.Detail = "superuser auth failed"
			if st > 0 {
				p.Detail += " (HTTP " + strconv.Itoa(st) + ")"
			}
		}
	}
	return p
}

// normalizeURL trims, defaults the scheme to http, and drops a trailing slash.
func normalizeURL(u string) string {
	u = strings.TrimSpace(u)
	if u == "" {
		return ""
	}
	if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
		u = "http://" + u
	}
	return strings.TrimRight(u, "/")
}

// viewFor builds the panel's connection view, including a live probe.
func viewFor(ctx context.Context, url, email, password string) pbConfigView {
	return pbConfigView{
		URL:             url,
		AdminEmail:      email,
		AdminConfigured: email != "" && password != "",
		Probe:           probePB(ctx, url, email, password),
	}
}

// GET /api/admin/pb-config — current PocketBase connection + a live probe.
func (s *Server) handleGetPBConfig(w http.ResponseWriter, r *http.Request) {
	url, email, password := s.pbSettings()
	writeJSON(w, http.StatusOK, viewFor(r.Context(), url, email, password))
}

// pbConfigBody is the editable connection payload. A blank adminPassword means
// "keep the current one"; a blank adminEmail/url means "keep current".
type pbConfigBody struct {
	URL           string `json:"url"`
	AdminEmail    string `json:"adminEmail"`
	AdminPassword string `json:"adminPassword"`
}

// resolve merges a request body onto the current settings, applying the
// keep-current semantics for blank fields.
func (s *Server) resolve(b pbConfigBody) (url, email, password string) {
	curURL, curEmail, curPassword := s.pbSettings()
	url = normalizeURL(b.URL)
	if url == "" {
		url = curURL
	}
	email = strings.TrimSpace(b.AdminEmail)
	if email == "" {
		email = curEmail
	}
	password = b.AdminPassword
	if password == "" {
		password = curPassword
	}
	return
}

// POST /api/admin/pb-config/test — probe a candidate connection WITHOUT applying
// it, so a superadmin can verify before saving.
func (s *Server) handleTestPBConfig(w http.ResponseWriter, r *http.Request) {
	var b pbConfigBody
	if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json")
		return
	}
	url, email, password := s.resolve(b)
	writeJSON(w, http.StatusOK, probePB(r.Context(), url, email, password))
}

// PUT /api/admin/pb-config — apply a new PocketBase connection at runtime and
// persist it to .env. Returns the new config plus a fresh probe.
func (s *Server) handleUpdatePBConfig(w http.ResponseWriter, r *http.Request) {
	var b pbConfigBody
	if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json")
		return
	}
	if normalizeURL(b.URL) == "" {
		writeError(w, http.StatusBadRequest, "a PocketBase URL is required")
		return
	}
	url, email, password := s.resolve(b)

	// Apply at runtime, then persist so the change survives a restart.
	s.setPBConfig(url, email, password)
	if err := config.UpdateEnvFile(config.EnvFile, map[string]string{
		"POCKETBASE_URL":            url,
		"POCKETBASE_ADMIN_EMAIL":    email,
		"POCKETBASE_ADMIN_PASSWORD": password,
	}); err != nil {
		// The runtime change already took effect; report that persistence failed.
		log.Printf("pb-config: persist to %s failed: %v", config.EnvFile, err)
		writeJSON(w, http.StatusOK, map[string]any{
			"config":  viewFor(r.Context(), url, email, password),
			"warning": "applied for this session, but could not be saved to .env: " + err.Error(),
		})
		return
	}

	log.Printf("pb-config: PocketBase connection updated to %s (by superadmin)", url)
	writeJSON(w, http.StatusOK, map[string]any{
		"config": viewFor(r.Context(), url, email, password),
	})
}

// webAppConfigView is the Web App shape returned to the panel: where the Web App
// lives (the address /api/status probes) and which browser origins CORS lets
// call this server.
type webAppConfigView struct {
	URL          string    `json:"url"`
	AllowOrigins []string  `json:"allowOrigins"`
	Probe        svcHealth `json:"probe"`
}

// webAppViewFor builds the panel's Web App view, including a live probe.
func webAppViewFor(ctx context.Context, url string, origins []string) webAppConfigView {
	return webAppConfigView{
		URL:          url,
		AllowOrigins: origins,
		Probe:        probe(ctx, url+"/healthz"),
	}
}

// GET /api/admin/webapp-config — current Web App settings + a live probe.
func (s *Server) handleGetWebAppConfig(w http.ResponseWriter, r *http.Request) {
	url, origins := s.webAppSettings()
	writeJSON(w, http.StatusOK, webAppViewFor(r.Context(), url, origins))
}

// webAppConfigBody is the editable Web App payload. A blank url means "keep the
// current one"; allowOrigins is taken as sent (it is a complete list, not a
// patch), so it may only be omitted, never emptied.
type webAppConfigBody struct {
	URL          string   `json:"url"`
	AllowOrigins []string `json:"allowOrigins"`
}

// POST /api/admin/webapp-config/test — probe a candidate Web App address WITHOUT
// applying it. CORS origins are not probeable, so this only checks the URL.
func (s *Server) handleTestWebAppConfig(w http.ResponseWriter, r *http.Request) {
	var b webAppConfigBody
	if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json")
		return
	}
	url := normalizeURL(b.URL)
	if url == "" {
		url, _ = s.webAppSettings()
	}
	writeJSON(w, http.StatusOK, probe(r.Context(), url+"/healthz"))
}

// PUT /api/admin/webapp-config — apply new Web App settings at runtime and
// persist them to .env. The CORS middleware re-reads the origin list on every
// request, so a change here takes effect without a restart.
func (s *Server) handleUpdateWebAppConfig(w http.ResponseWriter, r *http.Request) {
	var b webAppConfigBody
	if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json")
		return
	}
	url := normalizeURL(b.URL)
	if url == "" {
		writeError(w, http.StatusBadRequest, "a Web App URL is required")
		return
	}
	origins := cleanOrigins(b.AllowOrigins)
	if len(origins) == 0 {
		writeError(w, http.StatusBadRequest, "at least one allowed origin is required (use * for any)")
		return
	}

	// Apply at runtime, then persist so the change survives a restart.
	s.setWebAppConfig(url, origins)
	if err := config.UpdateEnvFile(config.EnvFile, map[string]string{
		"WEBAPP_URL":         url,
		"CORS_ALLOW_ORIGINS": strings.Join(origins, ","),
	}); err != nil {
		// The runtime change already took effect; report that persistence failed.
		log.Printf("webapp-config: persist to %s failed: %v", config.EnvFile, err)
		writeJSON(w, http.StatusOK, map[string]any{
			"config":  webAppViewFor(r.Context(), url, origins),
			"warning": "applied for this session, but could not be saved to .env: " + err.Error(),
		})
		return
	}

	log.Printf("webapp-config: Web App updated to %s, origins %s (by superadmin)", url, strings.Join(origins, ","))
	writeJSON(w, http.StatusOK, map[string]any{
		"config": webAppViewFor(r.Context(), url, origins),
	})
}

// maxServerNameLen bounds the display name. It is shown in client server
// pickers, so it has to stay short enough to render in one; the limit is on
// runes rather than bytes so a non-Latin name is not cut off early.
const maxServerNameLen = 64

// serverConfigView is this API Server's own identity as returned to the panel.
type serverConfigView struct {
	Name string `json:"name"`
}

// GET /api/admin/server-config — this server's display name.
func (s *Server) handleGetServerConfig(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, serverConfigView{Name: s.serverName()})
}

// serverConfigBody is the editable API Server payload.
type serverConfigBody struct {
	Name string `json:"name"`
}

// PUT /api/admin/server-config — rename this server at runtime and persist it to
// .env. /api/health reads the name per request, so the new one is live at once.
func (s *Server) handleUpdateServerConfig(w http.ResponseWriter, r *http.Request) {
	var b serverConfigBody
	if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json")
		return
	}
	name := strings.TrimSpace(b.Name)
	if name == "" {
		writeError(w, http.StatusBadRequest, "a server name is required")
		return
	}
	if len([]rune(name)) > maxServerNameLen {
		writeError(w, http.StatusBadRequest, "server name is too long (max "+strconv.Itoa(maxServerNameLen)+" characters)")
		return
	}

	// Apply at runtime, then persist so the change survives a restart.
	s.setServerName(name)
	if err := config.UpdateEnvFile(config.EnvFile, map[string]string{"SERVER_NAME": name}); err != nil {
		// The runtime change already took effect; report that persistence failed.
		log.Printf("server-config: persist to %s failed: %v", config.EnvFile, err)
		writeJSON(w, http.StatusOK, map[string]any{
			"config":  serverConfigView{Name: name},
			"warning": "applied for this session, but could not be saved to .env: " + err.Error(),
		})
		return
	}

	log.Printf("server-config: server renamed to %s (by superadmin)", name)
	writeJSON(w, http.StatusOK, map[string]any{
		"config": serverConfigView{Name: name},
	})
}

// cleanOrigins trims each origin and drops the blanks, so a trailing comma or a
// stray space in the panel's text field can't register an unmatchable origin.
func cleanOrigins(in []string) []string {
	out := make([]string, 0, len(in))
	for _, o := range in {
		if o = strings.TrimSpace(o); o != "" {
			out = append(out, o)
		}
	}
	return out
}
