package handlers

// Approval workflow HTTP endpoints (t-paliad-138).
//
// Three groups of routes:
//
//   1. Policy CRUD (admin-only, gated at the route layer):
//      GET    /api/projects/{id}/approval-policies
//      PUT    /api/projects/{id}/approval-policies/{entity_type}/{lifecycle}
//      DELETE /api/projects/{id}/approval-policies/{entity_type}/{lifecycle}
//
//   2. Inbox (any authenticated user — the service-layer query gates by
//      project visibility + approver eligibility):
//      GET /api/inbox/pending-mine     — requests I can approve
//      GET /api/inbox/mine             — requests I submitted
//      GET /api/inbox/count            — bell badge count
//      GET /api/approval-requests/{id} — one request hydrated
//
//   3. Decisions (any authenticated user — service layer gates the action):
//      POST /api/approval-requests/{id}/approve
//      POST /api/approval-requests/{id}/reject
//      POST /api/approval-requests/{id}/revoke

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

	"github.com/google/uuid"

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

// ============================================================================
// Policy CRUD (admin only — gated by RequireAdminFunc at registration).
// ============================================================================

// GET /api/projects/{id}/approval-policies
func handleListApprovalPolicies(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	if _, ok := requireUser(w, r); !ok {
		return
	}
	projectID, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project id"})
		return
	}
	rows, err := dbSvc.approval.ListProjectPolicies(r.Context(), projectID)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if rows == nil {
		rows = []models.ApprovalPolicy{} // ensure JSON [] not null
	}
	writeJSON(w, http.StatusOK, rows)
}

// PUT /api/projects/{id}/approval-policies/{entity_type}/{lifecycle}
//
// Body (split-grammar, t-paliad-160): {"requires_approval": bool, "min_role": "associate"|null}
// Body (legacy, dual-read window):    {"required_role": "associate"|"none"}
//
// Semantics: upsert. Replaces any existing row for the same
// (project, entity_type, lifecycle) tuple.
func handlePutApprovalPolicy(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 project id"})
		return
	}
	entityType := r.PathValue("entity_type")
	lifecycle := r.PathValue("lifecycle")
	body, err := decodePolicyBody(r)
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	var policy *models.ApprovalPolicy
	if body.useSplit {
		policy, err = dbSvc.approval.UpsertProjectPolicySplit(r.Context(), uid, projectID, entityType, lifecycle, body.requiresApproval, body.minRole)
	} else {
		policy, err = dbSvc.approval.UpsertProjectPolicy(r.Context(), uid, projectID, entityType, lifecycle, body.requiredRole)
	}
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, policy)
}

// policyUpsertBody is the parsed payload for the policy PUT endpoints,
// supporting both the new split-grammar shape and the legacy single-string
// shape during the M1 dual-read window. Exactly one path is taken:
//   - useSplit=true  → call *Split with (requiresApproval, minRole).
//   - useSplit=false → call legacy with requiredRole.
type policyUpsertBody struct {
	useSplit         bool
	requiresApproval bool
	minRole          *string
	requiredRole     string
}

// decodePolicyBody parses either split-grammar or legacy payload. The
// presence of the "requires_approval" key wins — explicit absence falls
// back to the legacy required_role path.
func decodePolicyBody(r *http.Request) (policyUpsertBody, error) {
	var raw map[string]json.RawMessage
	if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
		return policyUpsertBody{}, errors.New("invalid JSON")
	}
	if v, ok := raw["requires_approval"]; ok {
		var b policyUpsertBody
		b.useSplit = true
		if err := json.Unmarshal(v, &b.requiresApproval); err != nil {
			return policyUpsertBody{}, errors.New("requires_approval must be boolean")
		}
		if mr, ok := raw["min_role"]; ok && string(mr) != "null" {
			var s string
			if err := json.Unmarshal(mr, &s); err != nil {
				return policyUpsertBody{}, errors.New("min_role must be string or null")
			}
			b.minRole = &s
		}
		return b, nil
	}
	if v, ok := raw["required_role"]; ok {
		var s string
		if err := json.Unmarshal(v, &s); err != nil {
			return policyUpsertBody{}, errors.New("required_role must be string")
		}
		return policyUpsertBody{useSplit: false, requiredRole: s}, nil
	}
	return policyUpsertBody{}, errors.New("requires_approval or required_role required")
}

