package services

// Submission base catalog service — Composer Slice A (t-paliad-313,
// design doc docs/design-submission-generator-v2-2026-05-26.md §4.2 +
// §5.1).
//
// Each row in paliad.submission_bases maps a stable slug onto a Gitea
// path (the .docx body) plus a JSON section spec that drives the
// editor's default section seeding. Slice A surfaces this catalog via
// a sidebar picker and uses GetDefaultForCode to pre-fill base_id on
// new drafts.
//
// Read-only — admin mutations land in Slice C's /admin/submission-bases
// editor. Visibility is wide-open SELECT (the catalog is shared
// firm-wide); RLS denies mutations by default.

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

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

// SubmissionBase mirrors a row in paliad.submission_bases.
type SubmissionBase struct {
	ID               uuid.UUID      `db:"id"                 json:"id"`
	Slug             string         `db:"slug"               json:"slug"`
	Firm             *string        `db:"firm"               json:"firm,omitempty"`
	ProceedingFamily *string        `db:"proceeding_family"  json:"proceeding_family,omitempty"`
	LabelDE          string         `db:"label_de"           json:"label_de"`
	LabelEN          string         `db:"label_en"           json:"label_en"`
	DescriptionDE    *string        `db:"description_de"     json:"description_de,omitempty"`
	DescriptionEN    *string        `db:"description_en"     json:"description_en,omitempty"`
	GiteaPath        string         `db:"gitea_path"         json:"gitea_path"`
	SectionSpecRaw   []byte         `db:"section_spec"       json:"-"`
	IsDefaultForRaw  pq.StringArray `db:"is_default_for"     json:"-"`
	IsActive         bool           `db:"is_active"          json:"is_active"`

	// SectionSpec is the parsed section spec; populated on read by the
	// service so callers don't have to unmarshal manually.
	SectionSpec BaseSectionSpec `json:"section_spec"`

	// IsDefaultFor is the parsed string-slice form of the
	// is_default_for column.
	IsDefaultFor []string `json:"is_default_for"`
}

// BaseSectionSpec is the parsed shape of submission_bases.section_spec.
// Slice A consumes Defaults to seed submission_sections rows on draft
// create; later slices consume Stylemap (Slice B's MD→OOXML walker) and
// Version (forward compat).
type BaseSectionSpec struct {
	Version  int                        `json:"version"`
	Stylemap map[string]string          `json:"stylemap"`
	Defaults []BaseSectionSpecDefault   `json:"defaults"`
}

// BaseSectionSpecDefault declares one default section per base. SeedMD*
// is the Markdown copied into submission_sections.content_md_* on draft
// create. Empty seed = blank prose section.
type BaseSectionSpecDefault struct {
	SectionKey string `json:"section_key"`
	Kind       string `json:"kind"`
	OrderIndex int    `json:"order_index"`
	LabelDE    string `json:"label_de"`
	LabelEN    string `json:"label_en"`
	Included   bool   `json:"included"`
	SeedMDDE   string `json:"seed_md_de"`
	SeedMDEN   string `json:"seed_md_en"`
}

// BaseService reads the catalog. No mutations in Slice A.
type BaseService struct {
	db *sqlx.DB
}

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

// ErrBaseNotFound is the sentinel for "no base with that id/slug".
var ErrBaseNotFound = errors.New("submission base: not found")

const baseColumns = `id, slug, firm, proceeding_family, label_de, label_en,
                     description_de, description_en, gitea_path,
                     section_spec, is_default_for, is_active`

// List returns every active base ordered by firm-then-label.
// firmFilter (when non-empty) restricts to rows where firm matches OR
// firm IS NULL — the picker shows the firm's own bases plus the
// firm-agnostic ones.
func (s *BaseService) List(ctx context.Context, firmFilter string) ([]SubmissionBase, error) {
	var rows []SubmissionBase
	var err error
	if firmFilter == "" {
		err = s.db.SelectContext(ctx, &rows,
			`SELECT `+baseColumns+`
			   FROM paliad.submission_bases
			  WHERE is_active
			  ORDER BY COALESCE(firm, ''), label_de`)
	} else {
		err = s.db.SelectContext(ctx, &rows,
			`SELECT `+baseColumns+`
			   FROM paliad.submission_bases
			  WHERE is_active AND (firm = $1 OR firm IS NULL)
			  ORDER BY (firm IS NULL), label_de`,
			firmFilter)
	}
	if err != nil {
		return nil, fmt.Errorf("list submission bases: %w", err)
	}
	for i := range rows {
		if err := rows[i].decode(); err != nil {
			return nil, err
		}
	}
	return rows, nil
}

