package runner

import (
	"fmt"
	"net/http"
	"strings"
)

// AuthKind is the credential injection strategy of a provider's REST API.
type AuthKind uint8

const (
	// AuthNone marks a provider whose Actions need no connection credential
	// (public APIs).
	AuthNone AuthKind = iota

	// AuthBearer sends the credential as an Authorization: Bearer token.
	AuthBearer

	// AuthHeader sends the credential in a provider-specific header.
	AuthHeader

	// AuthBasic sends the credential as HTTP Basic authentication.
	AuthBasic

	// AuthHeaders injects several declared credential fields, each into its
	// own provider-specific header (for example a user-id header plus a token
	// header). Fields without a HeaderName are connection-scoped values the
	// provider resolves itself (for example the instance base URL) and are
	// never injected.
	AuthHeaders

	// AuthCredential collects connection-scoped credential fields the
	// provider resolves itself without any header injection (for example a
	// webhook token carried in the request path or query string). At least one
	// declared field must be required and secret.
	AuthCredential

	// AuthQuery injects the credential as one query parameter, for providers
	// whose official API carries its key in the URL query (for example
	// apikey=... endpoints).
	AuthQuery
)

// String returns the stable label for the auth kind.
func (kind AuthKind) String() string {
	switch kind {
	case AuthNone:
		return "none"
	case AuthBearer:
		return "bearer"
	case AuthHeader:
		return "header"
	case AuthBasic:
		return "basic"
	case AuthHeaders:
		return "headers"
	case AuthCredential:
		return "credentials"
	case AuthQuery:
		return "query"
	default:
		return "unknown"
	}
}

// CredentialField declares one connection credential field surfaced by the
// App connection API.
type CredentialField struct {
	// Name is the wire key of the field in the connection credential object
	// (for example "apiKey").
	Name string

	// DisplayName is the human-facing label shown when connecting the App.
	DisplayName string

	// Description explains what value the user must paste.
	Description string

	// Required reports whether the Connect flow rejects an empty value.
	Required bool

	// Secret reports whether the value is a credential that must never be
	// echoed back to clients.
	Secret bool

	// HeaderName is the request header this field is injected into under the
	// AuthHeaders kind. It is only valid on AuthHeaders providers; fields
	// without a HeaderName stay connection-scoped and are never injected.
	HeaderName string
}

// APIKeyField is the canonical single-secret credential field shared by
// token-authenticated providers.
func APIKeyField(label, description string) CredentialField {
	return CredentialField{
		Name:        "apiKey",
		DisplayName: label,
		Description: description,
		Required:    true,
		Secret:      true,
	}
}

// AuthSpec is the provider auth contract: how the credential is injected and
// which fields the connection collects. It is server-only metadata and never
// enters the registry digest.
type AuthSpec struct {
	// Kind selects the injection strategy.
	Kind AuthKind

	// Header is the header name for AuthHeader (for example "x-api-token").
	Header string

	// QueryName is the query parameter name for AuthQuery (for example
	// "apikey").
	QueryName string

	// BasicUsername is the fixed username for AuthBasic; the credential value
	// from the provider's single declared field is the password. It is mutually
	// exclusive with BasicUsernameField.
	BasicUsername string

	// BasicUsernameField and BasicPasswordField select the declared credential
	// fields used as the dynamic HTTP Basic username and password. They must be
	// configured together.
	BasicUsernameField string
	BasicPasswordField string

	// Fields declares the connection credential fields. AuthBearer/AuthHeader
	// and fixed-username AuthBasic providers inject the first field; dynamic
	// AuthBasic providers inject the explicitly selected pair; AuthHeaders
	// injects every field bound to a HeaderName. AuthNone declares no fields.
	Fields []CredentialField

	// Signature derives per-request credential headers from the credential
	// object (for example an MD5 or HMAC request token bound to a timestamp).
	// It is only valid under the headers kind, where it substitutes for
	// header bindings: the fields stay connection-scoped and the derived
	// headers are applied after every provider-set header, exactly like an
	// injected credential. Code-defined, never Agent input, and never part
	// of the registry digest.
	Signature func(credentials Credentials) (map[string]string, error)
}

// Credentials is the decrypted per-connection credential object.
type Credentials map[string]string

