package identity

import (
	"errors"
	"net"
	"net/url"
	"strings"
	"time"

	"github.com/google/uuid"
	"golang.org/x/net/publicsuffix"
)

var (
	ErrRevisionNotFound                     = errors.New("identity configuration revision not found")
	ErrRevisionConflict                     = errors.New("identity configuration revision conflicts with current state")
	ErrBreakGlassRequired                   = errors.New("a local break-glass manager credential is required")
	ErrLocalTenantInvalid                   = errors.New("local tenant mapping is invalid")
	ErrActiveConfigurationHandoffRequired   = errors.New("active identity configuration requires an explicit remote resource handoff before re-pairing")
	ErrRollbackConfigurationHandoffRequired = errors.New("rollback requires a fresh remote resource handoff")
	ErrSecurityEventRetirementPending       = errors.New("active security event connection must retire before identity can be disabled")
)

type RevisionPolicy struct {
	TenantMode             string `json:"tenantMode"`
	LocalTenantKey         string `json:"localTenantKey"`
	RolePrefix             string `json:"rolePrefix"`
	JITEnabled             bool   `json:"jitEnabled"`
	LegacyJWTEnabled       bool   `json:"legacyJwtEnabled"`
	SessionIdleSeconds     int    `json:"sessionIdleSeconds"`
	SessionAbsoluteSeconds int    `json:"sessionAbsoluteSeconds"`
	SessionRefreshSeconds  int    `json:"sessionRefreshSeconds"`
}

func (policy RevisionPolicy) Validate() error {
	if strings.TrimSpace(policy.RolePrefix) == "" ||
		policy.TenantMode != "multi_tenant" && strings.TrimSpace(policy.LocalTenantKey) == "" {
		return errors.New("local tenant mapping and role prefix are required")
	}
	if policy.SessionIdleSeconds <= 0 || policy.SessionAbsoluteSeconds <= policy.SessionIdleSeconds ||
		policy.SessionRefreshSeconds <= 0 || policy.SessionRefreshSeconds >= policy.SessionIdleSeconds {
		return errors.New("identity session policy is invalid")
	}
	return nil
}

type RevisionState string

const (
	RevisionDraft      RevisionState = "draft"
	RevisionValidated  RevisionState = "validated"
	RevisionActive     RevisionState = "active"
	RevisionSuperseded RevisionState = "superseded"
	RevisionFailed     RevisionState = "failed"
)

type Revision struct {
	ID                      string        `json:"id"`
	State                   RevisionState `json:"state"`
	SchemaVersion           int           `json:"schemaVersion"`
	TenantMode              string        `json:"tenantMode,omitempty"`
	AuthCenterURL           string        `json:"authCenterUrl"`
	Issuer                  string        `json:"issuer,omitempty"`
	TenantID                string        `json:"tenantId,omitempty"`
	ApplicationID           string        `json:"applicationId,omitempty"`
	Audience                string        `json:"audience,omitempty"`
	BrowserClientID         string        `json:"browserClientId,omitempty"`
	MachineClientID         string        `json:"machineClientId,omitempty"`
	Scopes                  []string      `json:"scopes"`
	Capabilities            []string      `json:"capabilities"`
	RolePrefix              string        `json:"rolePrefix"`
	LocalTenantKey          string        `json:"localTenantKey"`
	PublicBaseURL           string        `json:"publicBaseUrl"`
	WebBaseURL              string        `json:"webBaseUrl"`
	JITEnabled              bool          `json:"jitEnabled"`
	LegacyJWTEnabled        bool          `json:"legacyJwtEnabled"`
	TokenIntrospection      bool          `json:"tokenIntrospection"`
	SessionRevocation       bool          `json:"sessionRevocation"`
	SecurityEventIssuer     string        `json:"securityEventIssuer,omitempty"`
	SecurityEventConfigURL  string        `json:"securityEventConfigurationUrl,omitempty"`
	SecurityEventAudience   string        `json:"securityEventAudience,omitempty"`
	TenantContextEndpoint   string        `json:"tenantContextEndpoint,omitempty"`
	TenantContextAudience   string        `json:"tenantContextAudience,omitempty"`
	TenantContextScope      string        `json:"tenantContextScope,omitempty"`
	MachineCredentialRef    string        `json:"-"`
	SessionEncryptionKeyRef string        `json:"-"`
	SessionIdleSeconds      int           `json:"sessionIdleSeconds"`
	SessionAbsoluteSeconds  int           `json:"sessionAbsoluteSeconds"`
	SessionRefreshSeconds   int           `json:"sessionRefreshSeconds"`
	Version                 int64         `json:"version"`
	LastErrorCategory       string        `json:"lastErrorCategory,omitempty"`
	LastTraceID             string        `json:"lastTraceId,omitempty"`
	LastAuditID             string        `json:"lastAuditId,omitempty"`
	ValidatedAt             *time.Time    `json:"validatedAt,omitempty"`
	ActivatedAt             *time.Time    `json:"activatedAt,omitempty"`
	SupersededAt            *time.Time    `json:"supersededAt,omitempty"`
	CreatedAt               time.Time     `json:"createdAt"`
	UpdatedAt               time.Time     `json:"updatedAt"`
}