// DELETE /api/projects/{id}/approval-policies/{entity_type}/{lifecycle}
//
// Removes one policy row, reverting that lifecycle event back to the
// no-approval-needed default.
func handleDeleteApprovalPolicy(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 project id"})
		return
	}
	entityType := r.PathValue("entity_type")
	lifecycle := r.PathValue("lifecycle")
	if err := dbSvc.approval.DeleteProjectPolicy(r.Context(), uid, projectID, entityType, lifecycle); err != nil {
		writeServiceError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// ============================================================================
// Inbox.
// ============================================================================

// GET /api/inbox/pending-mine — requests I'm qualified to approve.
func handleListInboxPendingMine(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	rows, err := dbSvc.approval.ListPendingForApprover(r.Context(), uid, parseInboxFilter(r))
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if rows == nil {
		rows = []services.ApprovalRequestView{} // ensure JSON [] not null
	}
	writeJSON(w, http.StatusOK, rows)
}

// GET /api/inbox/mine — requests I submitted.
//
// Returns ALL statuses by default (pending, approved, rejected, revoked,
// superseded). Pass ?status=pending to narrow. Empty result is serialised
// as [] not null so the frontend doesn't trip on a null body and crash
// before rendering the empty state (t-paliad-160 §D regression hardening).
func handleListInboxMine(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	rows, err := dbSvc.approval.ListSubmittedByUser(r.Context(), uid, parseInboxFilter(r))
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if rows == nil {
		rows = []services.ApprovalRequestView{} // ensure JSON [] not null
	}
	writeJSON(w, http.StatusOK, rows)
}

// GET /api/inbox/count — bell badge count for the sidebar.
func handleInboxCount(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	n, err := dbSvc.approval.PendingCountForUser(r.Context(), uid)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]int{"count": n})
}

// parseInboxFilter pulls common filter knobs off the query string.
//
// Status / EntityType pass through validation: an unrecognised value is
// silently dropped so a stray ?status=foo from a stale frontend build
// doesn't shadow every row out of the result set (t-paliad-160 §D
// regression hardening — defence-in-depth against the "Meine Anfragen
// is empty" report).
func parseInboxFilter(r *http.Request) services.InboxFilter {
	q := r.URL.Query()
	f := services.InboxFilter{}
	if s := q.Get("status"); isValidInboxStatus(s) {
		f.Status = s
	}
	if e := q.Get("entity_type"); e == services.EntityTypeDeadline || e == services.EntityTypeAppointment {
		f.EntityType = e
	}
	if pid := q.Get("project_id"); pid != "" {
		if id, err := uuid.Parse(pid); err == nil {
			f.ProjectID = &id
		}
	}
	return f
}

// isValidInboxStatus is the allowlist of accepted status filter values.
// Empty string passes through as "no filter".
func isValidInboxStatus(s string) bool {
	switch s {
	case "",
		services.RequestStatusPending,
		services.RequestStatusApproved,
		services.RequestStatusRejected,
		services.RequestStatusRevoked,
		services.RequestStatusSuperseded,
		services.RequestStatusChangesRequested:
		return true
	}
	return false
}

// GET /api/approval-requests/{id} — one hydrated request.
func handleGetApprovalRequest(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	requestID, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request id"})
		return
	}
	row, err := dbSvc.approval.GetRequest(r.Context(), uid, requestID)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if row == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
		return
	}
	writeJSON(w, http.StatusOK, row)
}

// ============================================================================
// Decisions.
// ============================================================================

type approvalDecisionBody struct {
	Note string `json:"note"`
}

// POST /api/approval-requests/{id}/approve
func handleApproveApprovalRequest(w http.ResponseWriter, r *http.Request) {
	handleApprovalDecision(w, r, "approve")
}

// POST /api/approval-requests/{id}/reject
func handleRejectApprovalRequest(w http.ResponseWriter, r *http.Request) {
	handleApprovalDecision(w, r, "reject")
}

// POST /api/approval-requests/{id}/revoke
func handleRevokeApprovalRequest(w http.ResponseWriter, r *http.Request) {
	handleApprovalDecision(w, r, "revoke")
}

