package services

// Submission section service — Composer Slice A (t-paliad-313, design
// doc docs/design-submission-generator-v2-2026-05-26.md §4.3 + §6).
//
// Each row in paliad.submission_sections is one ordered, named block
// inside a Composer draft. Slice A seeds rows on draft create from the
// base's section_spec.defaults and exposes them read-only for the
// editor's section-list pane. Slice B turns them editable, Slice F
// adds reorder/hide/add-custom.
//
// Visibility flows through draft_id → submission_drafts → owner-scoped
// + can_see_project (RLS in mig 148 mirrors the four-policy shape on
// submission_drafts). Service calls go through SubmissionDraftService
// for the visibility gate before touching this table.

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
)

// SubmissionSection mirrors a row in paliad.submission_sections.
type SubmissionSection struct {
	ID           uuid.UUID `db:"id"             json:"id"`
	DraftID      uuid.UUID `db:"draft_id"       json:"draft_id"`
	SectionKey   string    `db:"section_key"    json:"section_key"`
	OrderIndex   int       `db:"order_index"    json:"order_index"`
	Kind         string    `db:"kind"           json:"kind"`
	LabelDE      string    `db:"label_de"       json:"label_de"`
	LabelEN      string    `db:"label_en"       json:"label_en"`
	Included     bool      `db:"included"       json:"included"`
	ContentMDDE  string    `db:"content_md_de"  json:"content_md_de"`
	ContentMDEN  string    `db:"content_md_en"  json:"content_md_en"`
	CreatedAt    time.Time `db:"created_at"     json:"created_at"`
	UpdatedAt    time.Time `db:"updated_at"     json:"updated_at"`
}

// SectionService handles per-draft section rows. Slice A: read + seed
// only. Editable mutations land in Slice B's brief.
type SectionService struct {
	db *sqlx.DB
}

// NewSectionService wires the service.
func NewSectionService(db *sqlx.DB) *SectionService {
	return &SectionService{db: db}
}

// ErrSubmissionSectionNotFound is the sentinel for "no section with
// that id visible to this user".
var ErrSubmissionSectionNotFound = errors.New("submission section: not found")

const sectionColumns = `id, draft_id, section_key, order_index, kind,
                        label_de, label_en, included,
                        content_md_de, content_md_en,
                        created_at, updated_at`

// ListForDraft returns every section row for a draft, ordered by
// order_index ASC. Caller is responsible for the visibility gate
// (SubmissionDraftService.Get returns ErrSubmissionDraftNotFound for
// un-visible drafts, which the handler maps to 404). RLS in mig 148
// additionally enforces owner-scope at the DB layer.
func (s *SectionService) ListForDraft(ctx context.Context, draftID uuid.UUID) ([]SubmissionSection, error) {
	var rows []SubmissionSection
	err := s.db.SelectContext(ctx, &rows,
		`SELECT `+sectionColumns+`
		   FROM paliad.submission_sections
		  WHERE draft_id = $1
		  ORDER BY order_index ASC`,
		draftID)
	if err != nil {
		return nil, fmt.Errorf("list submission sections: %w", err)
	}
	return rows, nil
}

// Get returns one section by id. Visibility gate is the caller's
// responsibility — Slice A handlers wrap this with a SubmissionDraftService.Get
// to enforce owner+can_see_project before exposing the section.
func (s *SectionService) Get(ctx context.Context, sectionID uuid.UUID) (*SubmissionSection, error) {
	var sec SubmissionSection
	err := s.db.GetContext(ctx, &sec,
		`SELECT `+sectionColumns+`
		   FROM paliad.submission_sections
		  WHERE id = $1`,
		sectionID)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrSubmissionSectionNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("get submission section: %w", err)
	}
	return &sec, nil
}

// SectionPatch carries optional fields for an Update call. nil pointer
// = "no change"; non-nil = "set to this".
type SectionPatch struct {
	ContentMDDE *string
	ContentMDEN *string
	Included    *bool
	LabelDE     *string
	LabelEN     *string
	OrderIndex  *int
}

