package services

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"strings"
	"time"

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

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

// ChecklistTemplateService is the write surface for user-authored checklist
// templates (paliad.checklists, mig 114). Create / Update / Delete on
// owner-only paths; SetVisibility on private↔firm only (Slice A — Slice B
// adds 'shared' grants, Slice C adds 'global' via admin promotion).
type ChecklistTemplateService struct {
	db      *sqlx.DB
	catalog *ChecklistCatalogService
	audit   *SystemAuditLogService
	users   *UserService
}

func NewChecklistTemplateService(db *sqlx.DB, catalog *ChecklistCatalogService, audit *SystemAuditLogService, users *UserService) *ChecklistTemplateService {
	return &ChecklistTemplateService{db: db, catalog: catalog, audit: audit, users: users}
}

// CreateTemplateInput is the POST body for authoring a new template.
//
// Body carries the groups[] / items[] sub-tree as JSONB; the surrounding
// metadata (title, regime, etc.) lives on dedicated columns. The
// handler validates the body shape upstream.
type CreateTemplateInput struct {
	Title       string          `json:"title"`
	Description string          `json:"description"`
	Regime      string          `json:"regime"`
	Court       string          `json:"court"`
	Reference   string          `json:"reference"`
	Deadline    string          `json:"deadline"`
	Lang        string          `json:"lang"`
	Body        json.RawMessage `json:"body"`
	Visibility  string          `json:"visibility"`
}

// UpdateTemplateInput patches the owner-editable fields. Any field left
// nil is unchanged.
type UpdateTemplateInput struct {
	Title       *string          `json:"title,omitempty"`
	Description *string          `json:"description,omitempty"`
	Regime      *string          `json:"regime,omitempty"`
	Court       *string          `json:"court,omitempty"`
	Reference   *string          `json:"reference,omitempty"`
	Deadline    *string          `json:"deadline,omitempty"`
	Body        *json.RawMessage `json:"body,omitempty"`
}

var (
	validRegimes      = map[string]bool{"UPC": true, "DE": true, "EPA": true, "OTHER": true}
	validLangs        = map[string]bool{"de": true, "en": true}
	// Author-settable visibilities. 'shared' is implicit (set
	// automatically when the first checklist_shares row exists); 'global'
	// is admin-only via ChecklistPromotionService.
	validVisibilities = map[string]bool{"private": true, "firm": true, "shared": true}
	titleMaxLen       = 200
	descriptionMaxLen = 2000
	freeTextMaxLen    = 200
	slugSafeChars     = regexp.MustCompile(`[^a-z0-9-]+`)
)

// Create inserts a new authored template owned by userID. Returns the
// created row; emits a `checklist.authored` audit event.
func (s *ChecklistTemplateService) Create(ctx context.Context, userID uuid.UUID, input CreateTemplateInput) (*models.Checklist, error) {
	title, err := requireNonEmptyTrimmed(input.Title, "title", titleMaxLen)
	if err != nil {
		return nil, err
	}
	regime, err := normaliseRegime(input.Regime)
	if err != nil {
		return nil, err
	}
	lang, err := normaliseLang(input.Lang)
	if err != nil {
		return nil, err
	}
	visibility, err := normaliseSliceAVisibility(input.Visibility)
	if err != nil {
		return nil, err
	}
	if err := validateBodyShape(input.Body); err != nil {
		return nil, err
	}

	slug, err := s.generateSlug(ctx, title)
	if err != nil {
		return nil, err
	}

	now := time.Now().UTC()
	id := uuid.New()

	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("begin create tx: %w", err)
	}
	defer tx.Rollback()

	_, err = tx.ExecContext(ctx,
		`INSERT INTO paliad.checklists
		   (id, slug, owner_id, title, description, regime, court, reference,
		    deadline, lang, body, visibility, created_at, updated_at)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13, $13)`,
		id, slug, userID, title,
		clampFreeText(input.Description, descriptionMaxLen),
		regime,
		clampFreeText(input.Court, freeTextMaxLen),
		clampFreeText(input.Reference, freeTextMaxLen),
		clampFreeText(input.Deadline, freeTextMaxLen),
		lang,
		string(input.Body),
		visibility,
		now,
	)
	if err != nil {
		return nil, fmt.Errorf("insert checklist: %w", err)
	}

	actorEmail, _ := s.actorEmail(ctx, userID)
	if err := s.audit.WriteChecklistEvent(ctx, tx, ChecklistAuditEvent{
		EventType: "checklist.authored",
		ActorID:   userID,
		ActorEmail: actorEmail,
		Metadata: map[string]any{
			"checklist_id": id,
			"slug":         slug,
			"visibility":   visibility,
		},
	}); err != nil {
		return nil, err
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit create checklist: %w", err)
	}
	return s.GetBySlug(ctx, userID, slug)
}

