package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/response"
	"baron-sso-backend/internal/service"
	"encoding/json"
	"errors"
	"sort"
	"strings"

	"github.com/gofiber/fiber/v2"
)

const (
	clientTenantAccessRestrictedKey = "tenant_access_restricted"
	clientAllowedTenantsKey         = "allowed_tenants"
)

func normalizeClientTenantAccessMetadata(metadata map[string]any) (map[string]any, error) {
	if metadata == nil {
		metadata = map[string]any{}
	}

	restricted := readMetadataBoolValue(metadata, clientTenantAccessRestrictedKey)
	allowedTenants := normalizeMetadataStringSlice(metadata[clientAllowedTenantsKey])
	ownerTenantID := normalizeMetadataString(metadata["tenant_id"])

	if len(allowedTenants) > 0 {
		restricted = true
	}

	if !restricted {
		delete(metadata, clientAllowedTenantsKey)
		metadata[clientTenantAccessRestrictedKey] = false
		return metadata, nil
	}

	if ownerTenantID != "" {
		allowedTenants = append(allowedTenants, ownerTenantID)
	}
	allowedTenants = uniqueSortedStrings(allowedTenants)
	if len(allowedTenants) == 0 {
		return nil, errors.New("allowed_tenants is required when tenant_access_restricted is enabled")
	}

	metadata[clientTenantAccessRestrictedKey] = true
	metadata[clientAllowedTenantsKey] = allowedTenants
	return metadata, nil
}

func clientTenantAccessRestricted(metadata map[string]any) bool {
	if metadata == nil {
		return false
	}
	if readMetadataBoolValue(metadata, clientTenantAccessRestrictedKey) {
		return true
	}
	return len(normalizeMetadataStringSlice(metadata[clientAllowedTenantsKey])) > 0
}

func clientAllowedTenants(metadata map[string]any) []string {
	if metadata == nil {
		return nil
	}
	if !clientTenantAccessRestricted(metadata) {
		return nil
	}
	return uniqueSortedStrings(normalizeMetadataStringSlice(metadata[clientAllowedTenantsKey]))
}

func normalizeMetadataStringSlice(raw any) []string {
	switch value := raw.(type) {
	case []string:
		return uniqueSortedStrings(value)
	case []any:
		items := make([]string, 0, len(value))
		for _, item := range value {
			if s, ok := item.(string); ok {
				items = append(items, s)
			}
		}
		return uniqueSortedStrings(items)
	default:
		return nil
	}
}

func normalizeMetadataString(raw any) string {
	s, ok := raw.(string)
	if !ok {
		return ""
	}
	return strings.TrimSpace(s)
}

func uniqueSortedStrings(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	sort.Strings(out)
	return out
}

func clientTenantAccessAllowed(profile *domain.UserProfileResponse, client domain.HydraClient) bool {
	if !clientTenantAccessRestricted(client.Metadata) {
		return true
	}
	allowed := clientAllowedTenants(client.Metadata)
	if len(allowed) == 0 {
		return false
	}
	keys := manageableTenantKeysFromProfile(profile)
	if len(keys) == 0 {
		return false
	}
	for _, tenantID := range allowed {
		if _, ok := keys[strings.ToLower(strings.TrimSpace(tenantID))]; ok {
			return true
		}
	}
	return false
}

func clientTenantAccessAllowedForSubtree(c *fiber.Ctx, tenantSvc service.TenantService, profile *domain.UserProfileResponse, client domain.HydraClient) bool {
	if clientTenantAccessAllowed(profile, client) {
		return true
	}
	if tenantSvc == nil || profile == nil {
		return false
	}

	allowedTenants := make([]domain.Tenant, 0)
	for _, identifier := range clientAllowedTenants(client.Metadata) {
		if tenant, ok := resolveTenantAccessTenant(c, tenantSvc, domain.Tenant{ID: identifier, Slug: identifier}); ok {
			allowedTenants = append(allowedTenants, tenant)
		}
	}
	if len(allowedTenants) == 0 {
		return false
	}

	for _, candidate := range tenantAccessProfileTenants(profile) {
		resolvedCandidate, ok := resolveTenantAccessTenant(c, tenantSvc, candidate)
		if !ok {
			continue
		}
		for _, allowed := range allowedTenants {
			if tenantMatchesOrDescendsFrom(c, tenantSvc, resolvedCandidate, allowed) {
				return true
			}
		}
	}
	return false
}

