// Package api exposes the HTTP REST surface of the DriverVault API Server.
//
// Clients (web app, phone app, Home Assistant plugin, ESP32 device) talk only to
// this server; this server is the only thing that talks to PocketBase. It also
// serves the superadmin web panel at the root.
//
// Authentication is PocketBase's own: /api/auth/login is proxied to the
// PocketBase users collection and the client keeps the token PocketBase minted.
// Every protected request re-resolves that token against PocketBase, so a role
// change or a deletion takes effect immediately.
//
//	# public
//	GET    /healthz
//	GET    /api/health
//	GET    /api/status
//	POST   /api/auth/login
//	GET    /api/auth/validate
//
//	# identity
//	GET    /api/auth/me
//	GET    /api/identity
//
//	# current user
//	GET    /api/me                     PATCH  /api/me                DELETE /api/me
//	POST   /api/me/password
//	POST   /api/me/avatar              GET    /api/me/avatar         DELETE /api/me/avatar
//	POST   /api/me/verify/request
//	GET    /api/me/export              POST   /api/me/import
//	POST   /api/me/delete              POST   /api/me/delete/cancel
//
//	# users + organizations (manager; POST /api/orgs is open to any user)
//	GET    /api/users                  POST   /api/users
//	PATCH  /api/users/{id}             DELETE /api/users/{id}
//	GET    /api/orgs                   POST   /api/orgs
//	PATCH  /api/orgs/{id}              DELETE /api/orgs/{id}
//
//	# superadmin
//	GET    /api/admin/pb-config        PUT    /api/admin/pb-config
//	POST   /api/admin/pb-config/test
//	GET    /api/admin/webapp-config    PUT    /api/admin/webapp-config
//	POST   /api/admin/webapp-config/test
//	GET    /api/admin/server-config    PUT    /api/admin/server-config
//	GET    /api/admin/plugins          POST   /api/admin/plugins
//	GET    /api/admin/plugins/{name}   PUT    /api/admin/plugins/{name}
//	DELETE /api/admin/plugins/{name}   POST   /api/admin/plugins/{name}/health
//
//	# integrations (per-user plugin settings; superadmin → org admin → user cascade)
//	GET    /api/integrations/toyota    PUT    /api/integrations/toyota
//	POST   /api/integrations/toyota/health
//	GET    /api/integrations/toyota/vehicles
//	GET    /api/integrations/anker-solix    PUT    /api/integrations/anker-solix
//	POST   /api/integrations/anker-solix/health
//	GET    /api/integrations/anker-solix/chargers
//	GET    /api/integrations/anker-solix/chargers/{sn}/details
//	POST   /api/integrations/anker-solix/chargers/{sn}/rfid-cards
//	POST   /api/integrations/anker-solix/chargers/{sn}/rfid-cards/scan
//	DELETE /api/integrations/anker-solix/chargers/{sn}/rfid-cards/{number}
//	GET    /api/integrations/anker-solix/chargers/{sn}/rfid-cards/charger
//	GET    /api/integrations/greencell      PUT    /api/integrations/greencell
//	POST   /api/integrations/greencell/health
//	GET    /api/integrations/greencell/chargers
//	GET    /api/integrations/greencell/chargers/{sn}/state
//
//	# vehicle providers (create a car from a manufacturer service; per-car tab)
//	GET    /api/vehicle-providers
//	GET    /api/vehicle-providers/{provider}/vehicles
//	POST   /api/vehicle-providers/{provider}/import
//
//	# cars, service records, parts, shares
//	GET    /api/cars                   POST   /api/cars
//	GET    /api/cars/{id}              PATCH  /api/cars/{id}         DELETE /api/cars/{id}
//	PUT    /api/cars/{id}/view
//	GET    /api/cars/{id}/provider     POST   /api/cars/{id}/provider
//	POST   /api/cars/{id}/provider/sync
//	GET    /api/cars/{id}/service-records
//	GET    /api/cars/{id}/parts
//	GET    /api/cars/{id}/shares       POST   /api/cars/{id}/shares
//	DELETE /api/cars/{id}/shares/{userId}
//	GET    /api/service-records        POST   /api/service-records
//	GET    /api/service-records/{id}   PATCH  /api/service-records/{id}
//	DELETE /api/service-records/{id}
//	GET    /api/parts                  POST   /api/parts
//	GET    /api/parts/{id}             PATCH  /api/parts/{id}        DELETE /api/parts/{id}
//
//	# technical checks (roadworthiness inspections; time-only, next date derived)
//	GET    /api/cars/{id}/technical-checks
//	GET    /api/technical-checks       POST   /api/technical-checks
//	GET    /api/technical-checks/{id}  PATCH  /api/technical-checks/{id}
//	DELETE /api/technical-checks/{id}
//
//	# fuel tracking (efficiency is derived on read, never stored)
//	GET    /api/cars/{id}/fuel-entries
//	GET    /api/cars/{id}/fuel-stats
//	GET    /api/fuel-entries           POST   /api/fuel-entries
//	GET    /api/fuel-entries/{id}      PATCH  /api/fuel-entries/{id}
//	DELETE /api/fuel-entries/{id}
//
//	# charging tracking (EV counterpart of fuel; efficiency derived on read)
//	GET    /api/cars/{id}/charging-sessions
//	GET    /api/cars/{id}/charging-stats
//	GET    /api/charging-sessions      POST   /api/charging-sessions
//	GET    /api/charging-sessions/{id} PATCH  /api/charging-sessions/{id}
//	DELETE /api/charging-sessions/{id}
//
//	# maintenance log (workshop visits + repairs; distinct from service records)
//	GET    /api/cars/{id}/maintenance
//	GET    /api/maintenance            POST   /api/maintenance
//	GET    /api/maintenance/{id}       PATCH  /api/maintenance/{id}
//	DELETE /api/maintenance/{id}
//
//	# document tracking (insurance, pollution certs, … + renewal dates)
//	GET    /api/cars/{id}/documents
//	GET    /api/car-documents          POST   /api/car-documents
//	GET    /api/car-documents/{id}     PATCH  /api/car-documents/{id}
//	DELETE /api/car-documents/{id}
//
//	# attachments — one optional file per record, same three verbs everywhere.
//	# {records} is car-documents | service-records | technical-checks | maintenance
//	#            | fuel-entries | charging-sessions | parts
//	POST   /api/{records}/{id}/file
//	GET    /api/{records}/{id}/file
//	DELETE /api/{records}/{id}/file
//
//	# reminders (stored + auto-derived from documents and service records)
//	GET    /api/cars/{id}/reminders
//	GET    /api/reminders              POST   /api/reminders
//	GET    /api/reminders/{id}         PATCH  /api/reminders/{id}
//	DELETE /api/reminders/{id}         POST   /api/reminders/{id}/complete
package api