// Update mutates an authored template. Owner-only; non-owner attempts
// return ErrForbidden. Emits `checklist.edited` with the names of the
// changed fields in metadata.changed_fields[].
func (s *ChecklistTemplateService) Update(ctx context.Context, userID uuid.UUID, slug string, input UpdateTemplateInput) (*models.Checklist, error) {
	row, err := s.requireOwnerOrAdmin(ctx, userID, slug)
	if err != nil {
		return nil, err
	}

	sets := []string{}
	args := []any{}
	next := 1
	changed := []string{}
	appendSet := func(col string, val any) {
		sets = append(sets, fmt.Sprintf("%s = $%d", col, next))
		args = append(args, val)
		next++
	}

	if input.Title != nil {
		t, err := requireNonEmptyTrimmed(*input.Title, "title", titleMaxLen)
		if err != nil {
			return nil, err
		}
		appendSet("title", t)
		changed = append(changed, "title")
	}
	if input.Description != nil {
		appendSet("description", clampFreeText(*input.Description, descriptionMaxLen))
		changed = append(changed, "description")
	}
	if input.Regime != nil {
		r, err := normaliseRegime(*input.Regime)
		if err != nil {
			return nil, err
		}
		appendSet("regime", r)
		changed = append(changed, "regime")
	}
	if input.Court != nil {
		appendSet("court", clampFreeText(*input.Court, freeTextMaxLen))
		changed = append(changed, "court")
	}
	if input.Reference != nil {
		appendSet("reference", clampFreeText(*input.Reference, freeTextMaxLen))
		changed = append(changed, "reference")
	}
	if input.Deadline != nil {
		appendSet("deadline", clampFreeText(*input.Deadline, freeTextMaxLen))
		changed = append(changed, "deadline")
	}
	if input.Body != nil {
		if err := validateBodyShape(*input.Body); err != nil {
			return nil, err
		}
		sets = append(sets, fmt.Sprintf("body = $%d::jsonb", next))
		args = append(args, string(*input.Body))
		next++
		changed = append(changed, "body")
	}

	if len(sets) == 0 {
		return row, nil
	}

	// Version bump (Slice C). Title and body are the meaningful edits
	// that warrant a "your snapshot is outdated" badge on existing
	// instances. Pure metadata tweaks (description / court / reference
	// / deadline) update updated_at but don't bump version — we don't
	// want every typo correction to nag users with an outdated badge.
	versionBumped := false
	for _, f := range changed {
		if f == "title" || f == "body" {
			versionBumped = true
			break
		}
	}
	if versionBumped {
		sets = append(sets, "version = version + 1")
	}

	appendSet("updated_at", time.Now().UTC())
	args = append(args, row.ID)

	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("begin update tx: %w", err)
	}
	defer tx.Rollback()

	q := fmt.Sprintf(`UPDATE paliad.checklists SET %s WHERE id = $%d`,
		strings.Join(sets, ", "), next)
	if _, err := tx.ExecContext(ctx, q, args...); err != nil {
		return nil, fmt.Errorf("update checklist: %w", err)
	}

	actorEmail, _ := s.actorEmail(ctx, userID)
	if err := s.audit.WriteChecklistEvent(ctx, tx, ChecklistAuditEvent{
		EventType: "checklist.edited",
		ActorID:   userID,
		ActorEmail: actorEmail,
		Metadata: map[string]any{
			"checklist_id":   row.ID,
			"slug":           slug,
			"changed_fields": changed,
		},
	}); err != nil {
		return nil, err
	}

	// Slice C — emit a separate 'checklist.versioned' event when the
	// edit actually bumped the version. Dashboards / future popularity
	// sort can read this without parsing changed_fields[].
	if versionBumped {
		if err := s.audit.WriteChecklistEvent(ctx, tx, ChecklistAuditEvent{
			EventType:  "checklist.versioned",
			ActorID:    userID,
			ActorEmail: actorEmail,
			Metadata: map[string]any{
				"checklist_id":  row.ID,
				"slug":          slug,
				"prior_version": row.Version,
				"new_version":   row.Version + 1,
			},
		}); err != nil {
			return nil, err
		}
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit update checklist: %w", err)
	}
	return s.GetBySlug(ctx, userID, slug)
}

