package handlers

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

	"github.com/google/uuid"

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

// dbServices bundles the Phase B services so handlers can stay thin.
// Nil if DATABASE_URL was unset at startup.
type dbServices struct {
	projects       *services.ProjectService
	team           *services.TeamService
	partnerUnit    *services.PartnerUnitService
	parties        *services.PartyService
	deadline       *services.DeadlineService
	appointment    *services.AppointmentService
	caldav         *services.CalDAVService
	caldavBindings *services.CalendarBindingService
	rules          *services.DeadlineRuleService
	calc           *services.DeadlineCalculator
	users          *services.UserService
	fristenrechner *services.FristenrechnerService
	eventDeadline  *services.EventDeadlineService
	eventTrigger   *services.EventTriggerService
	ruleEditor     *services.RuleEditorService
	deadlineSearch *services.DeadlineSearchService
	eventCategory  *services.EventCategoryService
	eventType      *services.EventTypeService
	dashboard      *services.DashboardService
	note           *services.NoteService
	checklistInst  *services.ChecklistInstanceService
	mail           *services.MailService
	invite         *services.InviteService
	agenda         *services.AgendaService
	audit          *services.AuditService
	emailTemplate  *services.EmailTemplateService
	link           *services.LinkService
	event          *services.EventService
	courts         *services.CourtService
	approval       *services.ApprovalService
	derivation     *services.DerivationService
	userView       *services.UserViewService
	broadcast      *services.BroadcastService
	pin            *services.PinService
	cardLayout     *services.CardLayoutService
	dashboardLayout *services.DashboardLayoutService
	projection     *services.ProjectionService
	export         *services.ExportService
}

var dbSvc *dbServices

// requireDB returns true if the DB-backed services are wired; otherwise
// writes a 503 response and returns false.
func requireDB(w http.ResponseWriter) bool {
	if dbSvc == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "database not configured — set DATABASE_URL on the server",
		})
		return false
	}
	return true
}

// requireUser pulls the authenticated user UUID from the request context.
func requireUser(w http.ResponseWriter, r *http.Request) (uuid.UUID, bool) {
	uid, ok := auth.UserIDFromContext(r.Context())
	if !ok {
		writeJSON(w, http.StatusUnauthorized, map[string]string{
			"error": "authentication required",
		})
		return uuid.Nil, false
	}
	return uid, true
}

// writeServiceError maps a services error to an HTTP status.
func writeServiceError(w http.ResponseWriter, err error) {
	if mapApprovalError(w, err) {
		return
	}
	switch {
	case errors.Is(err, services.ErrNotVisible):
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
	case errors.Is(err, services.ErrForbidden):
		writeJSON(w, http.StatusForbidden, map[string]string{"error": err.Error()})
	case errors.Is(err, services.ErrInvalidInput):
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
	case errors.Is(err, services.ErrInvalidProceedingTypeCategory):
		// Phase 3 Slice 5 (t-paliad-186). Bilingual user-facing message
		// matches what the project-form copy expects so the toast reads
		// naturally without an i18n round-trip in the handler.
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "Verfahrenstyp muss ein Fristenrechner-Typ sein / proceeding type must be a Fristenrechner type",
		})
	case errors.Is(err, services.ErrEventTypeSlugTaken):
		writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
	default:
		log.Printf("ERROR service: %v", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
	}
}

// mapApprovalError handles approval-flow errors that bubble through the
// shared writeServiceError path (entity mutation handlers — deadlines,
// appointments — go through writeServiceError, not writeApprovalError).
//
// Returns true iff err matched an approval-flow case and the response
// has been written. False = caller should keep walking the switch.
//
// Response shape (t-paliad-160 §B):
//
//	{
//	  code: "awaiting_approval" | "no_qualified_approver" | ...,
//	  message: "<localizable German hint>",
//	  request_id?: "<uuid>",       // present when known
//	  required_role?: "<role>",    // present when known
//	}
func mapApprovalError(w http.ResponseWriter, err error) bool {
	var pendingErr *services.PendingApprovalError
	if errors.As(err, &pendingErr) {
		body := map[string]string{
			"code":    "awaiting_approval",
			"message": "Diese Anforderung wartet auf Genehmigung.",
		}
		if pendingErr.RequestID != "" {
			body["request_id"] = pendingErr.RequestID
		}
		if pendingErr.RequiredRole != "" {
			body["required_role"] = pendingErr.RequiredRole
		}
		writeJSON(w, http.StatusConflict, body)
		return true
	}
	switch {
	case errors.Is(err, services.ErrConcurrentPending):
		writeJSON(w, http.StatusConflict, map[string]string{
			"code":    "awaiting_approval",
			"message": "Diese Anforderung wartet auf Genehmigung.",
		})
		return true
	case errors.Is(err, services.ErrNoQualifiedApprover):
		writeJSON(w, http.StatusConflict, map[string]string{
			"code":    "no_qualified_approver",
			"message": "Es gibt keinen anderen Benutzer, der diese Anfrage genehmigen kann.",
		})
		return true
	case errors.Is(err, services.ErrSelfApproval):
		writeJSON(w, http.StatusForbidden, map[string]string{
			"code":    "self_approval_blocked",
			"message": "Selbst-Genehmigung ist nicht erlaubt.",
		})
		return true
	case errors.Is(err, services.ErrNotApprover):
		writeJSON(w, http.StatusForbidden, map[string]string{
			"code":    "not_authorized",
			"message": "Sie sind für diese Genehmigung nicht berechtigt.",
		})
		return true
	case errors.Is(err, services.ErrRequestNotPending):
		writeJSON(w, http.StatusConflict, map[string]string{
			"code":    "request_not_pending",
			"message": "Die Anfrage ist nicht mehr offen.",
		})
		return true
	case errors.Is(err, services.ErrSuggestionRequiresChange):
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"code":    "suggestion_requires_change",
			"message": "Ein Vorschlag braucht entweder geänderte Werte oder einen Kommentar.",
		})
		return true
	case errors.Is(err, services.ErrSuggestionLifecycleInvalid):
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"code":    "suggestion_lifecycle_invalid",
			"message": "Änderungen vorschlagen ist nur für Update- und Complete-Anfragen möglich.",
		})
		return true
	}
	return false
}