import (
	"bufio"
	"context"
	"log"
	"net"
	"net/http"
	"sync"
	"time"

	"drivervault/apiserver/internal/bootstrap"
	"drivervault/apiserver/internal/config"
	"drivervault/apiserver/internal/ocpp"
	"drivervault/apiserver/internal/pb"
	"drivervault/apiserver/internal/plugins"
	_ "drivervault/apiserver/internal/plugins/builtin" // register built-in plugins
)

// PocketBase collection names.
const (
	colCars            = "cars"
	colServices        = "service_records"
	colTechnicalChecks = "technical_checks"
	colParts           = "parts"
	colShares          = "car_shares"
	colOrgs            = "organizations"
	colFuel            = "fuel_entries"
	colCharging        = "charging_sessions"
	colMaintenance     = "maintenance_entries"
	colDocuments       = "car_documents"
	colReminders       = "reminders"
	colHomeChargers    = "home_chargers"
	colControlAudit    = "control_audit"
	colAppSettings     = "app_settings"
)

// Server wires together the HTTP handlers and their dependencies.
type Server struct {
	mu      sync.RWMutex // guards the mutable PocketBase connection in cfg
	cfg     config.Config
	pb      *pb.Client
	plugins *plugins.Manager

	// pluginStore is the Manager's backing store; pluginsStop ends the
	// background load retry started by StartPlugins.
	pluginStore plugins.Store
	pluginsStop context.CancelFunc

	// ocpp is the OCPP 1.6J Central System that Anker Solix chargers connect to
	// when their owner picks a control mode of own/proxy (see internal/ocpp and
	// integrations_ankersolix_control.go). Nil-safe: control endpoints report a
	// clear error when a charger is not connected.
	ocpp    *ocpp.CSMS
	control *controlIndex // token -> owning user/charger for the /ocpp endpoint
	ctlRL   *rateLimiter  // per user+charger control-command rate limit
}