// suggestChangesBody is the JSON body for POST /api/approval-requests/{id}/suggest-changes.
// counter_payload is an entity-shaped jsonb of the approver's edited
// values (allowlist enforced server-side); note is the optional free-text
// explanation. The service rejects the call with
// ErrSuggestionRequiresChange when both are no-ops (counter is identical
// to the old row's payload AND note is empty).
type suggestChangesBody struct {
	CounterPayload map[string]any `json:"counter_payload"`
	Note           string         `json:"note"`
}

// POST /api/approval-requests/{id}/suggest-changes — t-paliad-216.
//
// In one transaction: close the pending request as 'changes_requested'
// (with the caller's note + counter_payload on the row), revert the entity
// from pre_image, then spawn a NEW pending approval_request authored by
// the caller carrying the counter_payload. Returns the new request id.
//
// Status mapping (see writeApprovalError → mapApprovalError):
//
//	400 suggestion_requires_change   — counter == old payload AND no note
//	400 suggestion_lifecycle_invalid — old row's lifecycle ∉ (update, complete)
//	403 self_approval_blocked        — caller == old row's requested_by
//	403 not_authorized               — caller doesn't satisfy canApprove
//	404                              — request not found / not visible
//	409 request_not_pending          — old row already decided
//	409 no_qualified_approver        — deadlock on the new row
func handleSuggestChangesApprovalRequest(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	requestID, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request id"})
		return
	}
	var body suggestChangesBody
	if r.Body != nil && r.ContentLength > 0 {
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{
				"code":    "invalid_body",
				"message": "Ungültiger Body.",
			})
			return
		}
	}
	newID, err := dbSvc.approval.SuggestChanges(r.Context(), requestID, uid, body.CounterPayload, body.Note)
	if err != nil {
		writeApprovalError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{
		"status":         "ok",
		"new_request_id": newID.String(),
	})
}

func handleApprovalDecision(w http.ResponseWriter, r *http.Request, action string) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	requestID, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request id"})
		return
	}
	var body approvalDecisionBody
	if r.Body != nil && r.ContentLength > 0 {
		_ = json.NewDecoder(r.Body).Decode(&body) // body is optional
	}

	switch action {
	case "approve":
		err = dbSvc.approval.Approve(r.Context(), requestID, uid, body.Note)
	case "reject":
		err = dbSvc.approval.Reject(r.Context(), requestID, uid, body.Note)
	case "revoke":
		err = dbSvc.approval.Revoke(r.Context(), requestID, uid)
	}
	if err != nil {
		writeApprovalError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

// GET /inbox — server-static page shell. Hydration is purely client-side
// (the bundle calls /api/inbox/pending-mine on load).
func handleInboxPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/inbox.html")
}

// ============================================================================
// t-paliad-154 — admin approval-policy authoring page + APIs.
//
// Most endpoints below register under /api/admin and are admin-gated by the
// outer adminGate(users, ...) wrapper at handlers.go. The form-time hint
// endpoint at /api/projects/{id}/approval-policies/effective is the one
// exception — it's reachable by every authenticated user authoring a
// deadline/appointment so the form can render the "this needs 4-eye"
// banner before they save.
// ============================================================================

// GET /admin/approval-policies — server-static page shell.
func handleAdminApprovalPoliciesPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/admin-approval-policies.html")
}

// GET /api/admin/partner-units/{unit_id}/approval-policies — list one
// partner unit's default policy rows.
func handleListUnitApprovalPolicies(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	if _, ok := requireUser(w, r); !ok {
		return
	}
	unitID, err := uuid.Parse(r.PathValue("unit_id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid unit id"})
		return
	}
	rows, err := dbSvc.approval.ListUnitPolicies(r.Context(), unitID)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if rows == nil {
		rows = []models.ApprovalPolicy{}
	}
	writeJSON(w, http.StatusOK, rows)
}