// GET /api/projects — list visible projects.
// Query params: ?type=case&status=active&parent_id=<uuid>&parent_null=1&search=foo
func handleListProjects(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	q := r.URL.Query()
	filter := services.ProjectFilter{
		Type:   q.Get("type"),
		Status: q.Get("status"),
		Search: q.Get("search"),
	}
	if pidStr := q.Get("parent_id"); pidStr != "" {
		pid, err := uuid.Parse(pidStr)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid parent_id"})
			return
		}
		filter.ParentID = &pid
	}
	if q.Get("parent_null") == "1" || q.Get("parent_null") == "true" {
		filter.ParentNullOnly = true
	}
	rows, err := dbSvc.projects.List(r.Context(), uid, filter)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// POST /api/projects — also accepts the legacy POST /api/akten body shape
// ({aktenzeichen, owning_office, court_ref}) for the frontend transition.
// aktenzeichen → reference, court_ref → case_number, owning_office is dropped
// (no longer part of the visibility model). Type defaults to 'case'.
func handleCreateProject(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	// Parse into a loose map so we can accept both old and new shapes.
	var raw map[string]any
	if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	input := services.CreateProjectInput{
		Type: services.ProjectTypeCase,
	}
	if v, ok := raw["type"].(string); ok && v != "" {
		input.Type = v
	}
	if v, ok := raw["title"].(string); ok {
		input.Title = v
	}
	// Legacy aktenzeichen → reference; new shape uses reference directly.
	if v, ok := raw["reference"].(string); ok && v != "" {
		input.Reference = &v
	} else if v, ok := raw["aktenzeichen"].(string); ok && v != "" {
		input.Reference = &v
	}
	if v, ok := raw["description"].(string); ok && v != "" {
		input.Description = &v
	}
	if v, ok := raw["status"].(string); ok {
		input.Status = v
	}
	if v, ok := raw["court"].(string); ok && v != "" {
		input.Court = &v
	}
	if v, ok := raw["case_number"].(string); ok && v != "" {
		input.CaseNumber = &v
	} else if v, ok := raw["court_ref"].(string); ok && v != "" {
		input.CaseNumber = &v
	}
	if v, ok := raw["parent_id"].(string); ok && v != "" {
		pid, err := uuid.Parse(v)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid parent_id"})
			return
		}
		input.ParentID = &pid
	}
	if v, ok := raw["client_number"].(string); ok && v != "" {
		input.ClientNumber = &v
	}
	if v, ok := raw["matter_number"].(string); ok && v != "" {
		input.MatterNumber = &v
	}
	if v, ok := raw["netdocuments_url"].(string); ok && v != "" {
		input.NetDocumentsURL = &v
	}
	if v, ok := raw["instance_level"].(string); ok {
		// Empty string is the explicit "clear" sentinel for the
		// service layer (nullableInstanceLevel writes NULL).
		input.InstanceLevel = &v
	}
	p, err := dbSvc.projects.Create(r.Context(), uid, input)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusCreated, p)
}

// GET /api/projects/{id}
func handleGetProject(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
	}
	p, err := dbSvc.projects.GetByID(r.Context(), uid, id)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, p)
}

// GET /api/projects/{id}/children — direct children.
func handleListProjectChildren(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
	}
	rows, err := dbSvc.projects.ListChildren(r.Context(), uid, id)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// GET /api/projects/tree — nested tree of every visible Project. Each node