// New constructs a Server around an already-built PocketBase client.
func New(cfg config.Config, client *pb.Client) *Server {
	// The global (L1) plugin layer lives in PocketBase alongside the org (L2)
	// and user (L3) layers, rather than in a file beside the binary.
	store := plugins.NewPocketBaseStore(client, colAppSettings)
	return &Server{
		cfg:         cfg,
		pb:          client,
		pluginStore: store,
		plugins:     plugins.NewManager(store),
		ocpp:        ocpp.NewCSMS(func(f string, a ...any) { log.Printf("ocpp: "+f, a...) }),
		control:     newControlIndex(),
		ctlRL:       newRateLimiter(30, time.Minute), // 30 control commands / min / charger
	}
}

// StartPlugins reads the plugin settings from PocketBase and initialises every
// enabled plugin. It also warms the OCPP control-token index from PocketBase (its
// source of truth), so the first charger to reconnect after a restart resolves
// immediately instead of triggering a lazy rebuild mid-handshake. The warm-up is
// best-effort and non-blocking; if PocketBase is not yet configured it no-ops and
// the lazy path rebuilds on first connect.
//
// The settings now live in the database, so at boot the database may not be
// reachable yet — a cold stack, or a service account still to be configured from
// the panel. That is not fatal and, crucially, not treated as "no plugins
// configured": the Manager stays unloaded, the admin endpoints answer 503, and a
// background retry keeps trying until the read succeeds. Nothing is written
// until something has been read, so an outage cannot erase the settings.
func (s *Server) StartPlugins() error {
	ctx, cancel := context.WithCancel(context.Background())
	s.pluginsStop = cancel

	go func() {
		warmCtx, warmCancel := context.WithTimeout(context.Background(), 15*time.Second)
		defer warmCancel()
		s.ensureControlIndex(warmCtx)
	}()

	err := s.loadPlugins(ctx)
	if err != nil {
		log.Printf("plugins: settings unavailable, retrying in the background (%v)", err)
		go s.retryLoadPlugins(ctx)
	}
	return err
}

// loadPlugins reads the settings, creating the collection they live in if it
// turns out not to exist.
//
// That happens on a stack upgraded with PB_BOOTSTRAP off: the on-boot schema
// pass never ran, so app_settings was never created, and because a missing
// collection is read as "not ready" (never as "no plugins configured", which
// would let the first save overwrite settings the server merely failed to find)
// the retry below would spin forever with the plugin panel stuck at 503. So
// create just that one collection — not a full schema reconcile, which an
// operator who turned the bootstrap off has not asked for — and read again.
func (s *Server) loadPlugins(ctx context.Context) error {
	err := s.plugins.Load(ctx)
	if err == nil || !plugins.IsMissingCollection(err) {
		return err
	}

	log.Printf("plugins: the %s collection does not exist; creating it", colAppSettings)
	if repairErr := bootstrap.EnsureCollection(ctx, s.pb, colAppSettings); repairErr != nil {
		log.Printf("plugins: could not create %s: %v", colAppSettings, repairErr)
		return err // report the original problem, not the repair's
	}
	log.Printf("plugins: created %s", colAppSettings)
	return s.plugins.Load(ctx)
}

// retryLoadPlugins keeps reading until the settings load or the server stops.
// The backoff caps at two minutes, so a long outage costs at most one log line
// every two minutes rather than a tight spin.
func (s *Server) retryLoadPlugins(ctx context.Context) {
	const maxBackoff = 2 * time.Minute
	backoff := 5 * time.Second
	for {
		select {
		case <-ctx.Done():
			return
		case <-time.After(backoff):
		}
		if err := s.loadPlugins(ctx); err == nil {
			log.Println("plugins: settings loaded")
			return
		} else {
			log.Printf("plugins: still unavailable, retrying in %s (%v)", backoff, err)
		}
		if backoff < maxBackoff {
			backoff *= 2
		}
	}
}

// Stop releases server-held resources (plugin instances and OCPP sessions).
func (s *Server) Stop(ctx context.Context) {
	if s.pluginsStop != nil {
		s.pluginsStop()
	}
	s.ocpp.Shutdown(ctx)
	s.plugins.Shutdown(ctx)
}

