// Package tenant carries validated tenant identity and public policy through
// request and business contexts. Values must come from trusted server
// configuration, never directly from request headers or payloads.
package tenant

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"regexp"
	"slices"
	"strings"
)

var (
	// ErrRequired is returned when tenant-owned work has no validated tenant.
	ErrRequired = errors.New("tenant context required")
	// ErrInvalid is returned when a tenant identity or policy is malformed.
	ErrInvalid = errors.New("invalid tenant")
	// ErrRebinding is returned when code attempts to replace a tenant already
	// bound to a context with a different identity or policy.
	ErrRebinding = errors.New("tenant context rebinding is not allowed")
)

var idPattern = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$`)

// ID is an immutable internal business partition identifier.
type ID string

// ParseID normalizes and validates a configured tenant identifier.
func ParseID(raw string) (ID, error) {
	id := ID(strings.ToLower(strings.TrimSpace(raw)))
	if !idPattern.MatchString(string(id)) {
		return "", fmt.Errorf("%w ID %q: use 1-64 lowercase letters, numbers, dots, underscores, or hyphens", ErrInvalid, raw)
	}
	return id, nil
}

// Info is immutable tenant identity and public request policy.
type Info struct {
	ID                         ID
	CanonicalBaseURL           string
	ConsoleBaseURL             string
	OAuthDeviceVerificationURL string
	OAuthLoopbackRedirects     bool
	CORSAllowedOrigins         []string
	ConnectedLoginEnabled      bool
	OIDCEnabled                bool
}

// Validate verifies that Info can safely identify a tenant request.
func (i Info) Validate() error {
	id, err := ParseID(string(i.ID))
	if err != nil || id != i.ID {
		return fmt.Errorf("%w: tenant ID is not normalized", ErrInvalid)
	}
	base, err := url.Parse(strings.TrimSpace(i.CanonicalBaseURL))
	if err != nil || base.Scheme == "" || base.Host == "" || base.User != nil {
		return fmt.Errorf("%w %q: canonical base URL must be absolute", ErrInvalid, i.ID)
	}
	if !strings.EqualFold(base.Scheme, "http") && !strings.EqualFold(base.Scheme, "https") {
		return fmt.Errorf("%w %q: canonical base URL must use HTTP or HTTPS", ErrInvalid, i.ID)
	}
	return nil
}

// Clone returns an independent copy of tenant policy.
func (i Info) Clone() Info {
	i.CORSAllowedOrigins = append([]string(nil), i.CORSAllowedOrigins...)
	return i
}

// Equal reports whether two values describe the same registry-owned tenant
// identity and public policy.
func (i Info) Equal(other Info) bool {
	return i.ID == other.ID &&
		i.CanonicalBaseURL == other.CanonicalBaseURL &&
		i.ConsoleBaseURL == other.ConsoleBaseURL &&
		i.OAuthDeviceVerificationURL == other.OAuthDeviceVerificationURL &&
		i.OAuthLoopbackRedirects == other.OAuthLoopbackRedirects &&
		slices.Equal(i.CORSAllowedOrigins, other.CORSAllowedOrigins) &&
		i.ConnectedLoginEnabled == other.ConnectedLoginEnabled &&
		i.OIDCEnabled == other.OIDCEnabled
}

// BaseURL returns the normalized canonical origin.
func (i Info) BaseURL() string {
	return strings.TrimRight(strings.TrimSpace(i.CanonicalBaseURL), "/")
}

// HTMLBaseURL returns the canonical browser URL. It preserves the historical
// upgrade from local HTTP API URLs to their HTTPS browser form.
func (i Info) HTMLBaseURL() string {
	return strings.Replace(i.BaseURL(), "http://", "https://", 1)
}

// Hostname returns the canonical tenant hostname.
func (i Info) Hostname() string {
	u, err := url.Parse(i.BaseURL())
	if err != nil {
		return ""
	}
	return u.Hostname()
}

// Secure reports whether browser cookies for this tenant require HTTPS.
func (i Info) Secure() bool {
	u, err := url.Parse(i.BaseURL())
	return err == nil && strings.EqualFold(u.Scheme, "https")
}

// AllowsOrigin reports whether origin is in the tenant CORS allowlist.
func (i Info) AllowsOrigin(origin string) bool {
	origin = strings.TrimSpace(origin)
	for _, allowed := range i.CORSAllowedOrigins {
		if origin == allowed {
			return true
		}
	}
	return false
}

type contextKey struct{}
type resolvedHostKey struct{}

type contextValue struct {
	info Info
	err  error
}

// WithInfo returns a context carrying trusted tenant identity and policy.
// Tenant identity is sticky: attempting to replace an existing binding poisons
// the derived context so tenant-owned work fails closed.
func WithInfo(ctx context.Context, info Info) context.Context {
	if current, ok := ctx.Value(contextKey{}).(contextValue); ok {
		if current.err != nil {
			return ctx
		}
		if current.info.Equal(info) {
			return ctx
		}
		return context.WithValue(ctx, contextKey{}, contextValue{err: ErrRebinding})
	}
	if err := info.Validate(); err != nil {
		return context.WithValue(ctx, contextKey{}, contextValue{err: err})
	}
	return context.WithValue(ctx, contextKey{}, contextValue{info: info.Clone()})
}

// FromContext returns the validated tenant on ctx.
func FromContext(ctx context.Context) (Info, bool) {
	if ctx == nil {
		return Info{}, false
	}
	value, ok := ctx.Value(contextKey{}).(contextValue)
	if !ok || value.err != nil || value.info.Validate() != nil {
		return Info{}, false
	}
	return value.info.Clone(), true
}

// Require returns the validated tenant or ErrRequired.
func Require(ctx context.Context) (Info, error) {
	if ctx == nil {
		return Info{}, ErrRequired
	}
	value, ok := ctx.Value(contextKey{}).(contextValue)
	if !ok {
		return Info{}, ErrRequired
	}
	if value.err != nil {
		return Info{}, value.err
	}
	if err := value.info.Validate(); err != nil {
		return Info{}, err
	}
	return value.info.Clone(), nil
}

// WithResolvedHost records the normalized host selected by the trusted tenant
// resolver. Business and compatibility routing must use this value instead of
// reparsing untrusted request headers.
func WithResolvedHost(ctx context.Context, host string) context.Context {
	return context.WithValue(ctx, resolvedHostKey{}, strings.ToLower(strings.TrimSpace(host)))
}

// ResolvedHostFromContext returns the host selected by the tenant resolver.
func ResolvedHostFromContext(ctx context.Context) (string, bool) {
	if ctx == nil {
		return "", false
	}
	host, ok := ctx.Value(resolvedHostKey{}).(string)
	return host, ok && host != ""
}

// Copy binds the tenant from source onto destination. It is intended for
// detached work that must retain tenant identity while using a lifecycle
// context with independent cancellation.
func Copy(destination, source context.Context) (context.Context, error) {
	info, err := Require(source)
	if err != nil {
		return nil, err
	}
	result := WithInfo(destination, info)
	copied, err := Require(result)
	if err != nil {
		return nil, err
	}
	if !copied.Equal(info) {
		return nil, ErrRebinding
	}
	return result, nil
}