// SetVisibility flips the visibility level. Slice A allows only the
// private ↔ firm transitions; Slice B opens 'shared' (requires share
// grants); Slice C opens 'global' via the admin promotion service.
func (s *ChecklistTemplateService) SetVisibility(ctx context.Context, userID uuid.UUID, slug string, visibility string) (*models.Checklist, error) {
	row, err := s.requireOwnerOrAdmin(ctx, userID, slug)
	if err != nil {
		return nil, err
	}
	target, err := normaliseSliceAVisibility(visibility)
	if err != nil {
		return nil, err
	}
	if row.Visibility == target {
		return row, nil
	}

	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("begin visibility tx: %w", err)
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx,
		`UPDATE paliad.checklists
		    SET visibility = $2, updated_at = now()
		  WHERE id = $1`, row.ID, target); err != nil {
		return nil, fmt.Errorf("update visibility: %w", err)
	}

	actorEmail, _ := s.actorEmail(ctx, userID)
	if err := s.audit.WriteChecklistEvent(ctx, tx, ChecklistAuditEvent{
		EventType: "checklist.visibility_changed",
		ActorID:   userID,
		ActorEmail: actorEmail,
		Metadata: map[string]any{
			"checklist_id": row.ID,
			"slug":         slug,
			"from":         row.Visibility,
			"to":           target,
		},
	}); err != nil {
		return nil, err
	}
	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit visibility: %w", err)
	}
	return s.GetBySlug(ctx, userID, slug)
}

// Delete removes the authored template. Existing instances survive via
// template_snapshot; new instance creation against this slug fails.
func (s *ChecklistTemplateService) Delete(ctx context.Context, userID uuid.UUID, slug string) error {
	row, err := s.requireOwnerOrAdmin(ctx, userID, slug)
	if err != nil {
		return err
	}

	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return fmt.Errorf("begin delete tx: %w", err)
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx,
		`DELETE FROM paliad.checklists WHERE id = $1`, row.ID); err != nil {
		return fmt.Errorf("delete checklist: %w", err)
	}

	actorEmail, _ := s.actorEmail(ctx, userID)
	if err := s.audit.WriteChecklistEvent(ctx, tx, ChecklistAuditEvent{
		EventType: "checklist.deleted",
		ActorID:   userID,
		ActorEmail: actorEmail,
		Metadata: map[string]any{
			"checklist_id":    row.ID,
			"slug":            slug,
			"was_visibility":  row.Visibility,
		},
	}); err != nil {
		return err
	}
	return tx.Commit()
}

// ListOwnedBy returns every authored template owned by the caller. Used
// by the 'Meine Vorlagen' tab on /checklists.
func (s *ChecklistTemplateService) ListOwnedBy(ctx context.Context, userID uuid.UUID) ([]models.Checklist, error) {
	rows := []models.Checklist{}
	if err := s.db.SelectContext(ctx, &rows,
		`SELECT id, slug, owner_id, title, description, regime, court, reference,
		        deadline, lang, body, visibility, promoted_at, promoted_by,
		        version, created_at, updated_at
		   FROM paliad.checklists
		  WHERE owner_id = $1
		  ORDER BY updated_at DESC`, userID); err != nil {
		return nil, fmt.Errorf("list owned checklists: %w", err)
	}
	return rows, nil
}

// GetBySlug returns one authored template by slug; applies visibility.
func (s *ChecklistTemplateService) GetBySlug(ctx context.Context, userID uuid.UUID, slug string) (*models.Checklist, error) {
	var row models.Checklist
	q := `SELECT id, slug, owner_id, title, description, regime, court, reference,
	             deadline, lang, body, visibility, promoted_at, promoted_by,
	             version, created_at, updated_at
	        FROM paliad.checklists
	       WHERE slug = $2
	         AND ` + checklistVisibilityPredicate("paliad.checklists", 1)
	err := s.db.GetContext(ctx, &row, q, userID, slug)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNotVisible
	}
	if err != nil {
		return nil, fmt.Errorf("fetch checklist: %w", err)
	}
	return &row, nil
}

// --- internals ------------------------------------------------------------

// requireOwnerOrAdmin fetches the row and returns it iff caller is owner
// or global_admin. Other callers get ErrForbidden (template visible to
// many users, only some can mutate).
func (s *ChecklistTemplateService) requireOwnerOrAdmin(ctx context.Context, userID uuid.UUID, slug string) (*models.Checklist, error) {
	row, err := s.GetBySlug(ctx, userID, slug)
	if err != nil {
		return nil, err
	}
	if row.OwnerID == userID {
		return row, nil
	}
	user, err := s.users.GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if user != nil && user.GlobalRole == "global_admin" {
		return row, nil
	}
	return nil, fmt.Errorf("%w: only the owner or a global_admin can modify this checklist", ErrForbidden)
}

func (s *ChecklistTemplateService) actorEmail(ctx context.Context, userID uuid.UUID) (string, error) {
	u, err := s.users.GetByID(ctx, userID)
	if err != nil || u == nil {
		return "", err
	}
	return u.Email, nil
}