// usersCollection returns the PocketBase auth collection holding app users.
func (s *Server) usersCollection() string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.cfg.UsersCollection
}

// webAppURL returns the Web App address probed by /api/status.
func (s *Server) webAppURL() string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.cfg.WebAppURL
}

// webAppSettings snapshots the Web App settings for the settings endpoints.
func (s *Server) webAppSettings() (url string, allowOrigins []string) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.cfg.WebAppURL, append([]string(nil), s.cfg.AllowOrigins...)
}

// setWebAppConfig applies new Web App settings at runtime. The CORS middleware
// reads the origin list per request, so the new list is live immediately.
func (s *Server) setWebAppConfig(url string, allowOrigins []string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.cfg.WebAppURL = url
	s.cfg.AllowOrigins = append([]string(nil), allowOrigins...)
}

// serverName snapshots this server's display name. /api/health reads it per
// request so a rename from the panel needs no restart.
func (s *Server) serverName() string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.cfg.ServerName
}

// setServerName renames this server at runtime.
func (s *Server) setServerName(name string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.cfg.ServerName = name
}

// pbSettings snapshots the PocketBase connection for the settings endpoints.
func (s *Server) pbSettings() (url, adminEmail, adminPassword string) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.cfg.PocketBaseURL, s.cfg.PocketBaseAdminEmail, s.cfg.PocketBaseAdminPassword
}

// setPBConfig retargets the PocketBase connection at runtime: it updates the
// cached config and repoints the client (which drops its cached superuser token,
// so the next call re-authenticates against the new target).
func (s *Server) setPBConfig(url, adminEmail, adminPassword string) {
	s.mu.Lock()
	s.cfg.PocketBaseURL = url
	s.cfg.PocketBaseAdminEmail = adminEmail
	s.cfg.PocketBaseAdminPassword = adminPassword
	s.mu.Unlock()

	s.pb.Reconfigure(url, adminEmail, adminPassword)
}

