package services

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

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

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

// CalendarBindingService — CRUD on paliad.user_calendar_bindings.
//
// Each row is one of N (calendar, scope) bindings layered on top of the
// user's single CalDAV server connection in paliad.user_caldav_config.
// Slice 1 (t-paliad-212) introduced the table + an auto-backfilled
// 'all_visible' binding per existing user; Slice 2a wires the service
// that owns the rows. The sync engine (CalDAVService) drives off
// ListEnabled to discover where to push.
//
// Validation of (scope_kind, scope_id) combinatorics is enforced both
// here (so the API returns a useful 400) and by the table's CHECK
// constraints (so direct SQL or older clients can't slip a bad row in).
type CalendarBindingService struct {
	db *sqlx.DB
}

func NewCalendarBindingService(db *sqlx.DB) *CalendarBindingService {
	return &CalendarBindingService{db: db}
}

const bindingColumns = `
    id, user_id, calendar_path, display_name,
    scope_kind, scope_id, include_personal, enabled,
    last_sync_at, last_sync_error, created_at, updated_at`

// ListForUser returns every binding owned by the user, ordered by
// scope_kind then created_at so the all_visible / personal_only roots
// always sort to the top.
func (s *CalendarBindingService) ListForUser(ctx context.Context, userID uuid.UUID) ([]models.UserCalendarBinding, error) {
	rows := []models.UserCalendarBinding{}
	if err := s.db.SelectContext(ctx, &rows,
		`SELECT `+bindingColumns+`
		   FROM paliad.user_calendar_bindings
		  WHERE user_id = $1
		  ORDER BY
		    CASE scope_kind
		      WHEN 'all_visible'   THEN 0
		      WHEN 'personal_only' THEN 1
		      ELSE 2
		    END,
		    created_at`, userID); err != nil {
		return nil, fmt.Errorf("list bindings: %w", err)
	}
	return rows, nil
}

// ListEnabled returns the user's bindings with enabled = true.
// Used by the CalDAVService sync loop.
func (s *CalendarBindingService) ListEnabled(ctx context.Context, userID uuid.UUID) ([]models.UserCalendarBinding, error) {
	rows := []models.UserCalendarBinding{}
	if err := s.db.SelectContext(ctx, &rows,
		`SELECT `+bindingColumns+`
		   FROM paliad.user_calendar_bindings
		  WHERE user_id = $1 AND enabled = true
		  ORDER BY created_at`, userID); err != nil {
		return nil, fmt.Errorf("list enabled bindings: %w", err)
	}
	return rows, nil
}

// ListAllEnabled returns every enabled binding across all users.
// Used at server boot to spawn one sync goroutine per (user) that
// owns at least one enabled binding.
func (s *CalendarBindingService) ListAllEnabled(ctx context.Context) ([]models.UserCalendarBinding, error) {
	rows := []models.UserCalendarBinding{}
	if err := s.db.SelectContext(ctx, &rows,
		`SELECT `+bindingColumns+`
		   FROM paliad.user_calendar_bindings
		  WHERE enabled = true
		  ORDER BY user_id, created_at`); err != nil {
		return nil, fmt.Errorf("list all enabled bindings: %w", err)
	}
	return rows, nil
}

// Get returns one binding scoped to the user; ErrNotVisible when the row
// doesn't exist or belongs to someone else.
func (s *CalendarBindingService) Get(ctx context.Context, userID, bindingID uuid.UUID) (*models.UserCalendarBinding, error) {
	var b models.UserCalendarBinding
	err := s.db.GetContext(ctx, &b,
		`SELECT `+bindingColumns+`
		   FROM paliad.user_calendar_bindings
		  WHERE id = $1 AND user_id = $2`, bindingID, userID)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNotVisible
	}
	if err != nil {
		return nil, fmt.Errorf("get binding: %w", err)
	}
	return &b, nil
}

// CreateInput is the payload for POST /api/caldav-bindings. Slice 2b
// wires this; Slice 2a exposes Create for tests + SQL-equivalent
// integration tests.
type CreateBindingInput struct {
	CalendarPath    string     `json:"calendar_path"`
	DisplayName     string     `json:"display_name"`
	ScopeKind       string     `json:"scope_kind"`
	ScopeID         *uuid.UUID `json:"scope_id,omitempty"`
	IncludePersonal bool       `json:"include_personal"`
	Enabled         bool       `json:"enabled"`
}

// Create inserts a new binding. Validates scope_kind / scope_id
// combinatorics; returns ErrInvalidInput on a bad payload.
func (s *CalendarBindingService) Create(ctx context.Context, userID uuid.UUID, in CreateBindingInput) (*models.UserCalendarBinding, error) {
	if err := validateScope(in.ScopeKind, in.ScopeID); err != nil {
		return nil, err
	}
	if in.CalendarPath == "" {
		return nil, fmt.Errorf("%w: calendar_path is required", ErrInvalidInput)
	}
	now := time.Now().UTC()
	var b models.UserCalendarBinding
	err := s.db.GetContext(ctx, &b,
		`INSERT INTO paliad.user_calendar_bindings
		    (user_id, calendar_path, display_name, scope_kind, scope_id,
		     include_personal, enabled, created_at, updated_at)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8)
		 RETURNING `+bindingColumns,
		userID, in.CalendarPath, in.DisplayName, in.ScopeKind, in.ScopeID,
		in.IncludePersonal, in.Enabled, now)
	if err != nil {
		return nil, fmt.Errorf("insert binding: %w", err)
	}
	return &b, nil
}

