package namedcredential

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"regexp"
	"strings"
	"time"

	"github.com/pingcap-inc/nexuscrm-contract/pkg/constants"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/models"
)

var (
	// ErrNotFound is returned when a named credential or secret does not exist.
	ErrNotFound = errors.New("named credential not found")
	// ErrPermissionDenied is returned when the caller lacks ManageNamedCredentials.
	ErrPermissionDenied = errors.New("named credential permission denied")
	// ErrInvalidDefinition is returned when a Definition fails validation.
	ErrInvalidDefinition = errors.New("invalid named credential definition")
	// ErrInvalidSecret is returned when a secret write fails validation.
	ErrInvalidSecret = errors.New("invalid named credential secret")
	// ErrConnectorPackInactive is returned by ResolveCredentialValue when the
	// credential's OwnerPack is set but the connector pack is inactive or unknown.
	ErrConnectorPackInactive = errors.New("named credential connector pack inactive or unknown")
)

// developerNameRE matches Salesforce-style API names: starts with a letter,
// followed by up to 239 alphanumeric/underscore characters (max 240 total).
var developerNameRE = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{0,239}$`)

// validAuthTypes is the exhaustive set of supported AuthType values.
var validAuthTypes = map[string]bool{
	AuthTypeOAuth2:      true,
	AuthTypeBasicAuth:   true,
	AuthTypeBearerToken: true,
	AuthTypeCustom:      true,
}

// PermissionChecker is the subset of the permissions service used by Service.
// *permissions.PermissionService satisfies this interface via CheckSystemPermission.
type PermissionChecker interface {
	CheckSystemPermission(ctx context.Context, user *models.UserSession, permission string) bool
}

// PackResolver verifies a connector-pack is active before runtime callout code
// reads a NamedCredential's secret value. Concrete impl lives in
// backend/internal/controlplane/packs (ResolveConnectorPackByNamespace).
type PackResolver interface {
	ResolveConnectorPackByNamespace(ctx context.Context, namespace string) (active bool, err error)
	ResolveDrainOnlyConnectorPackByInstall(ctx context.Context, namespace, installID string) (allowed bool, err error)
}

// Service provides the NamedCredential substrate operations.
// Definition reads/writes are open to any authenticated caller with ViewSetup.
// Secret reads/writes require ManageNamedCredentials.
type Service struct {
	repo  repository
	perms PermissionChecker
	packs PackResolver
}

// NewService constructs a Service with the given repository and permission checker.
// The PackResolver is nil; pack-scoped credentials will fail closed in ResolveCredentialValue.
func NewService(repo repository, perms PermissionChecker) *Service {
	return &Service{repo: repo, perms: perms}
}

// NewServiceWithPacks constructs a Service with pack-active-state gating for ResolveCredentialValue.
func NewServiceWithPacks(repo repository, perms PermissionChecker, packs PackResolver) *Service {
	return &Service{repo: repo, perms: perms, packs: packs}
}

func (s *Service) resolvePackAccess(ctx context.Context, ownerPack string) (bool, error) {
	ownerPack = strings.TrimSpace(ownerPack)
	if ownerPack == "" {
		return true, nil
	}
	if s.packs == nil {
		return false, nil
	}
	if namespace, installID, ok := DrainOnlyInstalledPackFromContext(ctx); ok {
		if namespace != ownerPack {
			return false, nil
		}
		return s.packs.ResolveDrainOnlyConnectorPackByInstall(ctx, namespace, installID)
	}
	active, err := s.packs.ResolveConnectorPackByNamespace(ctx, ownerPack)
	if err != nil {
		return false, err
	}
	return active, nil
}

// ListDefinitions returns all named credential definitions. No secrets are included.
// Requires ViewSetup permission — the platform baseline for any setup-object read.
func (s *Service) ListDefinitions(ctx context.Context, user *models.UserSession) ([]Definition, error) {
	if !s.perms.CheckSystemPermission(ctx, user, string(constants.PermViewSetup)) {
		return nil, ErrPermissionDenied
	}
	return s.repo.ListDefinitions(ctx)
}

// GetDefinition returns the named credential definition for devName.
// Requires ViewSetup permission — the platform baseline for any setup-object read.
// Returns ErrNotFound if no definition exists with that DeveloperName.
func (s *Service) GetDefinition(ctx context.Context, user *models.UserSession, devName string) (*Definition, error) {
	if !s.perms.CheckSystemPermission(ctx, user, string(constants.PermViewSetup)) {
		return nil, ErrPermissionDenied
	}
	d, err := s.repo.GetDefinitionByDeveloperName(ctx, devName)
	if err != nil {
		return nil, err
	}
	if d == nil {
		return nil, ErrNotFound
	}
	return d, nil
}

// UpsertDefinition validates and upserts a named credential definition.
// Requires ManageNamedCredentials permission. The table is BLOCKED at the
// generic CRUD layer (see system_tables.json), so this service is the only
// valid write path — every definition mutation goes through validateDefinition
// here and through the permission gate.
// Secret values are never part of this call — definitions can be installed
// without any secret (satisfies AC 1).
func (s *Service) UpsertDefinition(ctx context.Context, user *models.UserSession, def Definition) (Definition, error) {
	if !s.perms.CheckSystemPermission(ctx, user, string(constants.PermManageNamedCredentials)) {
		return Definition{}, ErrPermissionDenied
	}
	if err := s.rejectPackOwnedAdminMutation(ctx, def.DeveloperName); err != nil {
		return Definition{}, err
	}
	// Admin write path cannot author OwnerPack — that field is reserved for the
	// pack-install pipeline (future NCRM-INT-04 ComponentKind installer). Clearing
	// it here means admin-authored definitions skip the connector-pack active-state
	// gate in ResolveCredentialValue (intended for unscoped admin-installed creds).
	// When the pack-install pipeline lands it will use a separate write path
	// (e.g. Service.UpsertDefinitionAsPack) that DOES author OwnerPack.
	def.OwnerPack = ""
	if err := validateDefinition(def); err != nil {
		return Definition{}, err
	}
	if err := s.repo.UpsertDefinition(ctx, &def); err != nil {
		return Definition{}, err
	}
	return def, nil
}

// DeleteDefinition removes a named credential definition by DeveloperName.
// Requires ManageNamedCredentials permission. Definition delete CASCADEs to
// the secret rows via the FK on _System_NamedCredentialSecret.
func (s *Service) DeleteDefinition(ctx context.Context, user *models.UserSession, devName string) error {
	if !s.perms.CheckSystemPermission(ctx, user, string(constants.PermManageNamedCredentials)) {
		return ErrPermissionDenied
	}
	if err := s.rejectPackOwnedAdminMutation(ctx, devName); err != nil {
		return err
	}
	return s.repo.DeleteDefinition(ctx, devName)
}

func (s *Service) rejectPackOwnedAdminMutation(ctx context.Context, devName string) error {
	existing, err := s.repo.GetDefinitionByDeveloperName(ctx, devName)
	if err != nil {
		return err
	}
	if existing != nil && existing.OwnerPack != "" {
		return fmt.Errorf("namedcredential: admin mutation denied for pack-owned definition %q (OwnerPack=%q): %w", devName, existing.OwnerPack, ErrPermissionDenied)
	}
	return nil
}

// ListSecretMetadata returns the masked metadata (kind, hint, rotatedAt) for
// all secrets bound to devName. Requires ViewSetup permission — the platform
// baseline for any setup-object read. No plaintext SecretValue is ever returned
// by this method (AC 4). Masked values remain accessible to all setup-permitted
// callers without needing ManageNamedCredentials.
func (s *Service) ListSecretMetadata(ctx context.Context, user *models.UserSession, devName string) ([]SecretMetadata, error) {
	if !s.perms.CheckSystemPermission(ctx, user, string(constants.PermViewSetup)) {
		return nil, ErrPermissionDenied
	}
	d, err := s.repo.GetDefinitionByDeveloperName(ctx, devName)
	if err != nil {
		return nil, err
	}
	if d == nil {
		return nil, ErrNotFound
	}
	return s.repo.ListSecretMetadataByDefinitionID(ctx, d.ID)
}

// GetSecret returns the plaintext secret value for (devName, kind).
// Requires ManageNamedCredentials permission (AC 6).
// Returns ErrPermissionDenied, ErrNotFound, or the secret.
func (s *Service) GetSecret(ctx context.Context, user *models.UserSession, devName, kind string) (*Secret, error) {
	if !s.perms.CheckSystemPermission(ctx, user, string(constants.PermManageNamedCredentials)) {
		return nil, ErrPermissionDenied
	}
	d, err := s.repo.GetDefinitionByDeveloperName(ctx, devName)
	if err != nil {
		return nil, err
	}
	if d == nil {
		return nil, ErrNotFound
	}
	sec, err := s.repo.GetSecret(ctx, d.ID, kind)
	if err != nil {
		return nil, err
	}
	if sec == nil {
		return nil, ErrNotFound
	}
	return sec, nil
}

// SetSecret writes or rotates a secret for (devName, kind).
// Requires ManageNamedCredentials permission (AC 6).
// Computes MaskedHint from the last 4 runes of plaintext.
// The definition row is never modified by this call (AC 2).
func (s *Service) SetSecret(ctx context.Context, user *models.UserSession, devName, kind, plaintext string) (SecretMetadata, error) {
	if !s.perms.CheckSystemPermission(ctx, user, string(constants.PermManageNamedCredentials)) {
		return SecretMetadata{}, ErrPermissionDenied
	}
	if strings.TrimSpace(plaintext) == "" {
		return SecretMetadata{}, ErrInvalidSecret
	}
	d, err := s.repo.GetDefinitionByDeveloperName(ctx, devName)
	if err != nil {
		return SecretMetadata{}, err
	}
	if d == nil {
		return SecretMetadata{}, ErrNotFound
	}
	if err := validateSecretKindForAuthType(d.AuthType, kind); err != nil {
		return SecretMetadata{}, err
	}

	hint := maskedHint(plaintext)
	sec := &Secret{
		NamedCredentialID: d.ID,
		SecretKind:        kind,
		SecretValue:       plaintext,
		MaskedHint:        hint,
	}
	if err := s.repo.UpsertSecret(ctx, sec); err != nil {
		return SecretMetadata{}, err
	}
	return SecretMetadata{
		SecretKind: kind,
		MaskedHint: hint,
		RotatedAt:  sec.RotatedAt,
	}, nil
}

func validateSecretKindForAuthType(authType, kind string) error {
	if kind == "" {
		return ErrInvalidSecret
	}
	switch authType {
	case "", AuthTypeBearerToken, AuthTypeOAuth2:
		if kind != "Token" {
			return ErrInvalidSecret
		}
	case AuthTypeBasicAuth:
		if kind != "Username" && kind != "Password" {
			return ErrInvalidSecret
		}
	case AuthTypeCustom:
		return nil
	default:
		return ErrInvalidSecret
	}
	return nil
}

// RequiredSecretKindsForAuthType returns the complete secret-kind set required
// for auth types whose credential completeness can be answered authoritatively
// by the current NamedCredential model. Custom auth remains false because the
// platform has no per-definition required-kind metadata for arbitrary schemes.
func RequiredSecretKindsForAuthType(authType string) ([]string, bool) {
	switch authType {
	case "", AuthTypeBearerToken, AuthTypeOAuth2:
		return []string{"Token"}, true
	case AuthTypeBasicAuth:
		return []string{"Username", "Password"}, true
	case AuthTypeCustom:
		return nil, false
	default:
		return nil, false
	}
}

// HasRequiredSecrets reports whether devName has all required, locally usable
// secret values for its AuthType. It verifies the same decryptability substrate
// used by runtime callouts, but never returns plaintext to connector health.
func (s *Service) HasRequiredSecrets(ctx context.Context, devName string) (bool, error) {
	d, err := s.repo.GetDefinitionByDeveloperName(ctx, devName)
	if err != nil {
		return false, err
	}
	if d == nil {
		return false, nil
	}
	if d.OwnerPack != "" {
		active, err := s.resolvePackAccess(ctx, d.OwnerPack)
		if err != nil {
			return false, err
		}
		if !active {
			return false, nil
		}
	}
	required, ok := RequiredSecretKindsForAuthType(d.AuthType)
	if !ok || len(required) == 0 {
		return false, nil
	}
	for _, kind := range required {
		secret, err := s.repo.GetSecret(ctx, d.ID, kind)
		if err != nil {
			return false, nil
		}
		if secret == nil || strings.TrimSpace(secret.SecretValue) == "" {
			return false, nil
		}
	}
	return true, nil
}

// validateDefinition checks required fields and enum values for a Definition.
func validateDefinition(def Definition) error {
	if !developerNameRE.MatchString(def.DeveloperName) {
		return ErrInvalidDefinition
	}
	if def.Label == "" {
		return ErrInvalidDefinition
	}
	if def.Endpoint == "" {
		return ErrInvalidDefinition
	}
	u, err := url.Parse(def.Endpoint)
	if err != nil {
		return ErrInvalidDefinition
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return ErrInvalidDefinition
	}
	if u.Host == "" {
		return ErrInvalidDefinition
	}
	if credentialedAuthTypeRequiresHTTPS(def.AuthType) && strings.EqualFold(u.Scheme, "http") {
		return ErrInvalidDefinition
	}
	// Reject every URL substring that can smuggle secret material into the
	// less-gated definition row, bypassing the _System_NamedCredentialSecret
	// split. The definition row is intended to hold base-URL metadata only
	// (scheme://host/path); credential material belongs in the encrypted
	// _System_NamedCredentialSecret store and must be added at runtime by the
	// callout layer, not stored as part of Endpoint.
	if u.User != nil {
		// user:pass@host or user@host
		return ErrInvalidDefinition
	}
	if u.RawQuery != "" {
		// ?api_key=secret etc.
		return ErrInvalidDefinition
	}
	if u.Fragment != "" {
		// #token=... (some OAuth flows surface tokens in fragments)
		return ErrInvalidDefinition
	}
	if u.Opaque != "" {
		// Non-hierarchical URL bodies (rare, but accepted by url.Parse for
		// schemes like "scheme:opaque-data"); refuse to store opaque payloads.
		return ErrInvalidDefinition
	}
	if def.PrincipalType != PrincipalTypeNamedPrincipal {
		return ErrInvalidDefinition
	}
	if !validAuthTypes[def.AuthType] {
		return ErrInvalidDefinition
	}
	return nil
}

func credentialedAuthTypeRequiresHTTPS(authType string) bool {
	switch authType {
	case "", AuthTypeBearerToken, AuthTypeOAuth2, AuthTypeBasicAuth:
		return true
	default:
		return false
	}
}

// ResolveCredentialValue returns the plaintext secret value for (devName, kind).
// Intended for server-internal callout code only — never exposed via admin API.
// Applies the active-state gate: if the definition's OwnerPack is set, the
// connector pack must resolve as active via PackResolver; otherwise returns
// ErrConnectorPackInactive. If OwnerPack is empty, the gate is skipped (admin-
// installed credentials without pack scoping are allowed).
// Returns ErrNotFound for missing definition or secret.
func (s *Service) ResolveCredentialValue(ctx context.Context, devName, kind string) (value string, retErr error) {
	started := time.Now()
	var definitionMS, packAccessMS, secretMS int64
	ownerPack := ""
	defer func() {
		logNamedCredentialRuntimeResolveTiming(ctx, "ResolveCredentialValue", devName, kind, ownerPack, started, retErr, definitionMS, packAccessMS, secretMS)
	}()

	stageStart := time.Now()
	d, err := s.repo.GetDefinitionByDeveloperName(ctx, devName)
	definitionMS = namedCredentialTimingMillis(stageStart)
	if err != nil {
		return "", err
	}
	if d == nil {
		return "", ErrNotFound
	}
	ownerPack = d.OwnerPack

	if d.OwnerPack != "" {
		stageStart = time.Now()
		active, err := s.resolvePackAccess(ctx, d.OwnerPack)
		packAccessMS = namedCredentialTimingMillis(stageStart)
		if err != nil {
			return "", err
		}
		if !active {
			return "", ErrConnectorPackInactive
		}
	}

	stageStart = time.Now()
	sec, err := s.repo.GetSecret(ctx, d.ID, kind)
	secretMS = namedCredentialTimingMillis(stageStart)
	if err != nil {
		return "", err
	}
	if sec == nil {
		return "", ErrNotFound
	}
	return sec.SecretValue, nil
}

// ResolveEndpoint returns the base-URL endpoint for the named credential
// identified by devName. Intended for server-internal callout substrate only
// (e.g. connector_callout.Engine) — never exposed via admin API.
//
// This method bypasses the ViewSetup-gated admin GetDefinition path by design:
// outbound callout code runs in a server-internal context with no UserSession.
//
// Applies the same pack-active gate as ResolveCredentialValue: if the
// definition's OwnerPack is set, the connector pack must resolve as active;
// otherwise returns ErrConnectorPackInactive. Admin-installed credentials
// (OwnerPack == "") skip the pack-active gate.
//
// Returns ErrNotFound when no definition exists with devName.
func (s *Service) ResolveEndpoint(ctx context.Context, devName string) (value string, retErr error) {
	started := time.Now()
	var definitionMS, packAccessMS int64
	ownerPack := ""
	defer func() {
		logNamedCredentialRuntimeResolveTiming(ctx, "ResolveEndpoint", devName, "", ownerPack, started, retErr, definitionMS, packAccessMS, 0)
	}()

	stageStart := time.Now()
	d, err := s.repo.GetDefinitionByDeveloperName(ctx, devName)
	definitionMS = namedCredentialTimingMillis(stageStart)
	if err != nil {
		return "", err
	}
	if d == nil {
		return "", ErrNotFound
	}
	ownerPack = d.OwnerPack

	if d.OwnerPack != "" {
		stageStart = time.Now()
		active, err := s.resolvePackAccess(ctx, d.OwnerPack)
		packAccessMS = namedCredentialTimingMillis(stageStart)
		if err != nil {
			return "", err
		}
		if !active {
			return "", ErrConnectorPackInactive
		}
	}

	return d.Endpoint, nil
}

// ResolveAuthType returns the AuthType string for the named credential
// identified by devName. Intended for server-internal callout substrate only
// (e.g. connector_callout.Engine) — never exposed via admin API.
//
// Applies the same pack-active gate as ResolveEndpoint: if the definition's
// OwnerPack is set, the connector pack must resolve as active.
//
// Returns ErrNotFound when no definition exists with devName.
func (s *Service) ResolveAuthType(ctx context.Context, devName string) (value string, retErr error) {
	started := time.Now()
	var definitionMS, packAccessMS int64
	ownerPack := ""
	defer func() {
		logNamedCredentialRuntimeResolveTiming(ctx, "ResolveAuthType", devName, "", ownerPack, started, retErr, definitionMS, packAccessMS, 0)
	}()

	stageStart := time.Now()
	d, err := s.repo.GetDefinitionByDeveloperName(ctx, devName)
	definitionMS = namedCredentialTimingMillis(stageStart)
	if err != nil {
		return "", err
	}
	if d == nil {
		return "", ErrNotFound
	}
	ownerPack = d.OwnerPack

	if d.OwnerPack != "" {
		stageStart = time.Now()
		active, err := s.resolvePackAccess(ctx, d.OwnerPack)
		packAccessMS = namedCredentialTimingMillis(stageStart)
		if err != nil {
			return "", err
		}
		if !active {
			return "", ErrConnectorPackInactive
		}
	}

	return d.AuthType, nil
}

// ResolveCredentialPack returns the canonical identity (stable row ID),
// OwnerPack namespace, and active state for devName, without decrypting or
// fetching the secret value. Server-internal — callers are platform substrate
// (e.g. NCRM-INT-04B connector_throttle), never admin APIs.
//
// Semantics:
//   - Definition row missing                  → ("", "", false, nil)  — fail-closed
//   - OwnerPack == "" (admin-installed)        → (d.ID, "", true, nil) — eligible; throttler falls back to DefaultRate/DefaultBurst
//   - OwnerPack != "" and pack inactive/missing→ (d.ID, OwnerPack, false, nil) — fail-closed
//   - OwnerPack != "" and pack active          → (d.ID, OwnerPack, true, nil)
//
// canonicalID is the Definition row's stable primary key (ID). Because
// _System_NamedCredential.DeveloperName uses utf8mb4_unicode_ci collation,
// "MyCred" and "mycred" resolve to the same definition row and therefore
// return the same canonicalID. Callers (connector_throttle.Service) use
// canonicalID as the bucket key so that case-variant credentialRefs share
// one token bucket and cannot bypass per-credential quota.
//
// Admin-installed credentials are throttle-eligible because they are valid
// NamedCredentials per the issue contract. Their rate-limit cannot be
// configured via L2 in v1 — the throttler applies hardcoded defaults when
// LimitProvider returns ok=false for the empty namespace.
//
// PackResolver IO error propagates as err.
func (s *Service) ResolveCredentialPack(ctx context.Context, devName string) (canonicalID string, namespace string, active bool, err error) {
	d, err := s.repo.GetDefinitionByDeveloperName(ctx, devName)
	if err != nil {
		return "", "", false, err
	}
	if d == nil {
		return "", "", false, nil
	}
	if d.OwnerPack == "" {
		return d.ID, "", true, nil
	}
	active, err = s.resolvePackAccess(ctx, d.OwnerPack)
	if err != nil {
		return d.ID, d.OwnerPack, false, err
	}
	return d.ID, d.OwnerPack, active, nil
}

// maskedHint returns a display-safe hint for a secret value.
// Empty plaintext returns "". Short secrets (≤4 runes) return pure ellipsis
// "…" so no plaintext character is ever disclosed through the setup-UI
// metadata path. Longer secrets return "…" + last 4 runes (industry-standard
// tail-reveal masking). Rune-safe to avoid splitting multi-byte UTF-8.
func maskedHint(plaintext string) string {
	runes := []rune(plaintext)
	n := len(runes)
	if n == 0 {
		return ""
	}
	if n <= 4 {
		// Short secrets get pure ellipsis — never disclose plaintext runes
		// through the setup-UI metadata path, even if the caller has no
		// permission gate. Industry-standard masking only safely reveals the
		// tail of a longer secret.
		return "…"
	}
	return "…" + string(runes[n-4:])
}