// Clone returns a detached copy.
func (credentials Credentials) Clone() Credentials {
	if credentials == nil {
		return nil
	}
	clone := make(Credentials, len(credentials))
	for name, value := range credentials {
		clone[name] = value
	}
	return clone
}

const maxCredentialValueBytes = 16 * 1024

// ValidateSpec checks the provider auth declaration independently of any
// connection credential object.
func (spec AuthSpec) ValidateSpec() error {
	if spec.Signature != nil && spec.Kind != AuthHeaders {
		return fmt.Errorf("auth signature is only valid under the headers kind")
	}
	switch spec.Kind {
	case AuthNone:
		if len(spec.Fields) != 0 {
			return fmt.Errorf("provider declares credential fields under the none kind")
		}
		if spec.Header != "" || spec.QueryName != "" || spec.BasicUsername != "" ||
			spec.BasicUsernameField != "" || spec.BasicPasswordField != "" {
			return fmt.Errorf("none auth declares injection settings")
		}
		return nil
	case AuthBearer, AuthHeader, AuthBasic, AuthHeaders, AuthCredential, AuthQuery:
	default:
		return fmt.Errorf("provider auth kind is invalid")
	}
	if len(spec.Fields) == 0 {
		return fmt.Errorf("provider auth declares no credential fields")
	}
	if spec.Kind == AuthHeader && spec.Header == "" {
		return fmt.Errorf("header auth declares no header name")
	}
	switch spec.Kind {
	case AuthBearer:
		if spec.Header != "" || spec.QueryName != "" || spec.BasicUsername != "" ||
			spec.BasicUsernameField != "" || spec.BasicPasswordField != "" {
			return fmt.Errorf("Bearer auth declares unrelated injection settings")
		}
	case AuthHeader:
		if err := validateAuthHeaderName(spec.Header); err != nil {
			return fmt.Errorf("header auth name is invalid: %w", err)
		}
		if spec.QueryName != "" || spec.BasicUsername != "" ||
			spec.BasicUsernameField != "" || spec.BasicPasswordField != "" {
			return fmt.Errorf("header auth declares unrelated injection settings")
		}
	case AuthBasic:
		if spec.Header != "" || spec.QueryName != "" {
			return fmt.Errorf("Basic auth declares an unrelated injection setting")
		}
		if strings.ContainsAny(spec.BasicUsername, ":\r\n\x00") {
			return fmt.Errorf("Basic fixed username is invalid")
		}
	case AuthHeaders:
		if spec.Header != "" || spec.QueryName != "" || spec.BasicUsername != "" ||
			spec.BasicUsernameField != "" || spec.BasicPasswordField != "" {
			return fmt.Errorf("headers auth declares unrelated injection settings")
		}
	case AuthQuery:
		if spec.QueryName == "" {
			return fmt.Errorf("query auth declares no query parameter name")
		}
		if strings.ContainsAny(spec.QueryName, "&=# \t\r\n\x00") {
			return fmt.Errorf("query auth parameter name is invalid")
		}
		if spec.Header != "" || spec.BasicUsername != "" ||
			spec.BasicUsernameField != "" || spec.BasicPasswordField != "" {
			return fmt.Errorf("query auth declares unrelated injection settings")
		}
	case AuthCredential:
		if spec.Header != "" || spec.QueryName != "" || spec.BasicUsername != "" ||
			spec.BasicUsernameField != "" || spec.BasicPasswordField != "" {
			return fmt.Errorf("credentials auth declares injection settings")
		}
	}
	declared := make(map[string]CredentialField, len(spec.Fields))
	for _, field := range spec.Fields {
		if field.Name == "" || strings.TrimSpace(field.Name) != field.Name {
			return fmt.Errorf("credential field name is invalid")
		}
		if _, duplicate := declared[field.Name]; duplicate {
			return fmt.Errorf("credential field %q is declared twice", field.Name)
		}
		declared[field.Name] = field
	}
	if err := spec.validateHeaderBindings(); err != nil {
		return err
	}
	if spec.Kind == AuthHeaders {
		return nil
	}
	if spec.Kind == AuthCredential {
		return validateCredentialFields(spec.Fields)
	}
	if spec.Kind != AuthBasic {
		if len(spec.Fields) != 1 {
			return fmt.Errorf("provider auth must declare exactly one credential field")
		}
		field := spec.Fields[0]
		if !field.Required || !field.Secret {
			return fmt.Errorf("injected credential field must be required and secret")
		}
		return nil
	}
	hasUsernameField := spec.BasicUsernameField != ""
	hasPasswordField := spec.BasicPasswordField != ""
	if hasUsernameField != hasPasswordField {
		return fmt.Errorf("Basic username and password fields must be configured together")
	}
	if !hasUsernameField {
		if len(spec.Fields) != 1 {
			return fmt.Errorf("fixed Basic auth must declare exactly one password field")
		}
		field := spec.Fields[0]
		if !field.Required || !field.Secret {
			return fmt.Errorf("Basic password field must be required and secret")
		}
		return nil
	}
	if spec.BasicUsername != "" {
		return fmt.Errorf("Basic fixed and dynamic usernames are mutually exclusive")
	}
	if spec.BasicUsernameField == spec.BasicPasswordField {
		return fmt.Errorf("Basic username and password fields must be distinct")
	}
	usernameField, ok := declared[spec.BasicUsernameField]
	if !ok {
		return fmt.Errorf("credential field %q is not declared", spec.BasicUsernameField)
	}
	passwordField, ok := declared[spec.BasicPasswordField]
	if !ok {
		return fmt.Errorf("credential field %q is not declared", spec.BasicPasswordField)
	}
	if !usernameField.Required || !passwordField.Required {
		return fmt.Errorf("Basic username and password fields must be required")
	}
	if !passwordField.Secret {
		return fmt.Errorf("Basic password field must be secret")
	}
	return nil
}