// generateSlug builds a 'u-<title-slug>-<6hex>' slug. Three retries on
// collision (against authored table + static catalog). After three
// failures we fall back to a pure-random suffix so the create path
// never wedges.
func (s *ChecklistTemplateService) generateSlug(ctx context.Context, title string) (string, error) {
	base := slugifyTitle(title)
	if base == "" {
		base = "checklist"
	}
	for attempt := 0; attempt < 3; attempt++ {
		suffix, err := randomHex(3)
		if err != nil {
			return "", err
		}
		slug := "u-" + base + "-" + suffix
		if len(slug) > 64 {
			slug = slug[:64]
		}
		taken, err := s.slugTaken(ctx, slug)
		if err != nil {
			return "", err
		}
		if !taken {
			return slug, nil
		}
	}
	suffix, err := randomHex(6)
	if err != nil {
		return "", err
	}
	return "u-" + suffix, nil
}

func (s *ChecklistTemplateService) slugTaken(ctx context.Context, slug string) (bool, error) {
	if s.catalog.IsStaticSlug(slug) {
		return true, nil
	}
	var n int
	if err := s.db.GetContext(ctx, &n,
		`SELECT count(*) FROM paliad.checklists WHERE slug = $1`, slug); err != nil {
		return false, fmt.Errorf("slug taken check: %w", err)
	}
	return n > 0, nil
}

// --- pure helpers ---------------------------------------------------------

func slugifyTitle(title string) string {
	s := strings.ToLower(strings.TrimSpace(title))
	s = strings.ReplaceAll(s, "ä", "ae")
	s = strings.ReplaceAll(s, "ö", "oe")
	s = strings.ReplaceAll(s, "ü", "ue")
	s = strings.ReplaceAll(s, "ß", "ss")
	s = slugSafeChars.ReplaceAllString(s, "-")
	s = strings.Trim(s, "-")
	if len(s) > 40 {
		s = s[:40]
	}
	return strings.Trim(s, "-")
}

func randomHex(n int) (string, error) {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("rand: %w", err)
	}
	return hex.EncodeToString(b), nil
}

func requireNonEmptyTrimmed(v, field string, max int) (string, error) {
	t := strings.TrimSpace(v)
	if t == "" {
		return "", fmt.Errorf("%w: %s is required", ErrInvalidInput, field)
	}
	if len(t) > max {
		return "", fmt.Errorf("%w: %s exceeds %d characters", ErrInvalidInput, field, max)
	}
	return t, nil
}

func clampFreeText(v string, max int) string {
	v = strings.TrimSpace(v)
	if len(v) > max {
		v = v[:max]
	}
	return v
}

func normaliseRegime(v string) (string, error) {
	r := strings.ToUpper(strings.TrimSpace(v))
	if r == "" {
		r = "OTHER"
	}
	if !validRegimes[r] {
		return "", fmt.Errorf("%w: regime must be UPC | DE | EPA | OTHER, got %q", ErrInvalidInput, v)
	}
	return r, nil
}

func normaliseLang(v string) (string, error) {
	l := strings.ToLower(strings.TrimSpace(v))
	if l == "" {
		l = "de"
	}
	if !validLangs[l] {
		return "", fmt.Errorf("%w: lang must be de | en, got %q", ErrInvalidInput, v)
	}
	return l, nil
}

func normaliseSliceAVisibility(v string) (string, error) {
	x := strings.ToLower(strings.TrimSpace(v))
	if x == "" {
		x = "private"
	}
	if !validVisibilities[x] {
		return "", fmt.Errorf("%w: visibility must be private | firm | shared, got %q (global is admin-only)", ErrInvalidInput, v)
	}
	return x, nil
}

// validateBodyShape enforces { "groups": [...] } with at least one
// non-empty group and at least one non-empty item somewhere. Authored
// templates aren't useful without content.
func validateBodyShape(body json.RawMessage) error {
	if len(body) == 0 {
		return fmt.Errorf("%w: body is required", ErrInvalidInput)
	}
	var shape struct {
		Groups []checklists.Group `json:"groups"`
	}
	if err := json.Unmarshal(body, &shape); err != nil {
		return fmt.Errorf("%w: body must be {\"groups\":[...]} (%v)", ErrInvalidInput, err)
	}
	if len(shape.Groups) == 0 {
		return fmt.Errorf("%w: body must contain at least one group", ErrInvalidInput)
	}
	totalItems := 0
	for _, g := range shape.Groups {
		totalItems += len(g.Items)
	}
	if totalItems == 0 {
		return fmt.Errorf("%w: body must contain at least one item", ErrInvalidInput)
	}
	return nil
}