// Handler returns the root HTTP handler with all routes registered.
func (s *Server) Handler() http.Handler {
	mux := http.NewServeMux()

	// Web panel (public) — embedded Vue + Tailwind app. Only the explicit panel
	// paths are routed to it so unknown /api/* paths still 404 as JSON.
	panel := panelHandler()
	mux.Handle("GET /{$}", panel)
	mux.Handle("GET /assets/", panel)
	mux.Handle("GET /favicon.svg", panel)

	// Health (public).
	mux.HandleFunc("GET /healthz", s.handleHealth)
	mux.HandleFunc("GET /api/health", s.handleHealth)
	mux.HandleFunc("GET /api/status", s.handleStatus)

	// Auth — proxied to the PocketBase kept behind this server.
	mux.HandleFunc("POST /api/auth/login", s.handleAuthLogin)
	mux.HandleFunc("GET /api/auth/validate", s.handleAuthValidate)
	mux.HandleFunc("GET /api/auth/me", s.handleAuthMe)
	mux.HandleFunc("GET /api/identity", s.handleIdentity)

	// Current user (profile / appearance / avatar / data / account lifecycle).
	mux.HandleFunc("GET /api/me", s.handleGetMe)
	mux.HandleFunc("PATCH /api/me", s.handleUpdateMe)
	mux.HandleFunc("POST /api/me/password", s.handleChangePassword)
	mux.HandleFunc("POST /api/me/avatar", s.handleUploadAvatar)
	mux.HandleFunc("GET /api/me/avatar", s.handleGetAvatar)
	mux.HandleFunc("DELETE /api/me/avatar", s.handleDeleteAvatar)
	mux.HandleFunc("POST /api/me/verify/request", s.handleRequestVerification)
	mux.HandleFunc("GET /api/me/export", s.handleExportData)
	mux.HandleFunc("POST /api/me/import", s.handleImportData)
	mux.HandleFunc("POST /api/me/delete", s.handleRequestDeletion)
	mux.HandleFunc("POST /api/me/delete/cancel", s.handleCancelDeletion)
	mux.HandleFunc("DELETE /api/me", s.handleFinalizeDeletion)

	// User management — gated on the caller being a manager (admin or
	// superadmin). Admins are scoped to their own organization inside each
	// handler.
	mux.HandleFunc("GET /api/users", s.requireManager(s.handleListUsers))
	mux.HandleFunc("POST /api/users", s.requireManager(s.handleCreateUser))
	mux.HandleFunc("PATCH /api/users/{id}", s.requireManager(s.handleUpdateUser))
	mux.HandleFunc("DELETE /api/users/{id}", s.requireManager(s.handleDeleteUser))

	// Organizations — the tenants users belong to. Listing is manager-scoped (an
	// admin sees only their own org). Any org-less user may create an org and
	// becomes its admin; an admin may rename or delete their own org; a superadmin
	// spans every organization. Create carries no role gate, so it checks the
	// service account itself.
	mux.HandleFunc("GET /api/orgs", s.requireManager(s.handleListOrgs))
	mux.HandleFunc("POST /api/orgs", s.handleCreateOrg)
	mux.HandleFunc("PATCH /api/orgs/{id}", s.requireManager(s.handleUpdateOrg))
	mux.HandleFunc("DELETE /api/orgs/{id}", s.requireManager(s.handleDeleteOrg))

	// PocketBase connection settings — superadmin only. These do NOT require the
	// service account to already be configured (they exist to configure it).
	mux.HandleFunc("GET /api/admin/pb-config", s.requireSuperadminAuth(s.handleGetPBConfig))
	mux.HandleFunc("POST /api/admin/pb-config/test", s.requireSuperadminAuth(s.handleTestPBConfig))
	mux.HandleFunc("PUT /api/admin/pb-config", s.requireSuperadminAuth(s.handleUpdatePBConfig))

	// Web App settings — superadmin only. Where the Web App lives (probed by
	// /api/status) and which browser origins CORS admits.
	mux.HandleFunc("GET /api/admin/webapp-config", s.requireSuperadminAuth(s.handleGetWebAppConfig))
	mux.HandleFunc("POST /api/admin/webapp-config/test", s.requireSuperadminAuth(s.handleTestWebAppConfig))
	mux.HandleFunc("PUT /api/admin/webapp-config", s.requireSuperadminAuth(s.handleUpdateWebAppConfig))

	// API Server settings — superadmin only. This server's own display name,
	// which clients pointed at several DriverVaults use to tell them apart.
	mux.HandleFunc("GET /api/admin/server-config", s.requireSuperadminAuth(s.handleGetServerConfig))
	mux.HandleFunc("PUT /api/admin/server-config", s.requireSuperadminAuth(s.handleUpdateServerConfig))

	// Plugins — external-service integrations, managed by a superadmin.
	mux.HandleFunc("GET /api/admin/plugins", s.requireSuperadminAuth(s.handleListPlugins))
	mux.HandleFunc("POST /api/admin/plugins", s.requireSuperadminAuth(s.handleRegisterPlugin))
	mux.HandleFunc("GET /api/admin/plugins/{name}", s.requireSuperadminAuth(s.handleGetPlugin))
	mux.HandleFunc("PUT /api/admin/plugins/{name}", s.requireSuperadminAuth(s.handleUpdatePlugin))
	mux.HandleFunc("DELETE /api/admin/plugins/{name}", s.requireSuperadminAuth(s.handleDeletePlugin))
	mux.HandleFunc("POST /api/admin/plugins/{name}/health", s.requireSuperadminAuth(s.handlePluginHealth))

	// Integrations — per-user plugin settings under the superadmin → org admin →
	// user cascade (see integrations.go). Any authenticated user manages their
	// own layer; an org admin may also target their organization's layer.
	mux.HandleFunc("GET /api/integrations/toyota", s.handleGetToyota)
	mux.HandleFunc("PUT /api/integrations/toyota", s.handlePutToyota)
	mux.HandleFunc("POST /api/integrations/toyota/health", s.handleToyotaHealth)
	mux.HandleFunc("GET /api/integrations/toyota/vehicles", s.handleToyotaVehicles)
	mux.HandleFunc("GET /api/integrations/anker-solix", s.handleGetAnker)
	mux.HandleFunc("PUT /api/integrations/anker-solix", s.handlePutAnker)
	mux.HandleFunc("POST /api/integrations/anker-solix/health", s.handleAnkerHealth)
	mux.HandleFunc("GET /api/integrations/anker-solix/chargers", s.handleAnkerChargers)
	mux.HandleFunc("GET /api/integrations/anker-solix/chargers/{sn}/details", s.handleAnkerChargerDetails)
	mux.HandleFunc("POST /api/integrations/anker-solix/chargers/{sn}/rfid-cards", s.handleAnkerCardSave)
	mux.HandleFunc("POST /api/integrations/anker-solix/chargers/{sn}/rfid-cards/scan", s.handleAnkerCardScan)
	mux.HandleFunc("DELETE /api/integrations/anker-solix/chargers/{sn}/rfid-cards/{number}", s.handleAnkerCardDelete)
	mux.HandleFunc("GET /api/integrations/anker-solix/chargers/{sn}/rfid-cards/charger", s.handleAnkerChargerCards)
	mux.HandleFunc("GET /api/integrations/greencell", s.handleGetGreencell)
	mux.HandleFunc("PUT /api/integrations/greencell", s.handlePutGreencell)
	mux.HandleFunc("POST /api/integrations/greencell/health", s.handleGreencellHealth)
	mux.HandleFunc("GET /api/integrations/greencell/chargers", s.handleGreencellChargers)
	mux.HandleFunc("GET /api/integrations/greencell/chargers/{sn}/state", s.handleGreencellChargerState)

	// Anker Solix OCPP control (per-charger; gated by the same cascade plus a
	// control mode of own/proxy and a live CSMS session). See
	// integrations_ankersolix_control.go.
	mux.HandleFunc("GET /api/integrations/anker-solix/chargers/{sn}/control", s.handleAnkerControlStatus)
	mux.HandleFunc("POST /api/integrations/anker-solix/chargers/{sn}/control/token", s.handleAnkerControlToken)
	mux.HandleFunc("DELETE /api/integrations/anker-solix/chargers/{sn}/control/token", s.handleAnkerControlRevoke)
	mux.HandleFunc("PUT /api/integrations/anker-solix/chargers/{sn}/control/address", s.handleAnkerControlAddress)
	mux.HandleFunc("DELETE /api/integrations/anker-solix/chargers/{sn}/control/address", s.handleAnkerControlAddress)
	mux.HandleFunc("POST /api/integrations/anker-solix/chargers/{sn}/{action}", s.handleAnkerControlAction)

	// OCPP WebSocket endpoint the charger dials out to (own/proxy modes). It sits
	// outside /api/ so it bypasses bearer auth; it authenticates the charger with
	// OCPP Basic auth (serial + per-charger control token) instead.
	mux.HandleFunc("GET /ocpp/{serial}", s.handleOCPPConnect)

	// Vehicle providers — manufacturer services a car can be created from, and
	// the per-car provider tab. Generic over the registered providers; see
	// vehicleproviders.go.
	mux.HandleFunc("GET /api/vehicle-providers", s.handleListVehicleProviders)
	mux.HandleFunc("GET /api/vehicle-providers/{provider}/vehicles", s.handleProviderVehicles)
	mux.HandleFunc("POST /api/vehicle-providers/{provider}/import", s.handleProviderImport)

	// Charger providers — the same move for the wallbox: a charger on a connected
	// service becomes one of the caller's home chargers. See chargerproviders.go.
	mux.HandleFunc("GET /api/charger-providers", s.handleListChargerProviders)
	mux.HandleFunc("GET /api/charger-providers/{provider}/chargers", s.handleProviderChargers)
	mux.HandleFunc("POST /api/charger-providers/{provider}/import", s.handleChargerImport)

	// The caller's own chargers (homechargers.go).
	mux.HandleFunc("GET /api/home-chargers", s.listHomeChargers)
	mux.HandleFunc("PATCH /api/home-chargers/{id}", s.updateHomeCharger)
	mux.HandleFunc("DELETE /api/home-chargers/{id}", s.deleteHomeCharger)

	// Cars + sharing.
	mux.HandleFunc("GET /api/cars", s.listCars)
	mux.HandleFunc("POST /api/cars", s.createCar)
	mux.HandleFunc("GET /api/cars/{id}", s.getCar)
	mux.HandleFunc("PATCH /api/cars/{id}", s.updateCar)
	mux.HandleFunc("PUT /api/cars/{id}/view", s.updateCarView)
	mux.HandleFunc("DELETE /api/cars/{id}", s.deleteCar)
	mux.HandleFunc("GET /api/cars/{id}/service-records", s.listCarServiceRecords)
	mux.HandleFunc("GET /api/cars/{id}/technical-checks", s.listCarTechnicalChecks)
	mux.HandleFunc("GET /api/cars/{id}/parts", s.listCarParts)
	mux.HandleFunc("GET /api/cars/{id}/fuel-entries", s.listCarFuelEntries)
	mux.HandleFunc("GET /api/cars/{id}/fuel-stats", s.listCarFuelStats)
	mux.HandleFunc("GET /api/cars/{id}/charging-sessions", s.listCarChargingSessions)
	mux.HandleFunc("GET /api/cars/{id}/charging-stats", s.listCarChargingStats)
	mux.HandleFunc("GET /api/cars/{id}/maintenance", s.listCarMaintenance)
	mux.HandleFunc("GET /api/cars/{id}/documents", s.listCarDocuments)
	mux.HandleFunc("GET /api/cars/{id}/reminders", s.listCarReminders)
	mux.HandleFunc("GET /api/cars/{id}/provider", s.handleCarProvider)
	mux.HandleFunc("POST /api/cars/{id}/provider", s.handleLinkCarProvider)
	mux.HandleFunc("POST /api/cars/{id}/provider/sync", s.handleSyncCarProvider)
	mux.HandleFunc("GET /api/cars/{id}/shares", s.handleListShares)
	mux.HandleFunc("POST /api/cars/{id}/shares", s.handleUpsertShare)
	mux.HandleFunc("DELETE /api/cars/{id}/shares/{userId}", s.handleDeleteShare)

	// Service records.
	mux.HandleFunc("GET /api/service-records", s.listServiceRecords)
	mux.HandleFunc("POST /api/service-records", s.createServiceRecord)
	mux.HandleFunc("GET /api/service-records/{id}", s.getServiceRecord)
	mux.HandleFunc("PATCH /api/service-records/{id}", s.updateServiceRecord)
	mux.HandleFunc("DELETE /api/service-records/{id}", s.deleteServiceRecord)

	// Technical checks (roadworthiness inspections).
	mux.HandleFunc("GET /api/technical-checks", s.listTechnicalChecks)
	mux.HandleFunc("POST /api/technical-checks", s.createTechnicalCheck)
	mux.HandleFunc("GET /api/technical-checks/{id}", s.getTechnicalCheck)
	mux.HandleFunc("PATCH /api/technical-checks/{id}", s.updateTechnicalCheck)
	mux.HandleFunc("DELETE /api/technical-checks/{id}", s.deleteTechnicalCheck)

	// Parts.
	mux.HandleFunc("GET /api/parts", s.listParts)
	mux.HandleFunc("POST /api/parts", s.createPart)
	mux.HandleFunc("GET /api/parts/{id}", s.getPart)
	mux.HandleFunc("PATCH /api/parts/{id}", s.updatePart)
	mux.HandleFunc("DELETE /api/parts/{id}", s.deletePart)

	// Fuel entries.
	mux.HandleFunc("GET /api/fuel-entries", s.listFuelEntries)
	mux.HandleFunc("POST /api/fuel-entries", s.createFuelEntry)
	mux.HandleFunc("GET /api/fuel-entries/{id}", s.getFuelEntry)
	mux.HandleFunc("PATCH /api/fuel-entries/{id}", s.updateFuelEntry)
	mux.HandleFunc("DELETE /api/fuel-entries/{id}", s.deleteFuelEntry)

	// Charging sessions.
	mux.HandleFunc("GET /api/charging-sessions", s.listChargingSessions)
	mux.HandleFunc("POST /api/charging-sessions", s.createChargingSession)
	mux.HandleFunc("GET /api/charging-sessions/{id}", s.getChargingSession)
	mux.HandleFunc("PATCH /api/charging-sessions/{id}", s.updateChargingSession)
	mux.HandleFunc("DELETE /api/charging-sessions/{id}", s.deleteChargingSession)

	// Maintenance log.
	mux.HandleFunc("GET /api/maintenance", s.listMaintenance)
	mux.HandleFunc("POST /api/maintenance", s.createMaintenance)
	mux.HandleFunc("GET /api/maintenance/{id}", s.getMaintenance)
	mux.HandleFunc("PATCH /api/maintenance/{id}", s.updateMaintenance)
	mux.HandleFunc("DELETE /api/maintenance/{id}", s.deleteMaintenance)

	// Documents. Named /api/car-documents so the path can't be mistaken for the
	// user-facing account documents some other Vault services expose.
	mux.HandleFunc("GET /api/car-documents", s.listDocuments)
	mux.HandleFunc("POST /api/car-documents", s.createDocument)
	mux.HandleFunc("GET /api/car-documents/{id}", s.getDocument)
	mux.HandleFunc("PATCH /api/car-documents/{id}", s.updateDocument)
	mux.HandleFunc("DELETE /api/car-documents/{id}", s.deleteDocument)

	// Reminders.
	mux.HandleFunc("GET /api/reminders", s.listReminders)
	mux.HandleFunc("POST /api/reminders", s.createReminder)
	mux.HandleFunc("GET /api/reminders/{id}", s.getReminder)
	mux.HandleFunc("PATCH /api/reminders/{id}", s.updateReminder)
	mux.HandleFunc("DELETE /api/reminders/{id}", s.deleteReminder)
	mux.HandleFunc("POST /api/reminders/{id}/complete", s.handleCompleteReminder)

	// Attachments — one optional file per record, on identical terms for every
	// collection that takes one (see attachments.go). The GET handler passed
	// alongside is what renders the record after an upload.
	s.attachmentRoutes(mux, "/api/car-documents", colDocuments, s.getDocument)
	s.attachmentRoutes(mux, "/api/service-records", colServices, s.getServiceRecord)
	s.attachmentRoutes(mux, "/api/technical-checks", colTechnicalChecks, s.getTechnicalCheck)
	s.attachmentRoutes(mux, "/api/maintenance", colMaintenance, s.getMaintenance)
	s.attachmentRoutes(mux, "/api/fuel-entries", colFuel, s.getFuelEntry)
	s.attachmentRoutes(mux, "/api/charging-sessions", colCharging, s.getChargingSession)
	s.attachmentRoutes(mux, "/api/parts", colParts, s.getPart)

	return s.withMiddleware(mux)
}