func tenantAccessProfileTenants(profile *domain.UserProfileResponse) []domain.Tenant {
	if profile == nil {
		return nil
	}

	seen := make(map[string]struct{})
	tenants := make([]domain.Tenant, 0, len(profile.ManageableTenants)+len(profile.JoinedTenants)+2)
	add := func(tenant domain.Tenant) {
		key := strings.ToLower(firstNonEmptyString(tenant.ID, tenant.Slug, tenant.Name))
		if key == "" {
			return
		}
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		tenants = append(tenants, tenant)
	}

	if profile.Tenant != nil {
		add(*profile.Tenant)
	}
	if profile.TenantID != nil {
		add(domain.Tenant{ID: strings.TrimSpace(*profile.TenantID)})
	}
	for _, tenant := range profile.ManageableTenants {
		add(tenant)
	}
	for _, tenant := range profile.JoinedTenants {
		add(tenant)
	}
	return tenants
}

func resolveTenantAccessTenant(c *fiber.Ctx, tenantSvc service.TenantService, tenant domain.Tenant) (domain.Tenant, bool) {
	if tenantSvc == nil {
		return tenant, firstNonEmptyString(tenant.ID, tenant.Slug) != ""
	}
	if strings.TrimSpace(tenant.ID) != "" {
		if resolved, err := tenantSvc.GetTenant(c.Context(), strings.TrimSpace(tenant.ID)); err == nil && resolved != nil {
			return *resolved, true
		}
	}
	if strings.TrimSpace(tenant.Slug) != "" {
		if resolved, err := tenantSvc.GetTenantBySlug(c.Context(), strings.TrimSpace(tenant.Slug)); err == nil && resolved != nil {
			return *resolved, true
		}
	}
	return tenant, firstNonEmptyString(tenant.ID, tenant.Slug) != ""
}

func tenantMatchesOrDescendsFrom(c *fiber.Ctx, tenantSvc service.TenantService, tenant domain.Tenant, ancestor domain.Tenant) bool {
	if tenantAccessTenantMatches(tenant, ancestor) {
		return true
	}
	if tenantSvc == nil {
		return false
	}

	visited := make(map[string]struct{})
	current := tenant
	for current.ParentID != nil && strings.TrimSpace(*current.ParentID) != "" {
		parentID := strings.TrimSpace(*current.ParentID)
		if _, ok := visited[parentID]; ok {
			return false
		}
		visited[parentID] = struct{}{}

		parent, err := tenantSvc.GetTenant(c.Context(), parentID)
		if err != nil || parent == nil {
			return false
		}
		if tenantAccessTenantMatches(*parent, ancestor) {
			return true
		}
		current = *parent
	}
	return false
}

func tenantAccessTenantMatches(left, right domain.Tenant) bool {
	leftID := strings.ToLower(strings.TrimSpace(left.ID))
	rightID := strings.ToLower(strings.TrimSpace(right.ID))
	if leftID != "" && rightID != "" && leftID == rightID {
		return true
	}

	leftSlug := strings.ToLower(strings.TrimSpace(left.Slug))
	rightSlug := strings.ToLower(strings.TrimSpace(right.Slug))
	return leftSlug != "" && rightSlug != "" && leftSlug == rightSlug
}

type tenantAccessDeniedDetails struct {
	Account           tenantAccessDeniedAccount  `json:"account"`
	CurrentTenant     tenantAccessDeniedTenant   `json:"current_tenant"`
	AffiliatedTenants []tenantAccessDeniedTenant `json:"affiliated_tenants,omitempty"`
	AllowedTenants    []tenantAccessDeniedTenant `json:"allowed_tenants,omitempty"`
}

type tenantAccessDeniedAccount struct {
	Email string `json:"email,omitempty"`
}

type tenantAccessDeniedTenant struct {
	ID         string `json:"id,omitempty"`
	Slug       string `json:"slug,omitempty"`
	Name       string `json:"name,omitempty"`
	Identifier string `json:"identifier,omitempty"`
}

func tenantNotAllowedError(c *fiber.Ctx, details tenantAccessDeniedDetails) error {
	return response.ErrorWithDetails(
		c,
		fiber.StatusForbidden,
		"tenant_not_allowed",
		"허용되지 않은 테넌트입니다.",
		details,
	)
}

