package handlers

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

	"github.com/google/uuid"

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

// Admin rule-editor endpoints — Phase 3 Slice 11a (t-paliad-191).
// Every handler in this file is wired through auth.RequireAdminFunc
// in handlers.go, so the handlers themselves assume the caller is a
// global_admin and only validate request shape.
//
// Every write endpoint takes an audit_reason field on the request
// body. The service layer sets paliad.audit_reason in the same tx
// before the UPDATE so mig 079's audit trigger captures the rationale
// forever. Missing reason → 400 (ErrAuditReasonRequired).
//
// Lifecycle invariants live in the service layer: ErrInvalidLifecycleState
// is mapped to 409 Conflict so the editor UI can show a clear "must
// clone first" hint.

// Slice B.5 (t-paliad-305) JSON envelope renames:
//
//   - submission_code → code           (procedural-event identifier)
//   - event_type      → event_kind     (procedural-event taxonomy)
//
// Wire compatibility: every response emits BOTH the legacy and the
// canonical keys for one slice (see Deprecation HTTP header on the
// response). Input bodies accept either name on the request; the
// canonical key wins when both are present.
//
// adminRuleResponse wraps models.DeadlineRule (= litigationplanner.Rule)
// to add the canonical `code` + `event_kind` fields alongside the
// historical `submission_code` + `event_type` already on Rule's tags.
// The embedded *models.DeadlineRule carries every existing tag through
// json.Marshal unchanged; the wrapper only ADDS the two new keys.
//
// ProceedingTypeCode (t-paliad-321) is the joined paliad.proceeding_types.code
// for the row's proceeding_type_id. NULL on event-rooted rules. Lets the
// /admin/procedural-events list disambiguate same-named rules at a glance
// (e.g. "Berufungsbegründung" rows differ only by proceeding code).
type adminRuleResponse struct {
	*models.DeadlineRule
	Code               *string `json:"code,omitempty"`
	EventKind          *string `json:"event_kind,omitempty"`
	ProceedingTypeCode *string `json:"proceeding_type_code,omitempty"`
}

// wrapRuleResponse builds the dual-emit wrapper from a service result.
// Same values, two keys per concept — no semantic change. Pass a non-nil
// ptCode to populate the proceeding_type_code field; nil leaves it
// absent (e.g. on event-rooted rules with NULL proceeding_type_id).
func wrapRuleResponse(r *models.DeadlineRule) adminRuleResponse {
	if r == nil {
		return adminRuleResponse{}
	}
	return adminRuleResponse{
		DeadlineRule: r,
		Code:         r.SubmissionCode,
		EventKind:    r.EventType,
	}
}

// wrapRuleListResponse maps a slice of service results into the
// dual-emit wrapper. Used by the LIST endpoint. ptCodes is an
// optional id → code lookup populated by handleAdminListRules from a
// single batch query against paliad.proceeding_types; nil leaves
// every row's proceeding_type_code empty (the LIST endpoint always
// passes a populated map; other callers don't need it).
func wrapRuleListResponse(rows []models.DeadlineRule, ptCodes map[int]string) []adminRuleResponse {
	out := make([]adminRuleResponse, len(rows))
	for i := range rows {
		out[i] = wrapRuleResponse(&rows[i])
		if ptCodes != nil && rows[i].ProceedingTypeID != nil {
			if code, ok := ptCodes[*rows[i].ProceedingTypeID]; ok {
				out[i].ProceedingTypeCode = &code
			}
		}
	}
	return out
}

// adminRuleDeprecationHeaders writes the IETF "Deprecation" + "Sunset"
// HTTP headers signaling that the legacy `submission_code` /
// `event_type` JSON keys are being retired in favour of `code` /
// `event_kind`. RFC 8594 (Sunset) + draft-ietf-httpapi-deprecation-header.
// Clients should migrate within one slice cycle.
func adminRuleDeprecationHeaders(w http.ResponseWriter) {
	w.Header().Set("Deprecation", `true; key="submission_code,event_type"`)
	w.Header().Set("Link", `<https://mgit.msbls.de/m/paliad/issues/93>; rel="deprecation"`)
}

