package handlers

// Composer building-block handlers — t-paliad-315 Slice C.
//
// Two surfaces:
//
//  1. Lawyer-facing picker (any authenticated user):
//       GET  /api/submission-building-blocks?section_key=…&proceeding_family=…&q=…
//       POST /api/submission-building-blocks/{block_id}/insert-into/{section_id}
//
//     The picker list is visibility-tier-filtered (private/team/firm/
//     global) at the service layer. Insert is the paste mechanic
//     ratified by Q2 (m, 2026-05-26): plain text copy of
//     content_md_<lang> into submission_sections.content_md_<lang>.
//     No lineage stamped on the section.
//
//  2. Admin editor (adminGate via auth.RequireAdminFunc):
//       GET    /api/admin/submission-building-blocks
//       POST   /api/admin/submission-building-blocks
//       GET    /api/admin/submission-building-blocks/{block_id}
//       PATCH  /api/admin/submission-building-blocks/{block_id}
//       DELETE /api/admin/submission-building-blocks/{block_id}
//       GET    /api/admin/submission-building-blocks/{block_id}/versions
//       POST   /api/admin/submission-building-blocks/{block_id}/restore/{version_id}
//
//     Plus the page route /admin/submission-building-blocks (list +
//     edit shell, hydrated client-side).

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

	"github.com/google/uuid"

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

// blockJSON is the on-the-wire shape for both the picker and admin
// surfaces.
type buildingBlockJSON struct {
	ID               uuid.UUID  `json:"id"`
	Slug             string     `json:"slug"`
	Firm             *string    `json:"firm,omitempty"`
	SectionKey       string     `json:"section_key"`
	ProceedingFamily *string    `json:"proceeding_family,omitempty"`
	TitleDE          string     `json:"title_de"`
	TitleEN          string     `json:"title_en"`
	DescriptionDE    *string    `json:"description_de,omitempty"`
	DescriptionEN    *string    `json:"description_en,omitempty"`
	ContentMDDE      string     `json:"content_md_de"`
	ContentMDEN      string     `json:"content_md_en"`
	AuthorID         *uuid.UUID `json:"author_id,omitempty"`
	Visibility       string     `json:"visibility"`
	IsPublished      bool       `json:"is_published"`
	CreatedAt        time.Time  `json:"created_at"`
	UpdatedAt        time.Time  `json:"updated_at"`
}

type buildingBlockListResponse struct {
	Blocks []buildingBlockJSON `json:"blocks"`
}

// blockJSONFromService projects services.BuildingBlock into the wire shape.
func blockJSONFromService(b *services.BuildingBlock) buildingBlockJSON {
	return buildingBlockJSON{
		ID:               b.ID,
		Slug:             b.Slug,
		Firm:             b.Firm,
		SectionKey:       b.SectionKey,
		ProceedingFamily: b.ProceedingFamily,
		TitleDE:          b.TitleDE,
		TitleEN:          b.TitleEN,
		DescriptionDE:    b.DescriptionDE,
		DescriptionEN:    b.DescriptionEN,
		ContentMDDE:      b.ContentMDDE,
		ContentMDEN:      b.ContentMDEN,
		AuthorID:         b.AuthorID,
		Visibility:       b.Visibility,
		IsPublished:      b.IsPublished,
		CreatedAt:        b.CreatedAt,
		UpdatedAt:        b.UpdatedAt,
	}
}

// ─────────────────────────────────────────────────────────────────────
// Lawyer-facing picker
// ─────────────────────────────────────────────────────────────────────

func handleListBuildingBlocks(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.submissionBuildingBlock == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "building blocks not configured"})
		return
	}
	q := r.URL.Query()
	filter := services.BlockListFilter{
		SectionKey:       strings.TrimSpace(q.Get("section_key")),
		ProceedingFamily: strings.TrimSpace(q.Get("proceeding_family")),
		Search:           strings.TrimSpace(q.Get("q")),
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()

	rows, err := dbSvc.submissionBuildingBlock.ListVisible(ctx, uid, filter)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	out := make([]buildingBlockJSON, 0, len(rows))
	for i := range rows {
		out = append(out, blockJSONFromService(&rows[i]))
	}
	writeJSON(w, http.StatusOK, buildingBlockListResponse{Blocks: out})
}