func isClientTenantAccessAllowed(profile *domain.UserProfileResponse, client domain.HydraClient) bool {
	if profile == nil {
		return false
	}
	return clientTenantAccessAllowed(profile, client)
}

func enforceClientTenantAccess(c *fiber.Ctx, tenantSvc service.TenantService, client domain.HydraClient, profile *domain.UserProfileResponse, resolveErr error) bool {
	if !clientTenantAccessRestricted(client.Metadata) {
		return false
	}
	details := buildTenantAccessDeniedDetails(c, tenantSvc, client, profile)
	if resolveErr != nil || profile == nil {
		_ = tenantNotAllowedError(c, details)
		return true
	}
	if !clientTenantAccessAllowedForSubtree(c, tenantSvc, profile, client) {
		_ = tenantNotAllowedError(c, details)
		return true
	}
	return false
}

func buildTenantAccessDeniedDetails(c *fiber.Ctx, tenantSvc service.TenantService, client domain.HydraClient, profile *domain.UserProfileResponse) tenantAccessDeniedDetails {
	details := tenantAccessDeniedDetails{
		Account:           tenantAccessDeniedAccount{Email: strings.TrimSpace(profileEmail(profile))},
		CurrentTenant:     resolveCurrentTenantDetails(c, tenantSvc, profile),
		AffiliatedTenants: resolveAffiliatedTenantDetails(c, tenantSvc, profile),
	}

	for _, identifier := range clientAllowedTenants(client.Metadata) {
		details.AllowedTenants = append(details.AllowedTenants, resolveAllowedTenantDetails(c, tenantSvc, identifier))
	}

	return details
}

func resolveAffiliatedTenantDetails(c *fiber.Ctx, tenantSvc service.TenantService, profile *domain.UserProfileResponse) []tenantAccessDeniedTenant {
	if profile == nil {
		return nil
	}

	seen := make(map[string]struct{})
	out := make([]tenantAccessDeniedTenant, 0, len(profile.JoinedTenants)+1)
	appendTenant := func(tenant tenantAccessDeniedTenant) {
		key := strings.ToLower(firstNonEmptyString(tenant.ID, tenant.Slug, tenant.Identifier, tenant.Name))
		if key == "" {
			return
		}
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		out = append(out, tenant)
	}

	appendTenant(resolveCurrentTenantDetails(c, tenantSvc, profile))

	for _, joined := range profile.JoinedTenants {
		appendTenant(tenantAccessDeniedTenant{
			ID:         strings.TrimSpace(joined.ID),
			Slug:       strings.TrimSpace(joined.Slug),
			Name:       strings.TrimSpace(joined.Name),
			Identifier: firstNonEmptyString(strings.TrimSpace(joined.Slug), strings.TrimSpace(joined.ID)),
		})
	}

	return out
}

func resolveCurrentTenantDetails(c *fiber.Ctx, tenantSvc service.TenantService, profile *domain.UserProfileResponse) tenantAccessDeniedTenant {
	if profile == nil {
		return tenantAccessDeniedTenant{}
	}

	if profile.Tenant != nil {
		return tenantAccessDeniedTenant{
			ID:         strings.TrimSpace(profile.Tenant.ID),
			Slug:       strings.TrimSpace(profile.Tenant.Slug),
			Name:       strings.TrimSpace(profile.Tenant.Name),
			Identifier: firstNonEmptyString(strings.TrimSpace(profile.Tenant.Slug), strings.TrimSpace(profile.Tenant.ID)),
		}
	}

	if tenantSvc != nil {
		if profile.TenantID != nil && strings.TrimSpace(*profile.TenantID) != "" {
			if tenant, err := tenantSvc.GetTenant(c.Context(), strings.TrimSpace(*profile.TenantID)); err == nil && tenant != nil {
				return tenantAccessDeniedTenant{
					ID:         strings.TrimSpace(tenant.ID),
					Slug:       strings.TrimSpace(tenant.Slug),
					Name:       strings.TrimSpace(tenant.Name),
					Identifier: firstNonEmptyString(strings.TrimSpace(tenant.Slug), strings.TrimSpace(tenant.ID)),
				}
			}
		}
	}

	return tenantAccessDeniedTenant{
		ID:         strings.TrimSpace(pointerValue(profile.TenantID)),
		Identifier: strings.TrimSpace(pointerValue(profile.TenantID)),
	}
}

