package services

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

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

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

// EventChoiceService reads and writes paliad.project_event_choices —
// per-event-card user picks scoped to a project (t-paliad-265 /
// m/paliad#96). Three choice kinds today:
//
//	appellant   — claimant | defendant | both | none
//	include_ccr — true | false
//	skip        — true | false
//
// Visibility follows paliad.can_see_project (via ProjectService.CanSee).
// Audits via paliad.system_audit_log with event_type=project_event_choice.set
// (insert/update) or .deleted (delete).
//
// The CRUD surface is intentionally tight: List for a project (one read),
// Upsert one (idempotent re-pick), Delete one (kind-scoped). The
// projection engine receives the choices via ToCalcOptionsAddendum,
// which folds them into CalcOptions before Calculate runs.
type EventChoiceService struct {
	db       *sqlx.DB
	projects *ProjectService
	users    *UserService
}

func NewEventChoiceService(db *sqlx.DB, projects *ProjectService, users *UserService) *EventChoiceService {
	return &EventChoiceService{db: db, projects: projects, users: users}
}

// Allowed choice kinds + per-kind value namespaces. Validated server-side
// before any write; the DB CHECK constraint catches the same shape but
// the early validation gives a friendlier error and short-circuits the
// transaction.
var (
	allowedChoiceKinds = map[string]map[string]struct{}{
		"appellant":   {"claimant": {}, "defendant": {}, "both": {}, "none": {}},
		"include_ccr": {"true": {}, "false": {}},
		"skip":        {"true": {}, "false": {}},
	}
)

func validateChoice(kind, value string) error {
	values, ok := allowedChoiceKinds[kind]
	if !ok {
		return fmt.Errorf("%w: unknown choice_kind %q", ErrInvalidInput, kind)
	}
	if _, ok := values[value]; !ok {
		return fmt.Errorf("%w: invalid choice_value %q for kind %q", ErrInvalidInput, value, kind)
	}
	return nil
}

// ListForProject returns every choice row for the given project. Caller
// must hold visibility on the project.
func (s *EventChoiceService) ListForProject(ctx context.Context, userID, projectID uuid.UUID) ([]models.ProjectEventChoice, error) {
	if err := s.requireProjectVisible(ctx, userID, projectID); err != nil {
		return nil, err
	}
	out := []models.ProjectEventChoice{}
	err := s.db.SelectContext(ctx, &out,
		`SELECT id, project_id, submission_code, choice_kind, choice_value,
		        created_by, created_at, updated_by, updated_at
		   FROM paliad.project_event_choices
		  WHERE project_id = $1
		  ORDER BY submission_code, choice_kind`, projectID)
	if err != nil {
		return nil, fmt.Errorf("list event choices: %w", err)
	}
	return out, nil
}

// UpsertInput is the body shape for an upsert.
type UpsertEventChoiceInput struct {
	SubmissionCode string `json:"submission_code"`
	ChoiceKind     string `json:"choice_kind"`
	ChoiceValue    string `json:"choice_value"`
}

// Upsert inserts or updates one (project, submission_code, choice_kind)
// row. Audit-log row written in the same tx.
func (s *EventChoiceService) Upsert(ctx context.Context, userID, projectID uuid.UUID, input UpsertEventChoiceInput) (*models.ProjectEventChoice, error) {
	if err := s.requireProjectVisible(ctx, userID, projectID); err != nil {
		return nil, err
	}
	if input.SubmissionCode == "" {
		return nil, fmt.Errorf("%w: submission_code required", ErrInvalidInput)
	}
	if err := validateChoice(input.ChoiceKind, input.ChoiceValue); err != nil {
		return nil, err
	}

	actorEmail, err := s.actorEmail(ctx, userID)
	if err != nil {
		return nil, err
	}

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

	if _, err := tx.ExecContext(ctx,
		`SELECT set_config('paliad.audit_reason',
		     'project_event_choice.set ('||$1||','||$2||','||$3||')', true)`,
		input.SubmissionCode, input.ChoiceKind, input.ChoiceValue); err != nil {
		return nil, fmt.Errorf("set audit reason: %w", err)
	}

	var row models.ProjectEventChoice
	err = tx.GetContext(ctx, &row,
		`INSERT INTO paliad.project_event_choices
		     (project_id, submission_code, choice_kind, choice_value, created_by, updated_by)
		 VALUES ($1, $2, $3, $4, $5, $5)
		 ON CONFLICT (project_id, submission_code, choice_kind)
		 DO UPDATE SET choice_value = EXCLUDED.choice_value,
		               updated_by   = EXCLUDED.updated_by,
		               updated_at   = now()
		 RETURNING id, project_id, submission_code, choice_kind, choice_value,
		           created_by, created_at, updated_by, updated_at`,
		projectID, input.SubmissionCode, input.ChoiceKind, input.ChoiceValue, userID)
	if err != nil {
		return nil, fmt.Errorf("upsert event choice: %w", err)
	}

	if err := writeChoiceAudit(ctx, tx, "project_event_choice.set", userID, actorEmail, projectID, input.SubmissionCode, input.ChoiceKind, input.ChoiceValue); err != nil {
		return nil, err
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit upsert: %w", err)
	}
	return &row, nil
}