// GetByID fetches one base by uuid.
func (s *BaseService) GetByID(ctx context.Context, id uuid.UUID) (*SubmissionBase, error) {
	var b SubmissionBase
	err := s.db.GetContext(ctx, &b,
		`SELECT `+baseColumns+`
		   FROM paliad.submission_bases
		  WHERE id = $1 AND is_active`,
		id)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrBaseNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("get submission base by id: %w", err)
	}
	if err := b.decode(); err != nil {
		return nil, err
	}
	return &b, nil
}

// GetBySlug fetches one base by stable slug ("hlc-letterhead", …).
func (s *BaseService) GetBySlug(ctx context.Context, slug string) (*SubmissionBase, error) {
	var b SubmissionBase
	err := s.db.GetContext(ctx, &b,
		`SELECT `+baseColumns+`
		   FROM paliad.submission_bases
		  WHERE slug = $1 AND is_active`,
		slug)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrBaseNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("get submission base by slug: %w", err)
	}
	if err := b.decode(); err != nil {
		return nil, err
	}
	return &b, nil
}

// GetDefaultForCode picks the base SubmissionDraftService.Create should
// seed for a new draft, given the requesting firm and the draft's
// submission_code. Priority:
//
//  1. firm-matched base whose is_default_for[] contains the exact code.
//  2. firm-matched base whose proceeding_family matches the code's
//     family (first three dot-segments, e.g. "de.inf.lg" from
//     "de.inf.lg.erwidg").
//  3. firm-matched base with NULL proceeding_family (firm-agnostic
//     fallback within the firm).
//  4. firm-NULL (cross-firm) base by family match.
//  5. firm-NULL base with NULL family — the universal neutral fallback.
//  6. first active row (deterministic ordering on (firm IS NULL,
//     label_de)).
//
// Returns ErrBaseNotFound if the table is empty.
func (s *BaseService) GetDefaultForCode(ctx context.Context, firm, submissionCode string) (*SubmissionBase, error) {
	family := familyOfCode(submissionCode)

	tryQueries := []struct {
		sql    string
		args   []any
	}{
		{
			`SELECT ` + baseColumns + `
			   FROM paliad.submission_bases
			  WHERE is_active AND firm = $1 AND $2 = ANY(is_default_for)
			  ORDER BY label_de LIMIT 1`,
			[]any{firm, submissionCode},
		},
		{
			`SELECT ` + baseColumns + `
			   FROM paliad.submission_bases
			  WHERE is_active AND firm = $1 AND proceeding_family = $2
			  ORDER BY label_de LIMIT 1`,
			[]any{firm, family},
		},
		{
			`SELECT ` + baseColumns + `
			   FROM paliad.submission_bases
			  WHERE is_active AND firm = $1 AND proceeding_family IS NULL
			  ORDER BY label_de LIMIT 1`,
			[]any{firm},
		},
		{
			`SELECT ` + baseColumns + `
			   FROM paliad.submission_bases
			  WHERE is_active AND firm IS NULL AND proceeding_family = $1
			  ORDER BY label_de LIMIT 1`,
			[]any{family},
		},
		{
			`SELECT ` + baseColumns + `
			   FROM paliad.submission_bases
			  WHERE is_active AND firm IS NULL AND proceeding_family IS NULL
			  ORDER BY label_de LIMIT 1`,
			[]any{},
		},
		{
			`SELECT ` + baseColumns + `
			   FROM paliad.submission_bases
			  WHERE is_active
			  ORDER BY (firm IS NULL), label_de LIMIT 1`,
			[]any{},
		},
	}

	for _, q := range tryQueries {
		var b SubmissionBase
		err := s.db.GetContext(ctx, &b, q.sql, q.args...)
		if errors.Is(err, sql.ErrNoRows) {
			continue
		}
		if err != nil {
			return nil, fmt.Errorf("get default base: %w", err)
		}
		if err := b.decode(); err != nil {
			return nil, err
		}
		return &b, nil
	}
	return nil, ErrBaseNotFound
}

// familyOfCode returns the first three dot-segments of a submission_code.
// "de.inf.lg.erwidg" → "de.inf.lg". Codes with fewer than three segments
// pass through unchanged (none in the corpus today, but safe).
func familyOfCode(code string) string {
	parts := strings.SplitN(code, ".", 4)
	if len(parts) <= 3 {
		return code
	}
	return strings.Join(parts[:3], ".")
}

// decode fills the parsed views from the raw scan fields.
func (b *SubmissionBase) decode() error {
	if len(b.SectionSpecRaw) > 0 {
		if err := json.Unmarshal(b.SectionSpecRaw, &b.SectionSpec); err != nil {
			return fmt.Errorf("decode submission base section_spec: %w", err)
		}
	}
	b.IsDefaultFor = []string(b.IsDefaultForRaw)
	if b.IsDefaultFor == nil {
		b.IsDefaultFor = []string{}
	}
	return nil
}