type PairingInput struct {
	AuthCenterURL    string `json:"authCenterUrl"`
	OnboardingCode   string `json:"onboardingCode"`
	PublicBaseURL    string `json:"publicBaseUrl"`
	WebBaseURL       string `json:"webBaseUrl"`
	LocalTenantKey   string `json:"localTenantKey"`
	LegacyJWTEnabled bool   `json:"legacyJwtEnabled"`
}

type ConsumerMetadata struct {
	PublicBaseURL    string   `json:"public_base_url"`
	WebBaseURL       string   `json:"web_base_url"`
	RedirectURIs     []string `json:"redirect_uris"`
	LogoutURIs       []string `json:"logout_uris"`
	ReceiverEndpoint string   `json:"receiver_endpoint,omitempty"`
}

type ManifestApplication struct {
	Manifest                ManifestV1
	MachineCredentialRef    string
	SessionEncryptionKeyRef string
	TraceID                 string
	AuditID                 string
	AppEnv                  string
}

func NewDraft(input PairingInput, appEnv string) (Revision, error) {
	if _, err := input.ConsumerMetadata(false, appEnv); err != nil {
		return Revision{}, err
	}
	authCenter, _ := exactBaseURL(input.AuthCenterURL, appEnv)
	publicBase, _ := exactBaseURL(input.PublicBaseURL, appEnv)
	webBase, _ := exactBaseURL(input.WebBaseURL, appEnv)
	return Revision{
		ID: uuid.NewString(), State: RevisionDraft, SchemaVersion: 1, TenantMode: "single_tenant",
		AuthCenterURL: authCenter, RolePrefix: "gateway.", LocalTenantKey: strings.TrimSpace(input.LocalTenantKey),
		PublicBaseURL: publicBase, WebBaseURL: webBase, JITEnabled: true, LegacyJWTEnabled: input.LegacyJWTEnabled,
		Scopes: []string{}, Capabilities: []string{}, SessionIdleSeconds: 1800, SessionAbsoluteSeconds: 28800,
		SessionRefreshSeconds: 60, Version: 1,
	}, nil
}

