package handlers

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

	"github.com/google/uuid"

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

// U4 (m/paliad#151) — legacy /tools/fristenrechner and
// /tools/verfahrensablauf folded into /tools/procedures via hard 301
// redirects. Per m's Q11 divergence in the design (no 2-week dual-ship
// window), bookmarks resolve via Location preservation of query params;
// no `?legacy=1` escape, no in-product affordance points back at the
// retired URLs after the merge.

func redirectToProcedures(w http.ResponseWriter, r *http.Request) {
	loc := "/tools/procedures"
	if raw := r.URL.RawQuery; raw != "" {
		loc += "?" + raw
	}
	http.Redirect(w, r, loc, http.StatusMovedPermanently)
}

// handleFristenrechnerPage — kept as a registration name for the legacy
// URL so bookmarks (and the existing Sidebar history a former user may
// have cached) keep resolving. 301s to /tools/procedures.
func handleFristenrechnerPage(w http.ResponseWriter, r *http.Request) {
	redirectToProcedures(w, r)
}

// handleVerfahrensablaufPage — symmetrical 301 to /tools/procedures.
func handleVerfahrensablaufPage(w http.ResponseWriter, r *http.Request) {
	redirectToProcedures(w, r)
}

// Unified procedural-events tool page (m/paliad#151, design
// docs/design-unified-procedural-events-tool-2026-05-27.md). Consolidates
// Fristenrechner Mode A + Mode B + result + Verfahrensablauf into a
// single surface at /tools/procedures. No DB dependency — the page
// itself is static HTML; per-tab data flows over the existing
// /api/tools/fristenrechner/* endpoints.
func handleProceduresPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/procedures.html")
}