// UpdateInput captures the PATCH-shaped fields. Pointer fields = "leave
// as-is when nil".
type UpdateBindingInput struct {
	DisplayName     *string    `json:"display_name,omitempty"`
	ScopeKind       *string    `json:"scope_kind,omitempty"`
	ScopeID         *uuid.UUID `json:"scope_id,omitempty"`
	IncludePersonal *bool      `json:"include_personal,omitempty"`
	Enabled         *bool      `json:"enabled,omitempty"`
}

// Update mutates the binding. Validates the resulting (scope_kind, scope_id)
// combinatorics if either field changes.
func (s *CalendarBindingService) Update(ctx context.Context, userID, bindingID uuid.UUID, in UpdateBindingInput) (*models.UserCalendarBinding, error) {
	existing, err := s.Get(ctx, userID, bindingID)
	if err != nil {
		return nil, err
	}
	if in.ScopeKind != nil || in.ScopeID != nil {
		kind := existing.ScopeKind
		if in.ScopeKind != nil {
			kind = *in.ScopeKind
		}
		var sid *uuid.UUID
		if in.ScopeID != nil {
			sid = in.ScopeID
		} else {
			sid = existing.ScopeID
		}
		if err := validateScope(kind, sid); err != nil {
			return nil, err
		}
	}
	sets := []string{"updated_at = NOW()"}
	args := []any{}
	next := 1
	addSet := func(col string, val any) {
		sets = append(sets, fmt.Sprintf("%s = $%d", col, next))
		args = append(args, val)
		next++
	}
	if in.DisplayName != nil {
		addSet("display_name", *in.DisplayName)
	}
	if in.ScopeKind != nil {
		addSet("scope_kind", *in.ScopeKind)
	}
	if in.ScopeID != nil {
		addSet("scope_id", *in.ScopeID)
	}
	if in.IncludePersonal != nil {
		addSet("include_personal", *in.IncludePersonal)
	}
	if in.Enabled != nil {
		addSet("enabled", *in.Enabled)
	}
	// Append WHERE clause args last.
	args = append(args, bindingID, userID)
	q := fmt.Sprintf(`UPDATE paliad.user_calendar_bindings
	                     SET %s
	                   WHERE id = $%d AND user_id = $%d
	                  RETURNING %s`, strings.Join(sets, ", "), next, next+1, bindingColumns)
	var b models.UserCalendarBinding
	if err := s.db.GetContext(ctx, &b, q, args...); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotVisible
		}
		return nil, fmt.Errorf("update binding: %w", err)
	}
	return &b, nil
}

// Delete removes the binding row. Caller is responsible for the remote
// .ics cleanup (CalDAVService handles that via §2.6 of the Slice 2 brief)
// before invoking this; this method is the bare DB delete.
func (s *CalendarBindingService) Delete(ctx context.Context, userID, bindingID uuid.UUID) error {
	res, err := s.db.ExecContext(ctx,
		`DELETE FROM paliad.user_calendar_bindings
		  WHERE id = $1 AND user_id = $2`, bindingID, userID)
	if err != nil {
		return fmt.Errorf("delete binding: %w", err)
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return ErrNotVisible
	}
	return nil
}

// SetSyncStatus is called by CalDAVService after each sync attempt for
// this binding. last_sync_error nil clears the previous error.
func (s *CalendarBindingService) SetSyncStatus(ctx context.Context, bindingID uuid.UUID, errStr *string) error {
	_, err := s.db.ExecContext(ctx,
		`UPDATE paliad.user_calendar_bindings
		    SET last_sync_at = NOW(), last_sync_error = $1, updated_at = NOW()
		  WHERE id = $2`, errStr, bindingID)
	if err != nil {
		return fmt.Errorf("update binding sync status: %w", err)
	}
	return nil
}

// validateScope mirrors the table's CHECK constraints — we duplicate
// the rule here so the API can return a useful 400 instead of letting
// Postgres reject the row with a generic check_violation.
func validateScope(kind string, scopeID *uuid.UUID) error {
	switch kind {
	case models.BindingScopeAllVisible, models.BindingScopePersonalOnly:
		if scopeID != nil {
			return fmt.Errorf("%w: scope_id must be NULL when scope_kind = %q", ErrInvalidInput, kind)
		}
	case models.BindingScopeProject, models.BindingScopeClient, models.BindingScopeLitigation, models.BindingScopePatent, models.BindingScopeCase:
		if scopeID == nil {
			return fmt.Errorf("%w: scope_id is required when scope_kind = %q", ErrInvalidInput, kind)
		}
	default:
		return fmt.Errorf("%w: unknown scope_kind %q", ErrInvalidInput, kind)
	}
	return nil
}