func ApplyManifest(revision Revision, input ManifestApplication) (Revision, error) {
	if revision.State != RevisionDraft {
		return Revision{}, ErrRevisionConflict
	}
	if err := input.Manifest.Validate(input.AppEnv); err != nil {
		return Revision{}, err
	}
	capabilities := make(map[string]bool, len(input.Manifest.Capabilities))
	for _, capability := range input.Manifest.Capabilities {
		capabilities[capability] = true
	}
	if capabilities["machine_to_machine"] && strings.TrimSpace(input.MachineCredentialRef) == "" {
		return Revision{}, errors.New("machine credential reference is required")
	}
	if capabilities["oidc_login"] && strings.TrimSpace(input.SessionEncryptionKeyRef) == "" {
		return Revision{}, errors.New("session encryption key reference is required")
	}
	if input.Manifest.SchemaVersion == 1 && strings.TrimSpace(revision.LocalTenantKey) == "" {
		return Revision{}, ErrLocalTenantInvalid
	}
	revision.SchemaVersion = input.Manifest.SchemaVersion
	revision.Issuer = strings.TrimRight(input.Manifest.Issuer, "/")
	revision.TenantID = input.Manifest.TenantID
	if input.Manifest.SchemaVersion == 1 {
		// Manifest V1 intentionally omits tenant_mode on the wire, but the
		// persisted revision uses the explicit single-tenant discriminator.
		revision.TenantMode = "single_tenant"
	} else {
		revision.TenantMode = input.Manifest.TenantMode
	}
	revision.ApplicationID = input.Manifest.ApplicationID
	revision.Audience = input.Manifest.Audience
	revision.Scopes = append([]string(nil), input.Manifest.Scopes...)
	revision.Capabilities = append([]string(nil), input.Manifest.Capabilities...)
	if input.Manifest.Clients.BrowserLogin != nil {
		revision.BrowserClientID = input.Manifest.Clients.BrowserLogin.ClientID
	}
	if input.Manifest.Clients.MachineToMachine != nil {
		revision.MachineClientID = input.Manifest.Clients.MachineToMachine.ClientID
	}
	revision.TokenIntrospection = capabilities["token_introspection"]
	revision.SessionRevocation = capabilities["session_revocation"]
	if input.Manifest.SchemaVersion == 2 {
		revision.LocalTenantKey = ""
		revision.TenantContextEndpoint = input.Manifest.TenantContext.Endpoint
		revision.TenantContextAudience = input.Manifest.TenantContext.Audience
		revision.TenantContextScope = input.Manifest.TenantContext.Scope
	} else {
		revision.TenantContextEndpoint = ""
		revision.TenantContextAudience = ""
		revision.TenantContextScope = ""
	}
	if input.Manifest.SecurityEvents != nil {
		revision.SecurityEventIssuer = input.Manifest.SecurityEvents.TransmitterIssuer
		revision.SecurityEventConfigURL = input.Manifest.SecurityEvents.ConfigurationEndpoint
		revision.SecurityEventAudience = input.Manifest.SecurityEvents.Audience
	}
	revision.MachineCredentialRef = strings.TrimSpace(input.MachineCredentialRef)
	revision.SessionEncryptionKeyRef = strings.TrimSpace(input.SessionEncryptionKeyRef)
	revision.LastTraceID = strings.TrimSpace(input.TraceID)
	revision.LastAuditID = strings.TrimSpace(input.AuditID)
	return revision, nil
}

func CanTransition(from, to RevisionState) bool {
	switch from {
	case RevisionDraft:
		return to == RevisionValidated || to == RevisionFailed
	case RevisionValidated:
		return to == RevisionActive || to == RevisionFailed
	case RevisionActive:
		return to == RevisionSuperseded
	case RevisionSuperseded:
		return to == RevisionValidated
	default:
		return false
	}
}

func (input PairingInput) ConsumerMetadata(sessionRevocation bool, appEnv string) (ConsumerMetadata, error) {
	authCenter, err := exactBaseURL(input.AuthCenterURL, appEnv)
	if err != nil {
		return ConsumerMetadata{}, errors.New("auth center URL is invalid")
	}
	_ = authCenter
	publicBase, err := exactBaseURL(input.PublicBaseURL, appEnv)
	if err != nil {
		return ConsumerMetadata{}, errors.New("public base URL is invalid")
	}
	webBase, err := exactBaseURL(input.WebBaseURL, appEnv)
	if err != nil {
		return ConsumerMetadata{}, errors.New("web base URL is invalid")
	}
	if !sameSiteBaseURLs(publicBase, webBase) {
		return ConsumerMetadata{}, errors.New("public base URL and web base URL must be same-site")
	}
	metadata := ConsumerMetadata{
		PublicBaseURL: publicBase, WebBaseURL: webBase,
		RedirectURIs: []string{publicBase + "/api/v1/auth/oidc/callback"},
		LogoutURIs:   []string{webBase + "/"},
	}
	if sessionRevocation {
		metadata.ReceiverEndpoint = publicBase + "/api/v1/security-events/ssf"
	}
	return metadata, nil
}