// GET /admin/api/rules — paginated list with filters.
func handleAdminListRules(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	q := r.URL.Query()
	f := services.ListRulesFilter{
		LifecycleState: q.Get("lifecycle_state"),
		Query:          q.Get("q"),
	}
	if v := q.Get("proceeding_type_id"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid proceeding_type_id"})
			return
		}
		f.ProceedingTypeID = &n
	}
	if v := q.Get("trigger_event_id"); v != "" {
		n, err := strconv.ParseInt(v, 10, 64)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid trigger_event_id"})
			return
		}
		f.TriggerEventID = &n
	}
	if v := q.Get("offset"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil || n < 0 {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid offset"})
			return
		}
		f.Offset = n
	}
	if v := q.Get("limit"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil || n < 0 {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid limit"})
			return
		}
		f.Limit = n
	}
	rows, err := dbSvc.ruleEditor.ListRules(r.Context(), f)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	// t-paliad-321: batch-fetch proceeding_type.code for every rule
	// row that carries a non-NULL proceeding_type_id, so the LIST
	// response can show a Proceeding column without an N+1 join.
	ptCodes, err := dbSvc.ruleEditor.LoadProceedingTypeCodes(r.Context(), rows)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	adminRuleDeprecationHeaders(w)
	writeJSON(w, http.StatusOK, wrapRuleListResponse(rows, ptCodes))
}

// GET /admin/api/rules/{id}
func handleAdminGetRule(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	id, ok := parseRuleID(w, r)
	if !ok {
		return
	}
	row, err := dbSvc.ruleEditor.GetByID(r.Context(), id)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	adminRuleDeprecationHeaders(w)
	writeJSON(w, http.StatusOK, wrapRuleResponse(row))
}