func handleInsertBlockIntoSection(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.submissionBuildingBlock == nil || dbSvc.submissionSection == nil || dbSvc.submissionDraft == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "building blocks not configured"})
		return
	}
	blockID, ok := parseUUIDPath(w, r, "block_id", "block id")
	if !ok {
		return
	}
	sectionID, ok := parseUUIDPath(w, r, "section_id", "section id")
	if !ok {
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
	defer cancel()

	// Visibility on the section: section.draft_id must point to a
	// draft the caller owns. Composer Slice B's same owner gate.
	sec, err := dbSvc.submissionSection.Get(ctx, sectionID)
	if err != nil {
		if errors.Is(err, services.ErrSubmissionSectionNotFound) {
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "section not found"})
			return
		}
		writeServiceError(w, err)
		return
	}
	if _, err := dbSvc.submissionDraft.Get(ctx, uid, sec.DraftID); err != nil {
		writeSubmissionDraftServiceError(w, err)
		return
	}

	updated, err := dbSvc.submissionBuildingBlock.InsertIntoSection(ctx, uid, blockID, sectionID, dbSvc.submissionSection)
	if err != nil {
		if errors.Is(err, services.ErrBuildingBlockNotFound) {
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "block not found"})
			return
		}
		if errors.Is(err, services.ErrSubmissionSectionNotFound) {
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "section not found"})
			return
		}
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, sectionJSONFromService(updated))
}

// ─────────────────────────────────────────────────────────────────────
// Admin editor
// ─────────────────────────────────────────────────────────────────────

func handleAdminListBuildingBlocks(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	if dbSvc.submissionBuildingBlock == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "building blocks not configured"})
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	rows, err := dbSvc.submissionBuildingBlock.ListAllForAdmin(ctx)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	out := make([]buildingBlockJSON, 0, len(rows))
	for i := range rows {
		out = append(out, blockJSONFromService(&rows[i]))
	}
	writeJSON(w, http.StatusOK, buildingBlockListResponse{Blocks: out})
}

func handleAdminGetBuildingBlock(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	if dbSvc.submissionBuildingBlock == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "building blocks not configured"})
		return
	}
	blockID, ok := parseUUIDPath(w, r, "block_id", "block id")
	if !ok {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	b, err := dbSvc.submissionBuildingBlock.GetForAdmin(ctx, blockID)
	if err != nil {
		if errors.Is(err, services.ErrBuildingBlockNotFound) {
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "block not found"})
			return
		}
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, blockJSONFromService(b))
}

type buildingBlockCreateInput struct {
	Slug             string  `json:"slug"`
	Firm             *string `json:"firm,omitempty"`
	SectionKey       string  `json:"section_key"`
	ProceedingFamily *string `json:"proceeding_family,omitempty"`
	TitleDE          string  `json:"title_de"`
	TitleEN          string  `json:"title_en"`
	DescriptionDE    *string `json:"description_de,omitempty"`
	DescriptionEN    *string `json:"description_en,omitempty"`
	ContentMDDE      string  `json:"content_md_de"`
	ContentMDEN      string  `json:"content_md_en"`
	Visibility       string  `json:"visibility"`
	IsPublished      bool    `json:"is_published"`
}

func handleAdminCreateBuildingBlock(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.submissionBuildingBlock == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "building blocks not configured"})
		return
	}
	var in buildingBlockCreateInput
	if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
	defer cancel()
	b, err := dbSvc.submissionBuildingBlock.Create(ctx, uid, services.CreateInput{
		Slug:             in.Slug,
		Firm:             in.Firm,
		SectionKey:       in.SectionKey,
		ProceedingFamily: in.ProceedingFamily,
		TitleDE:          in.TitleDE,
		TitleEN:          in.TitleEN,
		DescriptionDE:    in.DescriptionDE,
		DescriptionEN:    in.DescriptionEN,
		ContentMDDE:      in.ContentMDDE,
		ContentMDEN:      in.ContentMDEN,
		Visibility:       in.Visibility,
		IsPublished:      in.IsPublished,
	})
	if err != nil {
		if errors.Is(err, services.ErrInvalidInput) || errors.Is(err, services.ErrBuildingBlockInvalidVisibility) {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
			return
		}
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusCreated, blockJSONFromService(b))
}