// carries open/overdue deadline counts and embedded children so the UI can
// render the full hierarchy in one round-trip. Visibility-scoped.
//
// Query parameters (all optional, additive):
//   ?scope=all|mine|pinned             — chip-driven scope (default "all")
//   ?status=active,archived,closed     — status whitelist (CSV; default = no narrowing)
//   ?type=client,litigation,patent,case,project,other — type whitelist
//   ?has_open_deadlines=true|false     — narrow by deadline activity
//   ?q=<term>                          — search title / reference / clientmatter
//   ?subtree_counts=true|false         — populate *_subtree fields (default true)
//
// Zero query string preserves the legacy behaviour for back-compat (existing
// callers that just want every visible project).
func handleGetProjectsTree(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}

	q := r.URL.Query()
	opts := services.BuildTreeOptions{
		IncludeSubtreeCounts: parseBoolQuery(q.Get("subtree_counts"), true),
		SearchTerm:           q.Get("q"),
		StatusIn:             splitCSV(q.Get("status")),
		TypeIn:               splitCSV(q.Get("type")),
	}
	switch q.Get("scope") {
	case "mine":
		opts.Scope = services.ScopeMine
	case "pinned":
		opts.Scope = services.ScopePinned
	}
	if v := q.Get("has_open_deadlines"); v != "" {
		b := parseBoolQuery(v, false)
		opts.HasOpenDeadlines = &b
	}

	// Pin set is needed when the response carries `pinned: bool` per node
	// (always, when PinService is wired) AND when scope=pinned narrows.
	if dbSvc.pin != nil {
		set, err := dbSvc.pin.PinnedSet(r.Context(), uid)
		if err != nil {
			writeServiceError(w, err)
			return
		}
		opts.PinnedSet = set
	} else if opts.Scope == services.ScopePinned {
		// scope=pinned without PinService can never have hits.
		writeJSON(w, http.StatusOK, []any{})
		return
	}

	tree, err := dbSvc.projects.BuildTreeWithOptions(r.Context(), uid, opts)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, tree)
}

// parseBoolQuery accepts true/false/1/0/yes/no/on/off (case-insensitive).
// Falls back to def for empty / unrecognised input.
func parseBoolQuery(v string, def bool) bool {
	switch v {
	case "true", "1", "yes", "on":
		return true
	case "false", "0", "no", "off":
		return false
	default:
		return def
	}
}

// GET /api/projects/cards-preview — per-project event rollups for the
// Cards view. Returns a flat list of {project_id, next_events,
// recent_verlauf, team_initials, team_count, last_activity_at} for every
// project the user can see (or the subset given via ?ids=<csv-of-uuids>).
//
// Visibility-scoped server-side. Caller (Cards mode) lazy-fetches batches
// via IntersectionObserver.
func handleProjectsCardsPreview(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}

	var ids []uuid.UUID
	if raw := r.URL.Query().Get("ids"); raw != "" {
		for _, s := range splitCSV(raw) {
			u, err := uuid.Parse(s)
			if err != nil {
				writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid uuid in ?ids"})
				return
			}
			ids = append(ids, u)
		}
	}

	previews, err := dbSvc.projects.CardsPreview(r.Context(), uid, ids)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	// Flat array for JSON; the map order is irrelevant to the client (it
	// keys on project_id when stitching to its tree-id list).
	out := make([]*services.ProjectCardPreview, 0, len(previews))
	for _, p := range previews {
		out = append(out, p)
	}
	writeJSON(w, http.StatusOK, out)
}

// splitCSV splits a comma-separated query value into trimmed non-empty
// tokens. Empty input → nil so callers can branch on `len(out) > 0`.
func splitCSV(s string) []string {
	if s == "" {
		return nil
	}
	parts := strings.Split(s, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p != "" {
			out = append(out, p)
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

// GET /api/projects/{id}/tree — full subtree depth-first (path-ordered).
func handleGetProjectTree(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
	}
	rows, err := dbSvc.projects.GetTree(r.Context(), uid, id)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// GET /api/projects/{id}/ancestors — ancestor chain for breadcrumbs.
func handleListProjectAncestors(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
	}
	rows, err := dbSvc.projects.ListAncestors(r.Context(), uid, id)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// PATCH /api/projects/{id}
func handleUpdateProject(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.UpdateProjectInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	p, err := dbSvc.projects.Update(r.Context(), uid, id, input)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, p)
}

// DELETE /api/projects/{id}
func handleDeleteProject(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.projects.Delete(r.Context(), uid, id); err != nil {
		writeServiceError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// GET /api/projects/{id}/events — audit trail with cursor pagination.
func handleListProjectEvents(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
	}
	q := r.URL.Query()
	var before *uuid.UUID
	if b := q.Get("before"); b != "" {
		bu, err := uuid.Parse(b)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid before cursor"})
			return
		}
		before = &bu
	}
	limit := 0
	if l := q.Get("limit"); l != "" {
		n, err := strconv.Atoi(l)
		if err != nil || n < 0 {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid limit"})
			return
		}
		limit = n
	}
	directOnly := parseDirectOnly(q.Get("direct_only"))
	rows, err := dbSvc.projects.ListEvents(r.Context(), uid, id, before, limit, directOnly)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// GET /api/projects/{id}/parties
func handleListParties(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
	}
	rows, err := dbSvc.parties.ListForProject(r.Context(), uid, id)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// POST /api/projects/{id}/parties
func handleCreateParty(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.CreatePartyInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	p, err := dbSvc.parties.Create(r.Context(), uid, id, input)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusCreated, p)
}

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