// Update applies a patch to one section row. Visibility is the caller's
// responsibility — handlers wrap with SubmissionDraftService.Get for
// owner-scoped checks. The DB-level RLS policy mirrors that check.
//
// Returns the refreshed row. ErrSubmissionSectionNotFound when the
// section doesn't exist or the calling owner can't see it (RLS
// filters at the SELECT step).
func (s *SectionService) Update(ctx context.Context, sectionID uuid.UUID, patch SectionPatch) (*SubmissionSection, error) {
	setParts := []string{}
	args := []any{}
	idx := 1

	if patch.ContentMDDE != nil {
		setParts = append(setParts, fmt.Sprintf("content_md_de = $%d", idx))
		args = append(args, *patch.ContentMDDE)
		idx++
	}
	if patch.ContentMDEN != nil {
		setParts = append(setParts, fmt.Sprintf("content_md_en = $%d", idx))
		args = append(args, *patch.ContentMDEN)
		idx++
	}
	if patch.Included != nil {
		setParts = append(setParts, fmt.Sprintf("included = $%d", idx))
		args = append(args, *patch.Included)
		idx++
	}
	if patch.LabelDE != nil {
		setParts = append(setParts, fmt.Sprintf("label_de = $%d", idx))
		args = append(args, *patch.LabelDE)
		idx++
	}
	if patch.LabelEN != nil {
		setParts = append(setParts, fmt.Sprintf("label_en = $%d", idx))
		args = append(args, *patch.LabelEN)
		idx++
	}
	if patch.OrderIndex != nil {
		setParts = append(setParts, fmt.Sprintf("order_index = $%d", idx))
		args = append(args, *patch.OrderIndex)
		idx++
	}

	if len(setParts) == 0 {
		return s.Get(ctx, sectionID)
	}

	args = append(args, sectionID)
	q := fmt.Sprintf(
		`UPDATE paliad.submission_sections
		    SET %s
		  WHERE id = $%d
		  RETURNING `+sectionColumns,
		strings.Join(setParts, ", "), idx,
	)

	var sec SubmissionSection
	err := s.db.GetContext(ctx, &sec, q, args...)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrSubmissionSectionNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("update submission section: %w", err)
	}
	return &sec, nil
}

// SectionCreateInput is the payload for adding a new (lawyer-custom)
// section to a draft (t-paliad-318 Slice F).
type SectionCreateInput struct {
	DraftID     uuid.UUID
	SectionKey  string
	Kind        string
	LabelDE     string
	LabelEN     string
	ContentMDDE string
	ContentMDEN string
	OrderIndex  int  // 0 = append at end
	Included    bool // defaults to true if not specified at the handler
}

// Create inserts a new section row for the draft. The section_key
// must not already exist on this draft (UNIQUE constraint at the DB
// catches collisions and surfaces as ErrInvalidInput).
//
// OrderIndex=0 means "auto-assign at the end" — the service queries
// the current max(order_index) and increments. Non-zero values insert
// at the requested position; the caller is responsible for any
// subsequent Reorder if they intend to push existing rows down.
func (s *SectionService) Create(ctx context.Context, in SectionCreateInput) (*SubmissionSection, error) {
	in.SectionKey = strings.TrimSpace(in.SectionKey)
	in.LabelDE = strings.TrimSpace(in.LabelDE)
	in.LabelEN = strings.TrimSpace(in.LabelEN)
	if in.SectionKey == "" || in.LabelDE == "" || in.LabelEN == "" {
		return nil, ErrInvalidInput
	}
	switch in.Kind {
	case "prose", "requests", "evidence":
	default:
		return nil, ErrInvalidInput
	}

	if in.OrderIndex == 0 {
		var maxOrder int
		err := s.db.GetContext(ctx, &maxOrder,
			`SELECT COALESCE(MAX(order_index), 0) FROM paliad.submission_sections WHERE draft_id = $1`,
			in.DraftID)
		if err != nil {
			return nil, fmt.Errorf("max order_index: %w", err)
		}
		in.OrderIndex = maxOrder + 1
	}

	var sec SubmissionSection
	err := s.db.GetContext(ctx, &sec,
		`INSERT INTO paliad.submission_sections
		     (draft_id, section_key, order_index, kind,
		      label_de, label_en, included,
		      content_md_de, content_md_en)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
		 RETURNING `+sectionColumns,
		in.DraftID, in.SectionKey, in.OrderIndex, in.Kind,
		in.LabelDE, in.LabelEN, in.Included,
		in.ContentMDDE, in.ContentMDEN)
	if err != nil {
		// UNIQUE (draft_id, section_key) collision → invalid input.
		if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "23505") {
			return nil, fmt.Errorf("%w: section_key already exists on this draft", ErrInvalidInput)
		}
		return nil, fmt.Errorf("create submission section: %w", err)
	}
	return &sec, nil
}

