package handlers

import (
	"encoding/json"
	"errors"
	"net/http"
	"time"

	"github.com/google/uuid"

	"mgit.msbls.de/m/paliad/internal/models"
	"mgit.msbls.de/m/paliad/internal/services"
)

// requireCalDAV reports the CalDAV-capable service or writes 501. Used by
// every CalDAV endpoint so misconfigured deployments return a clean error
// instead of crashing.
func requireCalDAV(w http.ResponseWriter) bool {
	if !requireDB(w) {
		return false
	}
	if dbSvc.caldav == nil || !dbSvc.caldav.Enabled() {
		writeJSON(w, http.StatusNotImplemented, map[string]string{
			"error": "CalDAV-Synchronisation derzeit nicht verf\u00fcgbar \u2014 Administrator kontaktieren (CALDAV_ENCRYPTION_KEY nicht gesetzt).",
		})
		return false
	}
	return true
}

// GET /api/appointments?project_id=&from=&to=&type=
func handleListAppointments(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	q := r.URL.Query()
	filter := services.AppointmentListFilter{}
	raw := q.Get("project_id")
	if raw == "" {
		raw = q.Get("project_id")
	}
	if raw != "" {
		projectID, err := uuid.Parse(raw)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project_id"})
			return
		}
		filter.ProjectID = &projectID
	}
	if raw := q.Get("from"); raw != "" {
		t, err := parseDateOrTime(raw)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid from"})
			return
		}
		filter.From = &t
	}
	if raw := q.Get("to"); raw != "" {
		t, err := parseDateOrTime(raw)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid to"})
			return
		}
		filter.To = &t
	}
	if raw := q.Get("type"); raw != "" {
		filter.Type = &raw
	}
	rows, err := dbSvc.appointment.ListVisibleForUser(r.Context(), uid, filter)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// GET /api/appointments/summary
func handleAppointmentsSummary(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	c, err := dbSvc.appointment.SummaryCounts(r.Context(), uid)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, c)
}

// GET /api/projects/{id}/appointments
func handleListAppointmentsForProject(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	projectID, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	directOnly := parseDirectOnly(r.URL.Query().Get("direct_only"))
	rows, err := dbSvc.appointment.ListForProject(r.Context(), uid, projectID, directOnly)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// POST /api/appointments
func handleCreateAppointment(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	var input services.CreateAppointmentInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	t, err := dbSvc.appointment.Create(r.Context(), uid, input)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusCreated, t)
}

// GET /api/appointments/{id}
func handleGetAppointment(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	t, err := dbSvc.appointment.GetByID(r.Context(), uid, id)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, t)
}

// PATCH /api/appointments/{id}
func handleUpdateAppointment(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	var input services.UpdateAppointmentInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	t, err := dbSvc.appointment.Update(r.Context(), uid, id, input)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, t)
}

