package handlers

// docforge template authoring handlers (t-paliad-349 slice 6).
//
// The admin-only authoring surface: upload a base .docx, see it rendered as
// run-addressable text, place {{variable}} slots into it, and save the
// result as a reusable template. Backed by docforge.TemplateStore
// (Postgres bytea carrier) + the docx authoring engine
// (ImportForAuthoring / InjectSlot).
//
// Endpoints (all under adminGate — templates are firm-shared, admin-
// authored, like submission_bases):
//   GET  /api/admin/templates              — catalog list
//   POST /api/admin/templates              — multipart upload → create v1
//   GET  /api/admin/templates/{id}         — authoring view (preview+slots)
//   POST /api/admin/templates/{id}/slots   — place a slot → new version
//
// Slot placement creates a new template version (immutable snapshot) per
// placement. That keeps the snapshot guarantee simple; batching a whole
// authoring session into one version on an explicit "save" is a documented
// future refinement (it trades the version-per-slot churn for a client- or
// session-held draft carrier).
//
// VERIFICATION CEILING: the live upload→render→select→inject→save flow
// needs the app running with DATABASE_URL + Supabase auth + Playwright; it
// is verified post-merge. The docx surgery (ImportForAuthoring/InjectSlot)
// and the store are unit/live-tested independently.

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

	"mgit.msbls.de/m/paliad/internal/branding"
	"mgit.msbls.de/m/paliad/pkg/docforge"
	"mgit.msbls.de/m/paliad/pkg/docforge/docx"
)

// maxTemplateUpload bounds an uploaded .docx. Templates are firm letterhead
// + chrome — tens of KB in practice; 10 MB is a generous ceiling.
const maxTemplateUpload = 10 << 20

type templateMetaJSON struct {
	ID           string `json:"id"`
	Slug         string `json:"slug,omitempty"`
	NameDE       string `json:"name_de"`
	NameEN       string `json:"name_en"`
	Kind         string `json:"kind"`
	SourceFormat string `json:"source_format"`
	Firm         string `json:"firm,omitempty"`
	IsActive     bool   `json:"is_active"`
	Version      int    `json:"version"`
	VersionID    string `json:"version_id,omitempty"`
}

type templateSlotJSON struct {
	Key        string `json:"key"`
	Anchor     string `json:"anchor"`
	Label      string `json:"label,omitempty"`
	OrderIndex int    `json:"order_index"`
}

type authoringViewJSON struct {
	Template    templateMetaJSON   `json:"template"`
	PreviewHTML string             `json:"preview_html"`
	Slots       []templateSlotJSON `json:"slots"`
}

func metaJSON(m docforge.TemplateMeta) templateMetaJSON {
	return templateMetaJSON{
		ID: m.ID, Slug: m.Slug, NameDE: m.NameDE, NameEN: m.NameEN,
		Kind: m.Kind, SourceFormat: m.SourceFormat, Firm: m.Firm,
		IsActive: m.IsActive, Version: m.Version, VersionID: m.VersionID,
	}
}

func slotsJSON(slots []docforge.TemplateSlot) []templateSlotJSON {
	out := make([]templateSlotJSON, 0, len(slots))
	for _, s := range slots {
		out = append(out, templateSlotJSON{Key: s.Key, Anchor: s.Anchor, Label: s.Label, OrderIndex: s.OrderIndex})
	}
	return out
}

// writeTemplateError maps docforge's not-found sentinel to 404 and falls
// back to the shared service-error mapper.
func writeTemplateError(w http.ResponseWriter, err error) {
	if errors.Is(err, docforge.ErrTemplateNotFound) {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "template not found"})
		return
	}
	writeServiceError(w, err)
}

func requireTemplateStore(w http.ResponseWriter) bool {
	if !requireDB(w) {
		return false
	}
	if dbSvc.templateStore == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "template store not configured"})
		return false
	}
	return true
}

// handleTemplatesAuthoringPage serves the authoring page shell. The client
// bundle hydrates the list, upload, preview, palette, and slots.
func handleTemplatesAuthoringPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/templates-authoring.html")
}

// handleListTemplates backs GET /api/admin/templates.
func handleListTemplates(w http.ResponseWriter, r *http.Request) {
	if !requireTemplateStore(w) {
		return
	}
	if _, ok := requireUser(w, r); !ok {
		return
	}
	metas, err := dbSvc.templateStore.List(r.Context(), docforge.TemplateFilter{ActiveOnly: true})
	if err != nil {
		writeTemplateError(w, err)
		return
	}
	out := make([]templateMetaJSON, 0, len(metas))
	for _, m := range metas {
		out = append(out, metaJSON(m))
	}
	writeJSON(w, http.StatusOK, map[string]any{"templates": out})
}

