package services

import "errors"

// Strict-ladder helpers for the 4-Augen-Prüfung approval gate. The ladder
// drives both the t-paliad-138 single-value `required_role` policy
// grammar and the t-paliad-148 (profession, responsibility) tuple-with-
// gate evaluation in paliad.user_project_authority_level().
//
// The ladder values match paliad.approval_role_level(text) in migration
// 057. Higher level always satisfies lower; level 0 means ineligible to
// approve at any level.

// Profession values on paliad.users.profession. Drive the ladder. NULL is
// represented as the empty string in Go (`*string` nil) — the ladder
// returns 0 for unknown values, including empty.
const (
	ProfessionPartner    = "partner"
	ProfessionOfCounsel  = "of_counsel"
	ProfessionAssociate  = "associate"
	ProfessionSeniorPA   = "senior_pa"
	ProfessionPA         = "pa"
	ProfessionParalegal  = "paralegal"
)

// Project-level responsibility values on paliad.project_teams.responsibility.
// Open the ladder gate (lead/member) or close it (observer/external).
const (
	ResponsibilityLead     = "lead"
	ResponsibilityMember   = "member"
	ResponsibilityObserver = "observer"
	ResponsibilityExternal = "external"
)

// RoleSeniorPA — kept as the legacy constant from t-paliad-138 for any
// remaining reference site that hasn't migrated. Equal to ProfessionSeniorPA.
const RoleSeniorPA = ProfessionSeniorPA

// EntityType values for the polymorphic approval workflow.
const (
	EntityTypeDeadline    = "deadline"
	EntityTypeAppointment = "appointment"
)

// LifecycleEvent values matching paliad.approval_policies.lifecycle_event
// and paliad.approval_requests.lifecycle_event CHECK constraints.
const (
	LifecycleCreate   = "create"
	LifecycleUpdate   = "update"
	LifecycleComplete = "complete"
	LifecycleDelete   = "delete"
)

// ApprovalStatus values on paliad.deadlines.approval_status and
// paliad.appointments.approval_status.
const (
	ApprovalStatusApproved = "approved"
	ApprovalStatusPending  = "pending"
	ApprovalStatusLegacy   = "legacy"
)

// RequestStatus values on paliad.approval_requests.status.
const (
	RequestStatusPending          = "pending"
	RequestStatusApproved         = "approved"
	RequestStatusRejected         = "rejected"
	RequestStatusRevoked          = "revoked"
	RequestStatusSuperseded       = "superseded"
	RequestStatusChangesRequested = "changes_requested"
)

// DecisionKind discriminates 'peer' (normal in-team sign-off) from
// 'admin_override' (global_admin used the escape-hatch path) and
// 'derived_peer' (a partner-unit-derived member with authority signed off
// — added by t-paliad-139 / migration 055). Verlauf chronology renders
// these distinctly.
const (
	DecisionKindPeer          = "peer"
	DecisionKindAdminOverride = "admin_override"
	DecisionKindDerivedPeer   = "derived_peer"
)

// professionLevel maps a profession value to its strict-ladder level.
// Mirrors paliad.approval_role_level(text). NULL profession (empty
// string) returns 0 — explicit so the trap is visible.
//
//	5: partner          — firm-tier ceiling (replaces legacy 'lead')
//	4: of_counsel
//	3: associate        ← default required level on new policies
//	2: senior_pa
//	1: pa
//	0: paralegal / "" / unknown — ineligible to approve
//
// CRITICAL: do not silently default NULL/empty to 'associate'. NULL
// profession means "no firm tier", which is the explicit signal that
// the user (e.g. external local counsel) cannot satisfy any tier.
// Test: TestProfessionLevel_NilIsZero pins this behaviour.
func professionLevel(profession string) int {
	switch profession {
	case ProfessionPartner:
		return 5
	case ProfessionOfCounsel:
		return 4
	case ProfessionAssociate:
		return 3
	case ProfessionSeniorPA:
		return 2
	case ProfessionPA:
		return 1
	default:
		return 0
	}
}

// responsibilityOpensGate returns true iff the project responsibility
// opens the approval gate. Mirrors the SQL predicate
// `pt.responsibility IN ('lead','member')` used by
// paliad.user_project_authority_level().
func responsibilityOpensGate(responsibility string) bool {
	return responsibility == ResponsibilityLead || responsibility == ResponsibilityMember
}

// IsValidRequiredRole returns true iff the role can be set as a policy's
// required_role (i.e. it has a non-zero strict-ladder level). Used by
// the policy-authoring page to validate the dropdown value.
func IsValidRequiredRole(role string) bool {
	return professionLevel(role) > 0
}

// IsValidProfession returns true iff the value is one of the recognised
// profession enum values. Empty string is intentionally rejected — the
// service layer represents NULL as a *string nil, not as "".
func IsValidProfession(p string) bool {
	switch p {
	case ProfessionPartner, ProfessionOfCounsel, ProfessionAssociate,
		ProfessionSeniorPA, ProfessionPA, ProfessionParalegal:
		return true
	}
	return false
}

// IsValidResponsibility returns true iff the value is one of the
// recognised project-responsibility enum values. Used by TeamService.
func IsValidResponsibility(r string) bool {
	switch r {
	case ResponsibilityLead, ResponsibilityMember,
		ResponsibilityObserver, ResponsibilityExternal:
		return true
	}
	return false
}

// Approval-flow errors. Handlers map these to the right HTTP status:
//
//	ErrSelfApproval            -> 403
//	ErrNoQualifiedApprover     -> 409 (with required_role hint)
//	ErrConcurrentPending       -> 409 (with the existing request id hint)
//	ErrNotApprover             -> 403
//	ErrRequestNotPending       -> 409
//	ErrUnknownEntityType       -> 500 (programming error)
var (
	ErrSelfApproval               = errors.New("self-approval blocked")
	ErrNoQualifiedApprover        = errors.New("no qualified approver available")
	ErrConcurrentPending          = errors.New("entity already has a pending approval request")
	ErrNotApprover                = errors.New("not authorized to approve this request")
	ErrRequestNotPending          = errors.New("request is not pending")
	ErrUnknownEntityType          = errors.New("unknown entity type")
	ErrSuggestionRequiresChange   = errors.New("suggestion requires a counter_payload diff or a note")
	ErrSuggestionLifecycleInvalid = errors.New("suggest-changes is only valid for update / complete lifecycles")
)

// PendingApprovalError wraps ErrConcurrentPending with the in-flight
// request's id + required role, so handlers can render a 409 body that
// tells the user which request is blocking them and lets the UI offer
// a "withdraw" affordance pointing at that request.
//
// Construct via NewPendingApprovalError(requestID, requiredRole). Unwraps
// to ErrConcurrentPending so existing errors.Is() checks still work.
type PendingApprovalError struct {
	RequestID    string
	RequiredRole string
}

func (e *PendingApprovalError) Error() string {
	if e.RequestID == "" {
		return ErrConcurrentPending.Error()
	}
	return ErrConcurrentPending.Error() + ": request_id=" + e.RequestID
}

func (e *PendingApprovalError) Unwrap() error { return ErrConcurrentPending }

// NewPendingApprovalError builds a PendingApprovalError for an entity row
// whose pending_request_id is non-nil. requestID may be the empty string
// when the entity row's pending_request_id is unexpectedly NULL — the
// error still works as a generic ErrConcurrentPending in that case.
func NewPendingApprovalError(requestID, requiredRole string) *PendingApprovalError {
	return &PendingApprovalError{RequestID: requestID, RequiredRole: requiredRole}
}