func exactBaseURL(raw, appEnv string) (string, error) {
	parsed, err := url.Parse(strings.TrimSpace(raw))
	if err != nil || parsed.Opaque != "" || parsed.User != nil || parsed.Host == "" || parsed.RawQuery != "" || parsed.Fragment != "" {
		return "", errors.New("invalid public URL")
	}
	hostname := strings.ToLower(parsed.Hostname())
	if parsed.Path != "" && parsed.Path != "/" {
		return "", errors.New("base URL must not contain a path")
	}
	scheme := strings.ToLower(parsed.Scheme)
	if scheme != "https" && !(scheme == "http" && isLocalIdentityEnvironment(appEnv) && isLoopbackHost(hostname)) {
		return "", errors.New("public URL must use HTTPS")
	}
	port := parsed.Port()
	if scheme == "https" && port == "443" || scheme == "http" && port == "80" {
		port = ""
	}
	host := hostname
	if strings.Contains(hostname, ":") {
		host = "[" + hostname + "]"
	}
	if port != "" {
		host = net.JoinHostPort(hostname, port)
	}
	return scheme + "://" + host, nil
}

func isLocalIdentityEnvironment(value string) bool {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "development", "dev", "local", "test":
		return true
	default:
		return false
	}
}

// ValidateRevisionURLs re-applies the deployment environment URL policy when
// constructing a Runtime. This protects against legacy or directly persisted
// revisions bypassing the pairing boundary.
func ValidateRevisionURLs(revision Revision, appEnv string) error {
	urls := []struct {
		label string
		raw   string
		base  bool
	}{
		{label: "auth center", raw: revision.AuthCenterURL, base: true},
		{label: "issuer", raw: revision.Issuer},
		{label: "public base", raw: revision.PublicBaseURL, base: true},
		{label: "web base", raw: revision.WebBaseURL, base: true},
	}
	for _, candidate := range urls {
		var err error
		if candidate.base {
			_, err = exactBaseURL(candidate.raw, appEnv)
		} else {
			err = validatePublicIdentityURL(candidate.raw, appEnv)
		}
		if err != nil {
			return errors.New("identity " + candidate.label + " URL must use HTTPS in this environment")
		}
	}
	if !sameSiteBaseURLs(revision.PublicBaseURL, revision.WebBaseURL) {
		return errors.New("identity public base and web base URLs must be same-site")
	}
	if revision.SessionRevocation || revision.SecurityEventIssuer != "" || revision.SecurityEventConfigURL != "" {
		if validatePublicIdentityURL(revision.SecurityEventIssuer, appEnv) != nil ||
			validatePublicIdentityURL(revision.SecurityEventConfigURL, appEnv) != nil {
			return errors.New("identity security event URLs must use HTTPS in this environment")
		}
	}
	return nil
}

func sameSiteBaseURLs(left, right string) bool {
	leftURL, leftErr := url.Parse(left)
	rightURL, rightErr := url.Parse(right)
	if leftErr != nil || rightErr != nil ||
		!strings.EqualFold(leftURL.Scheme, rightURL.Scheme) {
		return false
	}
	leftHost := strings.TrimSuffix(strings.ToLower(leftURL.Hostname()), ".")
	rightHost := strings.TrimSuffix(strings.ToLower(rightURL.Hostname()), ".")
	if leftHost == rightHost {
		return true
	}
	leftDomain, leftErr := publicsuffix.EffectiveTLDPlusOne(leftHost)
	rightDomain, rightErr := publicsuffix.EffectiveTLDPlusOne(rightHost)
	return leftErr == nil && rightErr == nil &&
		strings.EqualFold(leftDomain, rightDomain)
}

func isLoopbackHost(host string) bool {
	if host == "localhost" {
		return true
	}
	ip := net.ParseIP(host)
	return ip != nil && ip.IsLoopback()
}