// validateCredentialFields enforces the credentials-kind contract: at least
// one declared field must be required and secret so a connection always
// collects a real credential for the provider to resolve.
func validateCredentialFields(fields []CredentialField) error {
	hasRequiredSecret := false
	for _, field := range fields {
		if field.Required && field.Secret {
			hasRequiredSecret = true
			break
		}
	}
	if !hasRequiredSecret {
		return fmt.Errorf(
			"credentials auth must declare at least one required and secret field")
	}
	return nil
}

// Validate checks the credential object against the declared fields: exactly
// the declared names, required fields non-empty, no whitespace control
// characters (header/CRLF injection), and bounded length.
func (spec AuthSpec) Validate(credentials Credentials) error {
	if err := spec.ValidateSpec(); err != nil {
		return err
	}
	if spec.Kind == AuthNone {
		if len(credentials) != 0 {
			return fmt.Errorf("provider accepts no credentials")
		}
		return nil
	}
	declared := make(map[string]CredentialField, len(spec.Fields))
	for _, field := range spec.Fields {
		declared[field.Name] = field
	}
	for name := range credentials {
		if _, known := declared[name]; !known {
			return fmt.Errorf("credential field %q is not declared", name)
		}
	}
	for name, field := range declared {
		value, present := credentials[name]
		if !present || value == "" {
			if field.Required {
				return fmt.Errorf("credential field %q is required", name)
			}
			continue
		}
		if len(value) > maxCredentialValueBytes {
			return fmt.Errorf("credential field %q exceeds %d bytes", name, maxCredentialValueBytes)
		}
		if strings.ContainsAny(value, "\r\n\x00") {
			return fmt.Errorf("credential field %q contains control characters", name)
		}
	}
	if spec.Kind == AuthBasic && spec.BasicUsernameField != "" &&
		strings.Contains(credentials[spec.BasicUsernameField], ":") {
		return fmt.Errorf("Basic username credential contains a colon")
	}
	return nil
}

func credentialValue(fieldName string, credentials Credentials) (string, error) {
	value := credentials[fieldName]
	if value == "" {
		return "", fmt.Errorf("credential field %q is empty", fieldName)
	}
	return value, nil
}

// secretValue returns the injected credential value for the spec. Providers in
// fixed-value auth modes inject the first declared field.
func (spec AuthSpec) secretValue(credentials Credentials) (string, error) {
	if len(spec.Fields) == 0 {
		return "", fmt.Errorf("provider auth declares no credential fields")
	}
	return credentialValue(spec.Fields[0].Name, credentials)
}