// withMiddleware applies panic recovery, CORS, request logging, and
// authentication globally.
func (s *Server) withMiddleware(next http.Handler) http.Handler {
	return s.recoverer(s.cors(s.logger(s.withAuth(next))))
}

func (s *Server) logger(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(sw, r)
		log.Printf("%s %s %d %s", r.Method, r.URL.Path, sw.status, time.Since(start).Round(time.Millisecond))
	})
}

func (s *Server) recoverer(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				log.Printf("panic: %v", rec)
				writeError(w, http.StatusInternalServerError, "internal error")
			}
		}()
		next.ServeHTTP(w, r)
	})
}

// originAllowed reports whether origin may call this server, and whether it was
// the wildcard that allowed it. The allow-list is consulted per request rather
// than captured once, so editing it from the panel takes effect without a
// restart.
func (s *Server) originAllowed(origin string) (allowed, wildcard bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for _, o := range s.cfg.AllowOrigins {
		if o == "*" {
			return true, true
		}
		if o == origin {
			allowed = true
		}
	}
	return allowed, false
}

func (s *Server) cors(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if origin := r.Header.Get("Origin"); origin != "" {
			if allowed, wildcard := s.originAllowed(origin); allowed {
				if wildcard {
					w.Header().Set("Access-Control-Allow-Origin", "*")
				} else {
					w.Header().Set("Access-Control-Allow-Origin", origin)
					w.Header().Add("Vary", "Origin")
				}
				w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
				w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
			}
		}
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// statusWriter captures the response status code for logging.
type statusWriter struct {
	http.ResponseWriter
	status int
	wrote  bool
}

func (w *statusWriter) WriteHeader(code int) {
	if !w.wrote {
		w.status = code
		w.wrote = true
	}
	w.ResponseWriter.WriteHeader(code)
}

func (w *statusWriter) Write(b []byte) (int, error) {
	w.wrote = true
	return w.ResponseWriter.Write(b)
}

// Hijack lets the wrapped ResponseWriter be taken over for a protocol switch
// (the OCPP WebSocket upgrade at /ocpp/{serial}). Without this pass-through the
// logging middleware would hide the underlying http.Hijacker.
func (w *statusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	hj, ok := w.ResponseWriter.(http.Hijacker)
	if !ok {
		return nil, nil, http.ErrNotSupported
	}
	w.wrote = true // a hijacked connection writes its own response
	return hj.Hijack()
}