// POST /admin/api/rules — create draft.
func handleAdminCreateRule(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	var body struct {
		services.CreateRuleInput
		Reason string `json:"reason"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	// Slice B.5 (t-paliad-305): accept both legacy + canonical JSON keys.
	body.CreateRuleInput.CoalesceCanonicalKeys()
	row, err := dbSvc.ruleEditor.Create(r.Context(), body.CreateRuleInput, body.Reason)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	adminRuleDeprecationHeaders(w)
	writeJSON(w, http.StatusCreated, wrapRuleResponse(row))
}

// PATCH /admin/api/rules/{id} — partial update of a draft.
func handleAdminPatchRule(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	id, ok := parseRuleID(w, r)
	if !ok {
		return
	}
	var body struct {
		services.RulePatch
		Reason string `json:"reason"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	// Slice B.5 (t-paliad-305): accept both legacy + canonical JSON keys.
	body.RulePatch.CoalesceCanonicalKeys()
	row, err := dbSvc.ruleEditor.UpdateDraft(r.Context(), id, body.RulePatch, body.Reason)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	adminRuleDeprecationHeaders(w)
	writeJSON(w, http.StatusOK, wrapRuleResponse(row))
}

// POST /admin/api/rules/{id}/clone-as-draft
func handleAdminCloneAsDraft(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	id, ok := parseRuleID(w, r)
	if !ok {
		return
	}
	reason, ok := decodeReason(w, r)
	if !ok {
		return
	}
	row, err := dbSvc.ruleEditor.CloneAsDraft(r.Context(), id, reason)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	adminRuleDeprecationHeaders(w)
	writeJSON(w, http.StatusCreated, wrapRuleResponse(row))
}

// POST /admin/api/rules/{id}/publish
func handleAdminPublishRule(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	id, ok := parseRuleID(w, r)
	if !ok {
		return
	}
	reason, ok := decodeReason(w, r)
	if !ok {
		return
	}
	row, err := dbSvc.ruleEditor.Publish(r.Context(), id, reason)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	adminRuleDeprecationHeaders(w)
	writeJSON(w, http.StatusOK, wrapRuleResponse(row))
}

// POST /admin/api/rules/{id}/archive
func handleAdminArchiveRule(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	id, ok := parseRuleID(w, r)
	if !ok {
		return
	}
	reason, ok := decodeReason(w, r)
	if !ok {
		return
	}
	row, err := dbSvc.ruleEditor.Archive(r.Context(), id, reason)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	adminRuleDeprecationHeaders(w)
	writeJSON(w, http.StatusOK, wrapRuleResponse(row))
}

// POST /admin/api/rules/{id}/restore
func handleAdminRestoreRule(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	id, ok := parseRuleID(w, r)
	if !ok {
		return
	}
	reason, ok := decodeReason(w, r)
	if !ok {
		return
	}
	row, err := dbSvc.ruleEditor.Restore(r.Context(), id, reason)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	adminRuleDeprecationHeaders(w)
	writeJSON(w, http.StatusOK, wrapRuleResponse(row))
}

// GET /admin/api/rules/{id}/audit?offset=N&limit=M
func handleAdminGetRuleAudit(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	id, ok := parseRuleID(w, r)
	if !ok {
		return
	}
	offset, limit := 0, 0
	q := r.URL.Query()
	if v := q.Get("offset"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil || n < 0 {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid offset"})
			return
		}
		offset = n
	}
	if v := q.Get("limit"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil || n < 0 {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid limit"})
			return
		}
		limit = n
	}
	rows, err := dbSvc.ruleEditor.ListAudit(r.Context(), id, offset, limit)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// GET /admin/api/rules/{id}/preview?trigger_date=YYYY-MM-DD&flags=a,b&court_id=...
func handleAdminPreviewRule(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil || dbSvc.fristenrechner == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	id, ok := parseRuleID(w, r)
	if !ok {
		return
	}
	q := r.URL.Query()
	triggerDate := q.Get("trigger_date")
	if triggerDate == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "trigger_date required"})
		return
	}
	var flags []string
	if v := q.Get("flags"); v != "" {
		for _, f := range splitCSV(v) {
			if f != "" {
				flags = append(flags, f)
			}
		}
	}
	courtID := q.Get("court_id")
	resp, err := dbSvc.ruleEditor.Preview(r.Context(), dbSvc.fristenrechner, id, triggerDate, flags, courtID)
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, resp)
}

// =============================================================================
// Page handlers — serve the static SPA shells. Auth + admin gate live
// at the route registration in handlers.go.
// =============================================================================

func handleAdminRulesListPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/admin-rules-list.html")
}

func handleAdminRulesEditPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/admin-rules-edit.html")
}

// =============================================================================
// helpers
// =============================================================================

func parseRuleID(w http.ResponseWriter, r *http.Request) (uuid.UUID, bool) {
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return uuid.Nil, false
	}
	return id, true
}

func decodeReason(w http.ResponseWriter, r *http.Request) (string, bool) {
	var body struct {
		Reason string `json:"reason"`
	}
	if r.ContentLength > 0 {
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
			return "", false
		}
	}
	return body.Reason, true
}

// writeRuleEditorError maps the service-level typed errors to HTTP statuses.
// Distinct from writeServiceError (projects path) because the rule
// editor's lifecycle errors map to 409 Conflict, which the project
// service doesn't use.
func writeRuleEditorError(w http.ResponseWriter, err error) {
	switch {
	case errors.Is(err, services.ErrRuleNotFound):
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "rule not found"})
	case errors.Is(err, services.ErrAuditReasonRequired):
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error":   "audit_reason required",
			"message": "Every rule-editor write must include a non-empty `reason` body field.",
		})
	case errors.Is(err, services.ErrInvalidLifecycleState):
		writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
	case errors.Is(err, services.ErrCyclicSpawn):
		writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
	case errors.Is(err, services.ErrOrphanAlreadyResolved):
		writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
	case errors.Is(err, services.ErrOrphanCandidateMismatch):
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
	case errors.Is(err, services.ErrInvalidInput):
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
	default:
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}
}