// validateHeaderBindings enforces the AuthHeaders wire targets: header names
// are only legal on the AuthHeaders kind, every bound name must be a valid
// non-reserved header, and no two fields may claim the same header.
func (spec AuthSpec) validateHeaderBindings() error {
	bound := make(map[string]string, len(spec.Fields))
	for _, field := range spec.Fields {
		if field.HeaderName == "" {
			continue
		}
		if spec.Kind != AuthHeaders {
			return fmt.Errorf(
				"credential field %q declares a header outside the headers kind", field.Name)
		}
		if err := validateStaticHeader(field.HeaderName); err != nil {
			return fmt.Errorf("credential field %q header is invalid: %w", field.Name, err)
		}
		canonical := strings.ToLower(field.HeaderName)
		if owner, clash := bound[canonical]; clash {
			return fmt.Errorf(
				"credential fields %q and %q share header %q", owner, field.Name, field.HeaderName)
		}
		bound[canonical] = field.Name
	}
	return nil
}

// applyAuthentication sets the credential-bearing headers on request for the
// spec's injection strategy. It is called after every other header is bound
// so a credential can never collide with a provider-set header.
func (spec AuthSpec) applyAuthentication(request *http.Request, credentials Credentials) error {
	switch spec.Kind {
	case AuthNone:
		return nil
	case AuthBearer:
		value, err := spec.secretValue(credentials)
		if err != nil {
			return err
		}
		request.Header.Set("Authorization", "Bearer "+value)
		return nil
	case AuthHeader:
		if spec.Header == "" {
			return fmt.Errorf("provider auth header name is unset")
		}
		value, err := spec.secretValue(credentials)
		if err != nil {
			return err
		}
		request.Header.Set(spec.Header, value)
		return nil
	case AuthQuery:
		if spec.QueryName == "" {
			return fmt.Errorf("provider auth query parameter name is unset")
		}
		value, err := spec.secretValue(credentials)
		if err != nil {
			return err
		}
		query := request.URL.Query()
		query.Set(spec.QueryName, value)
		request.URL.RawQuery = query.Encode()
		return nil
	case AuthBasic:
		username := spec.BasicUsername
		var password string
		if spec.BasicUsernameField != "" {
			var err error
			username, err = credentialValue(spec.BasicUsernameField, credentials)
			if err != nil {
				return err
			}
			password, err = credentialValue(spec.BasicPasswordField, credentials)
			if err != nil {
				return err
			}
		} else {
			var err error
			password, err = spec.secretValue(credentials)
			if err != nil {
				return err
			}
		}
		request.SetBasicAuth(username, password)
		return nil
	case AuthHeaders:
		for _, field := range spec.Fields {
			if field.HeaderName == "" {
				continue
			}
			value := credentials[field.Name]
			if value == "" {
				return fmt.Errorf("credential field %q is empty", field.Name)
			}
			request.Header.Set(field.HeaderName, value)
		}
		if spec.Signature == nil {
			return nil
		}
		derived, err := spec.Signature(credentials)
		if err != nil {
			return err
		}
		for name, value := range derived {
			if err := validateStaticHeader(name); err != nil {
				return fmt.Errorf("auth signature header is invalid: %w", err)
			}
			request.Header.Set(name, value)
		}
		return nil
	case AuthCredential:
		// Connection-scoped credentials are resolved by the provider handler
		// (for example into the request path or query); nothing to inject.
		return nil
	default:
		return fmt.Errorf("provider auth kind is invalid")
	}
}

// validateAuthHeaderName admits the reserved Authorization name for the
// AuthHeader kind only: that spec IS the credential injection, so claiming
// the canonical token header is the provider contract (for example a raw
// Authorization token without a Bearer prefix), not a collision with a
// provider-set header. The exemption lifts the reserved-name policy only:
// the raw name must still pass the full header-name syntax, and static
// request headers keep the full reservation.
func validateAuthHeaderName(name string) error {
	if err := validateHeaderNameSyntax(name); err != nil {
		return err
	}
	if strings.EqualFold(name, "authorization") {
		return nil
	}
	if reservedRequestHeaders[strings.ToLower(name)] {
		return fmt.Errorf("header name %q is reserved", name)
	}
	return nil
}

// Redacted returns a secret-free view of the credential object for API
// responses: which declared fields carry a value, never the values.
func (credentials Credentials) Redacted(fields []CredentialField) map[string]bool {
	redacted := make(map[string]bool, len(fields))
	for _, field := range fields {
		redacted[field.Name] = credentials[field.Name] != ""
	}
	return redacted
}