// DELETE /api/appointments/{id}
func handleDeleteAppointment(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	if err := dbSvc.appointment.Delete(r.Context(), uid, id); err != nil {
		writeServiceError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// GET /api/caldav-config
func handleGetCalDAVConfig(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	// 501 only when CalDAV is fully unavailable. GET still works without
	// the cipher so the UI can show "no config" without failing — but if
	// the cipher is missing the user can't actually do anything, so return
	// 501 for consistency with PUT/DELETE.
	if dbSvc.caldav == nil || !dbSvc.caldav.Enabled() {
		writeJSON(w, http.StatusNotImplemented, map[string]any{
			"error":   "CalDAV-Synchronisation derzeit nicht verf\u00fcgbar \u2014 Administrator kontaktieren (CALDAV_ENCRYPTION_KEY nicht gesetzt).",
			"enabled": false,
		})
		return
	}
	cfg, err := dbSvc.caldav.GetConfig(r.Context(), uid)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if cfg == nil {
		writeJSON(w, http.StatusOK, map[string]any{"configured": false})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"configured":      true,
		"url":             cfg.URL,
		"username":        cfg.Username,
		"calendar_path":   cfg.CalendarPath,
		"enabled":         cfg.Enabled,
		"last_sync_at":    cfg.LastSyncAt,
		"last_sync_error": cfg.LastSyncError,
		"updated_at":      cfg.UpdatedAt,
	})
}

// PUT /api/caldav-config
func handlePutCalDAVConfig(w http.ResponseWriter, r *http.Request) {
	if !requireCalDAV(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	var input services.SaveConfigInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	cfg, err := dbSvc.caldav.SaveConfig(r.Context(), uid, input)
	if err != nil {
		writeCalDAVError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, cfg)
}

// DELETE /api/caldav-config
func handleDeleteCalDAVConfig(w http.ResponseWriter, r *http.Request) {
	if !requireCalDAV(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if err := dbSvc.caldav.DeleteConfig(r.Context(), uid); err != nil {
		writeServiceError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// POST /api/caldav-config/test — performs PROPFIND with the supplied
// credentials, no persistence.
func handleTestCalDAVConfig(w http.ResponseWriter, r *http.Request) {
	if !requireCalDAV(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	var input services.SaveConfigInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	ctx := services.WithCalDAVUser(r.Context(), uid)
	if err := dbSvc.caldav.TestConnection(ctx, input); err != nil {
		writeJSON(w, http.StatusOK, map[string]any{"ok": false, "error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}

// GET /api/caldav-bindings — list the authenticated user's CalDAV
// bindings (the (calendar, scope) entries layered on the single CalDAV
// server connection). Read-only in Slice 2a; full CRUD lands in Slice 2b.
func handleListCalDAVBindings(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.caldavBindings == nil {
		writeJSON(w, http.StatusNotImplemented, map[string]any{
			"error": "CalDAV bindings unavailable (CalDAV service not configured)",
		})
		return
	}
	rows, err := dbSvc.caldavBindings.ListForUser(r.Context(), uid)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if rows == nil {
		rows = []models.UserCalendarBinding{}
	}
	writeJSON(w, http.StatusOK, rows)
}

// POST /api/caldav-bindings — create a new binding for the
// authenticated user and synchronously fire a first push so the modal
// closes with events already landed. Returns 201 with the binding row.
func handleCreateCalDAVBinding(w http.ResponseWriter, r *http.Request) {
	if !requireCalDAV(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.caldavBindings == nil {
		writeJSON(w, http.StatusNotImplemented, map[string]any{"error": "CalDAV bindings unavailable"})
		return
	}
	var input services.CreateBindingInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	// Default to enabled=true so the modal "Hinzufügen" button does the
	// expected thing without forcing the user to toggle anything.
	if !input.Enabled {
		input.Enabled = true
	}
	binding, err := dbSvc.caldavBindings.Create(r.Context(), uid, input)
	if err != nil {
		writeCalDAVError(w, err)
		return
	}
	// Synchronous first push per Q5 of the Slice 2 design (m's 2026-05-20
	// pick): block the request so the user sees events already landed
	// when the modal closes. PushBindingNow logs per-event failures and
	// returns; we only surface a hard config/cipher error.
	pushed, pushErr := dbSvc.caldav.PushBindingNow(r.Context(), uid, binding)
	if pushErr != nil {
		// Binding was created; sync failed. Tell the UI both bits so it
		// can show "binding added, initial sync had a problem".
		writeJSON(w, http.StatusCreated, map[string]any{
			"binding":           binding,
			"initial_pushed":    pushed,
			"initial_sync_error": pushErr.Error(),
		})
		return
	}
	// Ensure the per-user goroutine is running so future ticks happen.
	dbSvc.caldav.EnsureLoop(uid)
	writeJSON(w, http.StatusCreated, map[string]any{
		"binding":        binding,
		"initial_pushed": pushed,
	})
}

// PATCH /api/caldav-bindings/{id} — partial update. Lazy scope cleanup
// per Q6: stale targets get dropped on the next sync tick, not here.
func handlePatchCalDAVBinding(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.caldavBindings == nil {
		writeJSON(w, http.StatusNotImplemented, map[string]any{"error": "CalDAV bindings unavailable"})
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	var input services.UpdateBindingInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	binding, err := dbSvc.caldavBindings.Update(r.Context(), uid, id, input)
	if err != nil {
		writeCalDAVError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, binding)
}

// DELETE /api/caldav-bindings/{id} — best-effort remote cleanup of every
// .ics this binding pushed, then drop the binding row. On partial remote
// failure the binding is disabled (not deleted) so the next sync tick
// can retry; the response is 202 Accepted in that case.
func handleDeleteCalDAVBinding(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.caldav == nil {
		writeJSON(w, http.StatusNotImplemented, map[string]any{"error": "CalDAV bindings unavailable"})
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	fully, err := dbSvc.caldav.RemoveBinding(r.Context(), uid, id)
	if err != nil {
		writeCalDAVError(w, err)
		return
	}
	if !fully {
		writeJSON(w, http.StatusAccepted, map[string]any{
			"status": "partial",
			"message": "Binding disabled; some remote events could not be deleted. Retry on next sync tick.",
		})
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// POST /api/caldav-mkcalendar — creates a new calendar on the user's
// CalDAV server via MKCALENDAR + a matching binding row in one logical
// transaction. Slice 2c only — visible when /api/caldav-discover
// reports supports_mkcalendar=true. Errors:
//   - 501 when supports_mkcalendar=false (caller should show the
//     Google-degrade UX with the manual-URL input).
//   - 409 when the slugified name + 3 retries all collide on the
//     server. UI should ask the user to type their own name.
func handleCalDAVMakeCalendar(w http.ResponseWriter, r *http.Request) {
	if !requireCalDAV(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	var input services.CreateCalendarInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	result, err := dbSvc.caldav.MakeCalendar(r.Context(), uid, input)
	if err != nil {
		switch {
		case errors.Is(err, services.ErrMKCalendarUnsupported):
			writeJSON(w, http.StatusNotImplemented, map[string]any{
				"error":               err.Error(),
				"supports_mkcalendar": false,
			})
		case errors.Is(err, services.ErrCalendarNameTaken):
			writeJSON(w, http.StatusConflict, map[string]any{
				"error": err.Error(),
			})
		default:
			// Binding-create / push errors carry the partial result so
			// the UI can surface "created remotely but binding failed".
			if result != nil {
				writeJSON(w, http.StatusCreated, map[string]any{
					"calendar_path":      result.CalendarPath,
					"binding":            result.Binding,
					"initial_pushed":     result.InitialPushed,
					"initial_sync_error": err.Error(),
				})
				return
			}
			writeCalDAVError(w, err)
		}
		return
	}
	writeJSON(w, http.StatusCreated, result)
}

// GET /api/caldav-discover — walks the calendar-home-set chain on the
// user's CalDAV server and returns the calendars they own. Cached
// server-side for 5 minutes per user (Q4 of Slice 2 brief).
func handleCalDAVDiscover(w http.ResponseWriter, r *http.Request) {
	if !requireCalDAV(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	result, err := dbSvc.caldav.DiscoverCalendars(r.Context(), uid)
	if err != nil {
		writeCalDAVError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, result)
}

// GET /api/caldav-config/log — last 5 sync attempts.
func handleCalDAVSyncLog(w http.ResponseWriter, r *http.Request) {
	if !requireCalDAV(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	rows, err := dbSvc.caldav.SyncLog(r.Context(), uid, 5)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// writeCalDAVError maps services.ErrCalDAVNoKey + invalid input to the
// right HTTP status; defers everything else to writeServiceError.
func writeCalDAVError(w http.ResponseWriter, err error) {
	switch {
	case errors.Is(err, services.ErrCalDAVNoKey):
		writeJSON(w, http.StatusNotImplemented, map[string]string{
			"error": "CalDAV-Synchronisation derzeit nicht verf\u00fcgbar \u2014 Administrator kontaktieren.",
		})
	default:
		writeServiceError(w, err)
	}
}

// parseDateOrTime accepts "2026-04-16" (interpreted as midnight UTC) or
// any RFC3339 timestamp.
func parseDateOrTime(s string) (time.Time, error) {
	if t, err := time.Parse(time.RFC3339, s); err == nil {
		return t.UTC(), nil
	}
	if t, err := time.Parse("2006-01-02", s); err == nil {
		return t.UTC(), nil
	}
	return time.Time{}, errors.New("expected RFC3339 or YYYY-MM-DD")
}