// =============================================================================
// Orphan-resolution handlers — Slice 11b admin add-on.
// Lists the unresolved rows from paliad.deadline_rule_backfill_orphans
// (mig 089) and lets an admin hand-bind each to one of the matcher's
// candidate rule_ids. The resolve write lands in a single tx via the
// rule editor service so the deadline row + the staging row stay in
// sync; admin-only at the route layer.
// =============================================================================

// GET /admin/api/orphans
func handleAdminListOrphans(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	rows, err := dbSvc.ruleEditor.ListOrphans(r.Context())
	if err != nil {
		writeRuleEditorError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// POST /admin/api/orphans/{id}/resolve  body: {"rule_id": "...", "reason": "..."}
func handleAdminResolveOrphan(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.ruleEditor == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	var body struct {
		RuleID string `json:"rule_id"`
		Reason string `json:"reason"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	ruleID, err := uuid.Parse(body.RuleID)
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid rule_id"})
		return
	}
	if err := dbSvc.ruleEditor.ResolveOrphan(r.Context(), id, ruleID, body.Reason); err != nil {
		writeRuleEditorError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"status": "resolved"})
}

// Slice B.6 (t-paliad-305) — 301 redirect helpers for the legacy
// /admin/rules* paths. New canonical paths live under
// /admin/procedural-events; the redirects keep external bookmarks,
// audit-log entries, and curl scripts working through one
// deprecation cycle.
//
// Three flavours:
//
//   * redirectToProceduralEvents(newPath) — fixed redirect target
//     (used by the parameter-less paths /admin/rules and
//     /admin/api/rules).
//   * redirectToProceduralEventEdit — page path with {id}/edit suffix.
//   * redirectToProceduralEventAPI(suffix) — JSON API paths that carry
//     an {id} and optional suffix (/clone-as-draft, /publish, …).
//
// All emit 301 Moved Permanently — caches and browsers learn the new
// URL once and stop hitting the legacy path. The IETF Deprecation
// header is added so machine clients see the migration signal
// alongside the redirect.

// redirectToProceduralEvents returns an http.HandlerFunc that 301s to
// the supplied destination path. Query string is preserved.
func redirectToProceduralEvents(dst string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		target := dst
		if r.URL.RawQuery != "" {
			target += "?" + r.URL.RawQuery
		}
		w.Header().Set("Deprecation", `true; path="/admin/rules"`)
		w.Header().Set("Link", `</admin/procedural-events>; rel="successor-version"`)
		http.Redirect(w, r, target, http.StatusMovedPermanently)
	}
}

// redirectToProceduralEventEdit 301s GET /admin/rules/{id}/edit →
// /admin/procedural-events/{id}/edit.
func redirectToProceduralEventEdit(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	target := "/admin/procedural-events/" + id + "/edit"
	if r.URL.RawQuery != "" {
		target += "?" + r.URL.RawQuery
	}
	w.Header().Set("Deprecation", `true; path="/admin/rules/{id}/edit"`)
	w.Header().Set("Link", `</admin/procedural-events/{id}/edit>; rel="successor-version"`)
	http.Redirect(w, r, target, http.StatusMovedPermanently)
}

// redirectToProceduralEventAPI 301s /admin/api/rules/{id}[/suffix] →
// /admin/api/procedural-events/{id}[/suffix]. The optional suffix
// covers /clone-as-draft, /publish, /archive, /restore, /audit, /preview.
func redirectToProceduralEventAPI(suffix string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		target := "/admin/api/procedural-events/" + id + suffix
		if r.URL.RawQuery != "" {
			target += "?" + r.URL.RawQuery
		}
		w.Header().Set("Deprecation", `true; path="/admin/api/rules/{id}`+suffix+`"`)
		w.Header().Set("Link", `</admin/api/procedural-events/{id}`+suffix+`>; rel="successor-version"`)
		http.Redirect(w, r, target, http.StatusMovedPermanently)
	}
}