func resolveAllowedTenantDetails(c *fiber.Ctx, tenantSvc service.TenantService, identifier string) tenantAccessDeniedTenant {
	identifier = strings.TrimSpace(identifier)
	if identifier == "" {
		return tenantAccessDeniedTenant{}
	}

	if tenantSvc != nil {
		if tenant, err := tenantSvc.GetTenant(c.Context(), identifier); err == nil && tenant != nil {
			return tenantAccessDeniedTenant{
				ID:         strings.TrimSpace(tenant.ID),
				Slug:       strings.TrimSpace(tenant.Slug),
				Name:       strings.TrimSpace(tenant.Name),
				Identifier: firstNonEmptyString(strings.TrimSpace(tenant.Slug), strings.TrimSpace(tenant.ID), identifier),
			}
		}
		if tenant, err := tenantSvc.GetTenantBySlug(c.Context(), identifier); err == nil && tenant != nil {
			return tenantAccessDeniedTenant{
				ID:         strings.TrimSpace(tenant.ID),
				Slug:       strings.TrimSpace(tenant.Slug),
				Name:       strings.TrimSpace(tenant.Name),
				Identifier: firstNonEmptyString(strings.TrimSpace(tenant.Slug), strings.TrimSpace(tenant.ID), identifier),
			}
		}
	}

	return tenantAccessDeniedTenant{Identifier: identifier}
}

func profileEmail(profile *domain.UserProfileResponse) string {
	if profile == nil {
		return ""
	}
	return profile.Email
}

func pointerValue(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

type clientStructuredScope struct {
	Name      string `json:"name"`
	Mandatory bool   `json:"mandatory"`
	Locked    bool   `json:"locked"`
}

func mergeRequestedScopesWithClientRequirements(client domain.HydraClient, requested []string) []string {
	combined := make([]string, 0, len(requested)+2)
	combined = append(combined, requested...)
	combined = append(combined, requiredClientScopes(client)...)

	return normalizeScopesInConsentOrder(combined)
}

func normalizeScopesInConsentOrder(scopes []string) []string {
	combined := make([]string, 0, len(scopes))
	combined = append(combined, scopes...)

	seen := make(map[string]struct{}, len(combined))
	out := make([]string, 0, len(combined))

	appendIfPresent := func(scope string) {
		scope = strings.TrimSpace(scope)
		if scope == "" || isLegacyRefreshTokenScopeAlias(scope) {
			return
		}
		if _, ok := seen[scope]; ok {
			return
		}
		for _, candidate := range combined {
			if strings.TrimSpace(candidate) != scope {
				continue
			}
			seen[scope] = struct{}{}
			out = append(out, scope)
			return
		}
	}

	appendIfPresent("openid")
	appendIfPresent("tenant")

	for _, scope := range combined {
		scope = strings.TrimSpace(scope)
		if scope == "" || isLegacyRefreshTokenScopeAlias(scope) {
			continue
		}
		if _, ok := seen[scope]; ok {
			continue
		}
		seen[scope] = struct{}{}
		out = append(out, scope)
	}

	return out
}

func requiredClientScopes(client domain.HydraClient) []string {
	required := make([]string, 0, 4)
	if clientTenantAccessRestricted(client.Metadata) {
		required = append(required, "tenant")
	}

	if client.Metadata == nil {
		return normalizeScopesInConsentOrder(required)
	}

	rawStructuredScopes, ok := client.Metadata["structured_scopes"]
	if !ok || rawStructuredScopes == nil {
		return normalizeScopesInConsentOrder(required)
	}

	rawBytes, err := json.Marshal(rawStructuredScopes)
	if err != nil {
		return normalizeScopesInConsentOrder(required)
	}

	var scopes []clientStructuredScope
	if err := json.Unmarshal(rawBytes, &scopes); err != nil {
		return normalizeScopesInConsentOrder(required)
	}

	for _, scope := range scopes {
		name := strings.TrimSpace(scope.Name)
		if name == "" {
			continue
		}
		if scope.Mandatory || scope.Locked {
			required = append(required, name)
		}
	}

	return normalizeScopesInConsentOrder(required)
}