type buildingBlockUpdateInput struct {
	Slug             *string `json:"slug,omitempty"`
	Firm             *string `json:"firm,omitempty"`
	FirmSet          bool    `json:"-"`
	SectionKey       *string `json:"section_key,omitempty"`
	ProceedingFamily *string `json:"proceeding_family,omitempty"`
	ProceedingFamilySet bool `json:"-"`
	TitleDE          *string `json:"title_de,omitempty"`
	TitleEN          *string `json:"title_en,omitempty"`
	DescriptionDE    *string `json:"description_de,omitempty"`
	DescriptionDESet bool    `json:"-"`
	DescriptionEN    *string `json:"description_en,omitempty"`
	DescriptionENSet bool    `json:"-"`
	ContentMDDE      *string `json:"content_md_de,omitempty"`
	ContentMDEN      *string `json:"content_md_en,omitempty"`
	Visibility       *string `json:"visibility,omitempty"`
	IsPublished      *bool   `json:"is_published,omitempty"`
	Note             *string `json:"note,omitempty"`
}

func (u *buildingBlockUpdateInput) UnmarshalJSON(data []byte) error {
	type alias buildingBlockUpdateInput
	var a alias
	if err := json.Unmarshal(data, &a); err != nil {
		return err
	}
	*u = buildingBlockUpdateInput(a)
	raw := map[string]json.RawMessage{}
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	_, u.FirmSet = raw["firm"]
	_, u.ProceedingFamilySet = raw["proceeding_family"]
	_, u.DescriptionDESet = raw["description_de"]
	_, u.DescriptionENSet = raw["description_en"]
	return nil
}

func handleAdminUpdateBuildingBlock(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.submissionBuildingBlock == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "building blocks not configured"})
		return
	}
	blockID, ok := parseUUIDPath(w, r, "block_id", "block id")
	if !ok {
		return
	}
	var in buildingBlockUpdateInput
	if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	patch := services.UpdatePatch{
		Slug:        in.Slug,
		SectionKey:  in.SectionKey,
		TitleDE:     in.TitleDE,
		TitleEN:     in.TitleEN,
		ContentMDDE: in.ContentMDDE,
		ContentMDEN: in.ContentMDEN,
		Visibility:  in.Visibility,
		IsPublished: in.IsPublished,
		Note:        in.Note,
	}
	if in.FirmSet {
		patch.Firm = &in.Firm
	}
	if in.ProceedingFamilySet {
		patch.ProceedingFamily = &in.ProceedingFamily
	}
	if in.DescriptionDESet {
		patch.DescriptionDE = &in.DescriptionDE
	}
	if in.DescriptionENSet {
		patch.DescriptionEN = &in.DescriptionEN
	}
	ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
	defer cancel()
	b, err := dbSvc.submissionBuildingBlock.Update(ctx, uid, blockID, patch)
	if err != nil {
		if errors.Is(err, services.ErrBuildingBlockNotFound) {
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "block not found"})
			return
		}
		if errors.Is(err, services.ErrBuildingBlockInvalidVisibility) {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
			return
		}
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, blockJSONFromService(b))
}

func handleAdminDeleteBuildingBlock(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.submissionBuildingBlock == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "building blocks not configured"})
		return
	}
	blockID, ok := parseUUIDPath(w, r, "block_id", "block id")
	if !ok {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	if err := dbSvc.submissionBuildingBlock.SoftDelete(ctx, uid, blockID); err != nil {
		if errors.Is(err, services.ErrBuildingBlockNotFound) {
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "block not found"})
			return
		}
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusNoContent, nil)
}

func handleAdminListBuildingBlockVersions(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	if dbSvc.submissionBuildingBlock == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "building blocks not configured"})
		return
	}
	blockID, ok := parseUUIDPath(w, r, "block_id", "block id")
	if !ok {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	rows, err := dbSvc.submissionBuildingBlock.ListVersions(ctx, blockID)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"versions": rows})
}

func handleAdminRestoreBuildingBlockVersion(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.submissionBuildingBlock == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "building blocks not configured"})
		return
	}
	blockID, ok := parseUUIDPath(w, r, "block_id", "block id")
	if !ok {
		return
	}
	versionID, ok := parseUUIDPath(w, r, "version_id", "version id")
	if !ok {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
	defer cancel()
	b, err := dbSvc.submissionBuildingBlock.RestoreVersion(ctx, uid, blockID, versionID)
	if err != nil {
		if errors.Is(err, services.ErrBuildingBlockNotFound) {
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "block or version not found"})
			return
		}
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, blockJSONFromService(b))
}

// handleAdminBuildingBlocksPage serves the admin editor shell. The
// client bundle hydrates the list + edit UI.
func handleAdminBuildingBlocksPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/admin-submission-building-blocks.html")
}