// Delete removes the (project, submission_code, choice_kind) row.
// Returns ErrNotVisible if the project isn't visible OR the row didn't
// exist (no leak between the two).
func (s *EventChoiceService) Delete(ctx context.Context, userID, projectID uuid.UUID, submissionCode, choiceKind string) error {
	if err := s.requireProjectVisible(ctx, userID, projectID); err != nil {
		return err
	}
	if submissionCode == "" || choiceKind == "" {
		return fmt.Errorf("%w: submission_code + choice_kind required", ErrInvalidInput)
	}
	if _, ok := allowedChoiceKinds[choiceKind]; !ok {
		return fmt.Errorf("%w: unknown choice_kind %q", ErrInvalidInput, choiceKind)
	}

	actorEmail, err := s.actorEmail(ctx, userID)
	if err != nil {
		return err
	}

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

	if _, err := tx.ExecContext(ctx,
		`SELECT set_config('paliad.audit_reason',
		     'project_event_choice.deleted ('||$1||','||$2||')', true)`,
		submissionCode, choiceKind); err != nil {
		return fmt.Errorf("set audit reason: %w", err)
	}

	res, err := tx.ExecContext(ctx,
		`DELETE FROM paliad.project_event_choices
		  WHERE project_id = $1 AND submission_code = $2 AND choice_kind = $3`,
		projectID, submissionCode, choiceKind)
	if err != nil {
		return fmt.Errorf("delete event choice: %w", err)
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return ErrNotVisible
	}
	if err := writeChoiceAudit(ctx, tx, "project_event_choice.deleted", userID, actorEmail, projectID, submissionCode, choiceKind, ""); err != nil {
		return err
	}
	return tx.Commit()
}

// CalcOptionsAddendum is the per-card slice of CalcOptions, built from
// the persisted choices. ProjectionService folds these into the parent
// CalcOptions before Calculate runs.
type CalcOptionsAddendum struct {
	PerCardAppellant map[string]string   // submission_code → appellant value
	SkipRules        map[string]struct{} // set of submission_code
	IncludeCCRFor    map[string]struct{} // set of submission_code
}

// ToCalcOptionsAddendum converts a list of choices into the calc-options
// shape. Empty input yields an addendum whose maps are non-nil but empty
// so callers can use map indexing without nil checks.
func ToCalcOptionsAddendum(choices []models.ProjectEventChoice) CalcOptionsAddendum {
	out := CalcOptionsAddendum{
		PerCardAppellant: map[string]string{},
		SkipRules:        map[string]struct{}{},
		IncludeCCRFor:    map[string]struct{}{},
	}
	for _, c := range choices {
		switch c.ChoiceKind {
		case "appellant":
			out.PerCardAppellant[c.SubmissionCode] = c.ChoiceValue
		case "skip":
			if c.ChoiceValue == "true" {
				out.SkipRules[c.SubmissionCode] = struct{}{}
			}
		case "include_ccr":
			if c.ChoiceValue == "true" {
				out.IncludeCCRFor[c.SubmissionCode] = struct{}{}
			}
		}
	}
	return out
}

// writeChoiceAudit inserts a project-scoped row into paliad.system_audit_log
// with the choice details in metadata. Same shape as the data-export +
// checklist audit writers.
func writeChoiceAudit(ctx context.Context, tx *sqlx.Tx, eventType string, actorID uuid.UUID, actorEmail string, projectID uuid.UUID, submissionCode, choiceKind, choiceValue string) error {
	if _, err := tx.ExecContext(ctx,
		`INSERT INTO paliad.system_audit_log
		     (event_type, actor_id, actor_email, scope, scope_root, metadata)
		 VALUES ($1, $2, $3, 'project', $4,
		         jsonb_build_object(
		             'submission_code', $5::text,
		             'choice_kind',     $6::text,
		             'choice_value',    $7::text
		         ))`,
		eventType, actorID, actorEmail, projectID, submissionCode, choiceKind, choiceValue); err != nil {
		return fmt.Errorf("audit insert: %w", err)
	}
	return nil
}

func (s *EventChoiceService) actorEmail(ctx context.Context, userID uuid.UUID) (string, error) {
	var email string
	err := s.db.GetContext(ctx, &email,
		`SELECT email FROM paliad.users WHERE id = $1`, userID)
	if errors.Is(err, sql.ErrNoRows) {
		return "", ErrNotVisible
	}
	if err != nil {
		return "", fmt.Errorf("lookup actor: %w", err)
	}
	return email, nil
}

func (s *EventChoiceService) requireProjectVisible(ctx context.Context, userID, projectID uuid.UUID) error {
	visible, err := s.projects.CanSee(ctx, userID, projectID)
	if err != nil {
		return err
	}
	if !visible {
		return ErrNotVisible
	}
	return nil
}