// PUT /api/admin/partner-units/{unit_id}/approval-policies/{entity_type}/{lifecycle}
//
// Body (split-grammar): {"requires_approval": bool, "min_role": "associate"|null}
// Body (legacy):        {"required_role": "associate"|"none"}
func handlePutUnitApprovalPolicy(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	unitID, err := uuid.Parse(r.PathValue("unit_id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid unit id"})
		return
	}
	entityType := r.PathValue("entity_type")
	lifecycle := r.PathValue("lifecycle")
	body, err := decodePolicyBody(r)
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	var policy *models.ApprovalPolicy
	if body.useSplit {
		policy, err = dbSvc.approval.UpsertUnitPolicySplit(r.Context(), uid, unitID, entityType, lifecycle, body.requiresApproval, body.minRole)
	} else {
		policy, err = dbSvc.approval.UpsertUnitPolicy(r.Context(), uid, unitID, entityType, lifecycle, body.requiredRole)
	}
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, policy)
}

// DELETE /api/admin/partner-units/{unit_id}/approval-policies/{entity_type}/{lifecycle}
func handleDeleteUnitApprovalPolicy(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	unitID, err := uuid.Parse(r.PathValue("unit_id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid unit id"})
		return
	}
	entityType := r.PathValue("entity_type")
	lifecycle := r.PathValue("lifecycle")
	if err := dbSvc.approval.DeleteUnitPolicy(r.Context(), uid, unitID, entityType, lifecycle); err != nil {
		writeServiceError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// GET /api/admin/approval-policies/seeded — has any policy been authored
// firm-wide? Used by /inbox to gate the admin "configure policies" nudge.
func handleApprovalPoliciesSeeded(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	if _, ok := requireUser(w, r); !ok {
		return
	}
	any, err := dbSvc.approval.PoliciesExist(r.Context())
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]bool{"any": any})
}

// GET /api/admin/approval-policies/matrix?project_id=... — 8 effective
// policy rows for one project, with attribution chips.
func handleApprovalPoliciesMatrix(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	if _, ok := requireUser(w, r); !ok {
		return
	}
	pidStr := r.URL.Query().Get("project_id")
	if pidStr == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "project_id required"})
		return
	}
	projectID, err := uuid.Parse(pidStr)
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project_id"})
		return
	}
	rows, err := dbSvc.approval.GetEffectivePoliciesMatrix(r.Context(), projectID)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// POST /api/admin/approval-policies/apply-to-descendants
//
// Body: {"source_project_id": uuid, "target_project_ids": [uuid, ...]}
//
// Copies the source's effective matrix down to every target as
// project-scoped rows. Targets must be actual descendants of source.
func handleApplyMatrixToDescendants(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	var body struct {
		SourceProjectID  uuid.UUID   `json:"source_project_id"`
		TargetProjectIDs []uuid.UUID `json:"target_project_ids"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	if body.SourceProjectID == uuid.Nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "source_project_id required"})
		return
	}
	writes, err := dbSvc.approval.ApplyMatrixToDescendants(r.Context(), uid, body.SourceProjectID, body.TargetProjectIDs)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"writes":  writes,
		"targets": len(body.TargetProjectIDs),
	})
}

// GET /api/projects/{id}/approval-policies/effective?entity_type=&lifecycle=
//
// Single-cell effective policy lookup. Used by the deadline + appointment
// new/edit forms to render the form-time 4-eye hint above the Speichern
// button (Q13 of the locked design). Reachable by every authenticated user
// (NOT admin-gated) — they need to know their save will trigger an
// approval request.
func handleProjectEffectivePolicy(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	if _, ok := requireUser(w, r); !ok {
		return
	}
	projectID, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project id"})
		return
	}
	q := r.URL.Query()
	entityType := q.Get("entity_type")
	lifecycle := q.Get("lifecycle")
	if entityType == "" || lifecycle == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "entity_type and lifecycle required"})
		return
	}
	row, err := dbSvc.approval.GetEffectivePolicyOne(r.Context(), projectID, entityType, lifecycle)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, row)
}

// writeApprovalError maps approval-flow errors to HTTP status codes. The
// code/message body shape is shared with mapApprovalError so the frontend
// has a single switch on `code` regardless of which endpoint surfaced
// the error (entity mutation vs explicit approve/reject/revoke decision).
func writeApprovalError(w http.ResponseWriter, err error) {
	if mapApprovalError(w, err) {
		return
	}
	writeServiceError(w, err)
}