// Delete removes one section row by id. Owner-scope is the caller's
// responsibility (the handler runs SubmissionDraftService.Get first).
func (s *SectionService) Delete(ctx context.Context, sectionID uuid.UUID) error {
	res, err := s.db.ExecContext(ctx,
		`DELETE FROM paliad.submission_sections WHERE id = $1`,
		sectionID)
	if err != nil {
		return fmt.Errorf("delete submission section: %w", err)
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return ErrSubmissionSectionNotFound
	}
	return nil
}

// Reorder updates the order_index of every section row for the draft
// according to the supplied ID sequence. Transactional — partial
// failures roll back. Any section_id present on the draft but not in
// the sequence keeps its previous order_index, then sorts last by
// updated_at (so a partial reorder doesn't lose rows the caller
// forgot to mention).
func (s *SectionService) Reorder(ctx context.Context, draftID uuid.UUID, order []uuid.UUID) ([]SubmissionSection, error) {
	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("reorder tx: %w", err)
	}
	committed := false
	defer func() {
		if !committed {
			_ = tx.Rollback()
		}
	}()

	// Each id in order gets order_index 10, 20, 30, ... (gaps so a
	// future single-row insert doesn't trigger a full reflow). Ids
	// not present on the draft are silently ignored.
	for i, sectionID := range order {
		idx := (i + 1) * 10
		_, err := tx.ExecContext(ctx,
			`UPDATE paliad.submission_sections
			    SET order_index = $1
			  WHERE id = $2 AND draft_id = $3`,
			idx, sectionID, draftID)
		if err != nil {
			return nil, fmt.Errorf("reorder update: %w", err)
		}
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit reorder: %w", err)
	}
	committed = true

	return s.ListForDraft(ctx, draftID)
}

// SeedFromSpec inserts one row per BaseSectionSpec.Default into
// submission_sections for the given draft. Runs inside the caller's
// transaction (the SubmissionDraftService.Create path wraps the
// draft INSERT + section seed in one tx so a failed seed rolls back
// the draft too).
//
// Idempotent at the row level — UNIQUE (draft_id, section_key) returns
// an error if the seed runs twice for the same draft, which is the
// desired safety net (we never want to silently double-seed).
//
// Per the Q10 ratification: every kind is one of prose | requests |
// evidence — there is no *_auto kind. Caption/letterhead/signature
// sections are regular prose rows seeded with bag-driven Markdown.
func (s *SectionService) SeedFromSpec(ctx context.Context, tx *sqlx.Tx, draftID uuid.UUID, spec BaseSectionSpec) error {
	if len(spec.Defaults) == 0 {
		return nil
	}
	for _, d := range spec.Defaults {
		_, err := tx.ExecContext(ctx,
			`INSERT INTO paliad.submission_sections
			     (draft_id, section_key, order_index, kind,
			      label_de, label_en, included,
			      content_md_de, content_md_en)
			 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
			draftID, d.SectionKey, d.OrderIndex, d.Kind,
			d.LabelDE, d.LabelEN, d.Included,
			d.SeedMDDE, d.SeedMDEN)
		if err != nil {
			return fmt.Errorf("seed submission section %s: %w", d.SectionKey, err)
		}
	}
	return nil
}