// POST /api/tools/fristenrechner — calculate the UI timeline for a proceeding.
//
// Phase C: routes through FristenrechnerService which pulls rules from
// paliad.deadline_rules. When DATABASE_URL is unset, returns 503; the page
// itself still renders because it's static HTML.
func handleFristenrechnerAPI(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.fristenrechner == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "Fristenrechner ist vorübergehend nicht verfügbar (keine Datenbank).",
		})
		return
	}
	var req struct {
		ProceedingType  string            `json:"proceedingType"`
		TriggerDate     string            `json:"triggerDate"`
		PriorityDate    string            `json:"priorityDate,omitempty"`
		Flags           []string          `json:"flags,omitempty"`
		AnchorOverrides map[string]string `json:"anchorOverrides,omitempty"`
		CourtID         string            `json:"courtId,omitempty"`
		// t-paliad-265: per-event-card choices. Two parallel inputs:
		// - ProjectID lets the server pull persisted choices from
		//   paliad.project_event_choices (project-bound /tools/fristenrechner).
		// - PerCardChoices lets the unbound /tools/verfahrensablauf
		//   send an inline-CSV-decoded list straight off the URL
		//   without persisting. When both are present the inline list
		//   wins (what-if exploration overrides the saved state).
		ProjectID       string                                  `json:"projectId,omitempty"`
		PerCardChoices  []services.UpsertEventChoiceInput       `json:"perCardChoices,omitempty"`
		// t-paliad-290 (m/paliad#122): re-surface previously-hidden
		// optional cards. When true the calculator marks skipped rows
		// with UIDeadline.IsHidden instead of dropping them; descendants
		// stay in the result list. Default false preserves the legacy
		// suppression. HiddenCount on the response is independent.
		IncludeHidden bool `json:"includeHidden,omitempty"`
		// Slice B1 / m/paliad#124 §18.1: narrows the unified UPC
		// Berufung (upc.apl) timeline to the rule subset whose
		// applies_to_target contains the requested slug. Empty = no
		// filter. Valid values: endentscheidung | kostenentscheidung
		// | anordnung | schadensbemessung | bucheinsicht. Unknown
		// slugs are silently dropped (no filter) so a stale frontend
		// chip doesn't 400 the request.
		AppealTarget string `json:"appealTarget,omitempty"`
		// t-paliad-348 / yoUPC#178 — surface the engine's two new
		// CalcOptions axes to the HTTP boundary:
		//
		//   IncludeOptional: when true, priority='optional' rules
		//     surface on the timeline. Default false matches the
		//     engine's default (mandatory backbone only).
		//   TriggerEventAnchors: per-event-code anchor dates the
		//     engine consults for rules carrying trigger_event_id.
		//     When a rule's anchor is absent the engine renders the
		//     rule as IsConditional rather than fabricating a date
		//     off the proceeding's trigger date.
		IncludeOptional     bool              `json:"includeOptional,omitempty"`
		TriggerEventAnchors map[string]string `json:"triggerEventAnchors,omitempty"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage"})
		return
	}
	if req.ProceedingType == "" || req.TriggerDate == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "proceedingType und triggerDate sind erforderlich"})
		return
	}

	// Fold per-card choices into the CalcOptions addendum. The inline
	// PerCardChoices wins over the persisted ProjectID lookup when both
	// are non-empty.
	var addendum services.CalcOptionsAddendum
	if len(req.PerCardChoices) > 0 {
		choices := make([]models.ProjectEventChoice, 0, len(req.PerCardChoices))
		for _, c := range req.PerCardChoices {
			choices = append(choices, models.ProjectEventChoice{
				SubmissionCode: c.SubmissionCode,
				ChoiceKind:     c.ChoiceKind,
				ChoiceValue:    c.ChoiceValue,
			})
		}
		addendum = services.ToCalcOptionsAddendum(choices)
	} else if req.ProjectID != "" && dbSvc.eventChoice != nil {
		if pid, err := uuid.Parse(req.ProjectID); err == nil {
			if uid, ok := requireUser(w, r); ok {
				if choices, err := dbSvc.eventChoice.ListForProject(r.Context(), uid, pid); err == nil {
					addendum = services.ToCalcOptionsAddendum(choices)
				}
				// Visibility-filtered lookup: a non-visible project
				// returns ErrNotVisible from ListForProject; in that
				// case we project without per-card overlays rather
				// than 404 — the timeline itself is non-PII data.
			}
		}
	}

	resp, err := dbSvc.fristenrechner.Calculate(r.Context(), req.ProceedingType, req.TriggerDate, services.CalcOptions{
		PriorityDateStr:     req.PriorityDate,
		Flags:               req.Flags,
		AnchorOverrides:     req.AnchorOverrides,
		CourtID:             req.CourtID,
		PerCardAppellant:    addendum.PerCardAppellant,
		SkipRules:           addendum.SkipRules,
		IncludeCCRFor:       addendum.IncludeCCRFor,
		IncludeHidden:       req.IncludeHidden,
		AppealTarget:        req.AppealTarget,
		IncludeOptional:     req.IncludeOptional,
		TriggerEventAnchors: req.TriggerEventAnchors,
	})
	if err != nil {
		if errors.Is(err, services.ErrUnknownProceedingType) {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unbekannter Verfahrenstyp: " + req.ProceedingType})
			return
		}
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, resp)
}

// POST /api/tools/fristenrechner/calculate-rule — single-rule calc for
// the v4 (t-paliad-136 Phase B) result-card click flow.
//
// Body: { ruleId? } OR { proceedingCode, ruleLocalCode }, plus
//       triggerDate (YYYY-MM-DD, required) and flags? (string array,
//       optional condition_flag inputs).
//
// Returns a RuleCalculation (see services.RuleCalculation) — the rule
// metadata + computed dueDate / originalDate / adjustmentReason. Used by
// the result-card calc panel; distinct from the full-timeline endpoint
// at POST /api/tools/fristenrechner.
func handleFristenrechnerCalculateRule(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.fristenrechner == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "Fristenrechner ist vorübergehend nicht verfügbar (keine Datenbank).",
		})
		return
	}
	var req struct {
		RuleID         string   `json:"ruleId"`
		ProceedingCode string   `json:"proceedingCode"`
		RuleLocalCode  string   `json:"ruleLocalCode"`
		TriggerDate    string   `json:"triggerDate"`
		Flags          []string `json:"flags,omitempty"`
		CourtID        string   `json:"courtId,omitempty"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage"})
		return
	}
	if req.TriggerDate == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "triggerDate ist erforderlich"})
		return
	}
	if req.RuleID == "" && (req.ProceedingCode == "" || req.RuleLocalCode == "") {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "Entweder ruleId oder (proceedingCode + ruleLocalCode) ist erforderlich",
		})
		return
	}

	resp, err := dbSvc.fristenrechner.CalculateRule(r.Context(), services.CalcRuleParams{
		RuleID:         req.RuleID,
		ProceedingCode: req.ProceedingCode,
		RuleLocalCode:  req.RuleLocalCode,
		TriggerDate:    req.TriggerDate,
		Flags:          req.Flags,
		CourtID:        req.CourtID,
	})
	if err != nil {
		switch {
		case errors.Is(err, services.ErrUnknownRule):
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unbekannte Regel"})
		case errors.Is(err, services.ErrUnknownProceedingType):
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unbekannter Verfahrenstyp: " + req.ProceedingCode})
		default:
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		}
		return
	}
	writeJSON(w, http.StatusOK, resp)
}