// handlePickerTemplates backs GET /api/templates — the firm-shared catalog
// any authenticated lawyer reads to pick an uploaded template for
// generation (t-paliad-349 slice 7). Unlike the admin list it filters by
// firm (the deployment's branding firm + firm-agnostic templates), matching
// the submission_bases picker contract. Metadata only — no carrier bytes.
func handlePickerTemplates(w http.ResponseWriter, r *http.Request) {
	if !requireTemplateStore(w) {
		return
	}
	if _, ok := requireUser(w, r); !ok {
		return
	}
	metas, err := dbSvc.templateStore.List(r.Context(),
		docforge.TemplateFilter{Firm: branding.Name, ActiveOnly: true})
	if err != nil {
		writeTemplateError(w, err)
		return
	}
	out := make([]templateMetaJSON, 0, len(metas))
	for _, m := range metas {
		out = append(out, metaJSON(m))
	}
	writeJSON(w, http.StatusOK, map[string]any{"templates": out})
}

// handleUploadTemplate backs POST /api/admin/templates (multipart). Reads
// the uploaded .docx, validates it parses, detects any slots already in it,
// and creates the template at version 1.
func handleUploadTemplate(w http.ResponseWriter, r *http.Request) {
	if !requireTemplateStore(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if err := r.ParseMultipartForm(maxTemplateUpload); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid multipart form"})
		return
	}
	file, _, err := r.FormFile("file")
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "file field required"})
		return
	}
	defer file.Close()
	carrier, err := io.ReadAll(io.LimitReader(file, maxTemplateUpload))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "could not read uploaded file"})
		return
	}
	nameDE := r.FormValue("name_de")
	nameEN := r.FormValue("name_en")
	if nameDE == "" || nameEN == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name_de and name_en required"})
		return
	}

	// Validate + detect existing slots before persisting.
	view, err := docx.ImportForAuthoring(carrier)
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "not a parseable .docx: " + err.Error()})
		return
	}

	tmpl, err := dbSvc.templateStore.Create(r.Context(),
		docforge.TemplateMetaInput{
			Slug:      r.FormValue("slug"),
			NameDE:    nameDE,
			NameEN:    nameEN,
			Firm:      r.FormValue("firm"),
			CreatedBy: uid.String(),
		},
		docforge.TemplateVersionInput{
			CarrierBytes: carrier,
			Slots:        view.Slots,
			CreatedBy:    uid.String(),
		})
	if err != nil {
		writeTemplateError(w, err)
		return
	}
	writeJSON(w, http.StatusCreated, authoringViewJSON{
		Template:    metaJSON(tmpl.TemplateMeta),
		PreviewHTML: view.PreviewHTML,
		Slots:       slotsJSON(tmpl.Slots),
	})
}

// handleGetTemplateAuthoring backs GET /api/admin/templates/{id} — the
// authoring view: current carrier rendered run-addressable + its slots.
func handleGetTemplateAuthoring(w http.ResponseWriter, r *http.Request) {
	if !requireTemplateStore(w) {
		return
	}
	if _, ok := requireUser(w, r); !ok {
		return
	}
	tmpl, err := dbSvc.templateStore.Get(r.Context(), r.PathValue("id"))
	if err != nil {
		writeTemplateError(w, err)
		return
	}
	view, err := docx.ImportForAuthoring(tmpl.CarrierBytes)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "stored carrier failed to parse: " + err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, authoringViewJSON{
		Template:    metaJSON(tmpl.TemplateMeta),
		PreviewHTML: view.PreviewHTML,
		Slots:       slotsJSON(view.Slots),
	})
}

type placeSlotInput struct {
	RunIndex     int    `json:"run_index"`
	SelectedText string `json:"selected_text"`
	SlotKey      string `json:"slot_key"`
}

// handlePlaceTemplateSlot backs POST /api/admin/templates/{id}/slots —
// inject a slot at the selection and persist as a new version.
func handlePlaceTemplateSlot(w http.ResponseWriter, r *http.Request) {
	if !requireTemplateStore(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	var in placeSlotInput
	if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"})
		return
	}
	id := r.PathValue("id")
	tmpl, err := dbSvc.templateStore.Get(r.Context(), id)
	if err != nil {
		writeTemplateError(w, err)
		return
	}

	newCarrier, err := docx.InjectSlot(tmpl.CarrierBytes, in.RunIndex, in.SelectedText, in.SlotKey)
	if err != nil {
		// Injection failures are client-fixable (bad selection / key).
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	// Re-detect slots from the new carrier so template_slots mirrors the
	// carrier's actual {{tokens}} (single source of truth).
	newView, err := docx.ImportForAuthoring(newCarrier)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("post-inject parse: %v", err)})
		return
	}
	updated, err := dbSvc.templateStore.AddVersion(r.Context(), id,
		docforge.TemplateVersionInput{
			CarrierBytes: newCarrier,
			Stylemap:     tmpl.Stylemap,
			Slots:        newView.Slots,
			CreatedBy:    uid.String(),
		})
	if err != nil {
		writeTemplateError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, authoringViewJSON{
		Template:    metaJSON(updated.TemplateMeta),
		PreviewHTML: newView.PreviewHTML,
		Slots:       slotsJSON(updated.Slots),
	})
}