// GET /api/tools/proceeding-types — metadata list for the wizard buttons.
// Returns 503 with an empty array when DATABASE_URL is unset so the page
// still renders (buttons are server-rendered from tsx and don't depend on
// this endpoint for existence, only for dynamic list updates).
//
// Optional query params (Fristenrechner overhaul S3, m/paliad#146):
//   jurisdiction  - "UPC" | "DE" | "EPA" | "DPMA". Narrows the chip
//                   pool to one jurisdiction. Empty = any.
//   kind          - "proceeding" | "phase" | "side_action" | "meta".
//                   Narrows to one structural kind from the taxonomy
//                   cleanup (m/paliad#147, mig 153). Mode A passes
//                   "proceeding" to exclude phase / side_action / meta
//                   rows. Empty = any.
func handleProceedingTypes(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.fristenrechner == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "Verfahrenstypen vorübergehend nicht verfügbar (keine Datenbank).",
		})
		return
	}
	opts := services.ProceedingListOptions{
		Jurisdiction: strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("jurisdiction"))),
		Kind:         strings.TrimSpace(r.URL.Query().Get("kind")),
		EventKind:    strings.TrimSpace(r.URL.Query().Get("event_kind")),
	}
	types, err := dbSvc.fristenrechner.ListProceedings(r.Context(), opts)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "konnte Verfahrenstypen nicht laden"})
		return
	}
	writeJSON(w, http.StatusOK, types)
}

// GET /api/tools/trigger-events — list active UPC trigger events for the
// "Was kommt nach…" mode picker. Sorted alphabetically by name.
func handleTriggerEventsList(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.eventDeadline == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "Trigger-Ereignisse vorübergehend nicht verfügbar (keine Datenbank).",
		})
		return
	}
	events, err := dbSvc.eventDeadline.ListTriggerEvents(r.Context())
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "konnte Trigger-Ereignisse nicht laden"})
		return
	}
	writeJSON(w, http.StatusOK, events)
}

// POST /api/tools/event-deadlines — compute all deadlines flowing from a
// trigger event + date. Body: {"triggerEventId": <int>, "triggerDate": "YYYY-MM-DD"}.
//
// DEPRECATED (m/paliad#149 Phase 2 P4 partial, t-paliad-331). This route
// serves the 73 orphan globals (sequencing_rules with proceeding_type_id
// IS NULL, addressed only via trigger_event_id). The route is held live
// until those 73 are reparented onto real proceeding-type chains via
// /admin/procedural-events (editorial work; tracked separately).
//
// Once the orphan count hits zero, the planned final-P4 lands:
//   - DROP TABLE paliad.trigger_events
//   - ALTER TABLE paliad.sequencing_rules DROP COLUMN trigger_event_id
//   - remove this handler + EventDeadlineService + the 5 read sites
//     enumerated in the design (deadline_rule_service.go:226,
//     event_deadline_service.go:79+244, event_type_service.go:40+414,
//     export_service.go:1680, cmd/gen-upc-snapshot/main.go:185-202).
//
// The Deprecation + Sunset response headers below let callers see the
// signal without breaking — see RFC 8594 / RFC 9745.
func handleEventDeadlinesCalculate(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Deprecation", "true")
	w.Header().Set("Link", `<https://mgit.msbls.de/m/paliad/issues/149>; rel="deprecation"; type="text/html"`)
	if dbSvc == nil || dbSvc.eventDeadline == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "Fristenrechner ist vorübergehend nicht verfügbar (keine Datenbank).",
		})
		return
	}
	var req struct {
		TriggerEventID int64  `json:"triggerEventId"`
		TriggerDate    string `json:"triggerDate"`
		CourtID        string `json:"courtId"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage"})
		return
	}
	if req.TriggerEventID <= 0 || req.TriggerDate == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "triggerEventId und triggerDate sind erforderlich"})
		return
	}
	resp, err := dbSvc.eventDeadline.Calculate(r.Context(), req.TriggerEventID, req.TriggerDate, req.CourtID)
	if err != nil {
		if errors.Is(err, services.ErrUnknownTriggerEvent) {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unbekanntes Trigger-Ereignis"})
			return
		}
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, resp)
}

// GET /api/tools/courts — list active courts for the Fristenrechner court
// picker. Optional ?courtType=UPC-LD filter narrows to a single tier so the
// UI can render only the courts compatible with the selected proceeding.
// Returns the deadline-computation slice (id, code, names, country, regime,
// court_type, sort_order) — NOT the full Gerichtsverzeichnis catalog. The
// rich addresses / phone / languages payload still lives at /api/courts.
func handleCourtsList(w http.ResponseWriter, r *http.Request) {
	if dbSvc == nil || dbSvc.courts == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "Gerichte vorübergehend nicht verfügbar (keine Datenbank).",
		})
		return
	}
	courtType := r.URL.Query().Get("courtType")
	var (
		courts []services.Court
		err    error
	)
	if courtType != "" {
		courts, err = dbSvc.courts.ByCourtType(courtType)
	} else {
		courts, err = dbSvc.courts.All()
	}
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "konnte Gerichte nicht laden"})
		return
	}
	writeJSON(w, http.StatusOK, courts)
}
