package store

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
)

var (
	ErrOIDCUserNotProvisioned = errors.New("OIDC gateway user is not provisioned")
	ErrOIDCUserDisabled       = errors.New("OIDC gateway user is disabled")
	ErrOIDCTenantUnavailable  = errors.New("OIDC gateway tenant is unavailable")
)

type ResolveOrProvisionOIDCUserInput struct {
	Issuer                  string
	ApplicationID           string
	Subject                 string
	Username                string
	DisplayName             string
	Email                   string
	Phone                   string
	AvatarURL               string
	Roles                   []string
	ContextType             string
	TenantID                string
	TenantMode              string
	TenantName              string
	TenantSlug              string
	TenantMetadataStatus    string
	TenantMetadataVersion   string
	TenantMetadataETag      string
	TenantMetadataUpdatedAt time.Time
	OIDCClientID            string
	GatewayTenantKey        string
	ProvisioningEnabled     bool
	RequestIP               string
	UserAgent               string
}

type ResolveOrProvisionOIDCUserResult struct {
	User    *auth.User
	Created bool
	AuditID string
}

type oidcUserProjection struct {
	user          GatewayUser
	userGroupKey  string
	tenantStatus  string
	tenantDeleted bool
	groupStatus   string
	userDeleted   bool
}

func (s *Store) ResolveOrProvisionOIDCUser(ctx context.Context, input ResolveOrProvisionOIDCUserInput) (ResolveOrProvisionOIDCUserResult, error) {
	input = normalizeOIDCUserInput(input)
	switch oidcProjectionKind(input) {
	case "platform":
		return s.resolveOrProvisionOIDCPlatformUser(ctx, input)
	case "multi_tenant":
		return s.resolveOrProvisionOIDCMultiTenantUser(ctx, input)
	case "single_tenant":
		// Continue through the fixed local Tenant projection below.
	default:
		return ResolveOrProvisionOIDCUserResult{},
			errors.New("invalid OIDC user context")
	}
	if input.Issuer == "" || input.Subject == "" || input.TenantID == "" {
		return ResolveOrProvisionOIDCUserResult{}, errors.New("invalid OIDC user projection input")
	}

	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	defer rollbackTransaction(tx)

	projection, err := loadOIDCUserProjection(ctx, tx, input.Subject)
	if err == nil {
		result, resolveErr := s.syncExistingOIDCUser(ctx, tx, projection, input)
		if resolveErr != nil {
			return ResolveOrProvisionOIDCUserResult{}, resolveErr
		}
		if err := tx.Commit(ctx); err != nil {
			return ResolveOrProvisionOIDCUserResult{}, err
		}
		return result, nil
	}
	if !errors.Is(err, pgx.ErrNoRows) {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	if !input.ProvisioningEnabled {
		return ResolveOrProvisionOIDCUserResult{}, ErrOIDCUserNotProvisioned
	}
	if input.GatewayTenantKey == "" {
		return ResolveOrProvisionOIDCUserResult{}, ErrOIDCTenantUnavailable
	}

	tenantID, userGroupID, userGroupKey, err := loadOIDCProvisioningTenant(ctx, tx, input.GatewayTenantKey)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return ResolveOrProvisionOIDCUserResult{}, ErrOIDCTenantUnavailable
		}
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	rolesJSON, err := json.Marshal(input.Roles)
	if err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	userKey := deriveOIDCUserKey(input.Issuer, input.Subject)
	username := input.Username
	if username == "" {
		username = "oidc-" + strings.TrimPrefix(userKey, "oidc:")[:12]
	}
	metadataJSON := `{"provisioningMode":"oidc-jit"}`

	createdUser, err := scanUser(tx.QueryRow(ctx, `
INSERT INTO gateway_users (
  user_key, source, external_user_id, username, display_name, email, phone, avatar_url,
  gateway_tenant_id, tenant_id, tenant_key,
  default_user_group_id, roles, auth_profile, metadata, status, last_login_at, synced_at, source_updated_at
)
VALUES ($1, 'oidc', $2, $3, NULLIF($4, ''), NULLIF($5, ''), NULLIF($6, ''), NULLIF($7, ''),
        $8::uuid, $9, $10, $11::uuid, $12::jsonb, '{}'::jsonb, $13::jsonb,
        'active', now(), now(), now())
ON CONFLICT DO NOTHING
RETURNING `+userColumns,
		userKey,
		input.Subject,
		username,
		input.DisplayName,
		input.Email,
		input.Phone,
		input.AvatarURL,
		tenantID,
		input.TenantID,
		input.GatewayTenantKey,
		userGroupID,
		string(rolesJSON),
		metadataJSON,
	))
	if err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	if errors.Is(err, pgx.ErrNoRows) {
		projection, err = loadOIDCUserProjection(ctx, tx, input.Subject)
		if err != nil {
			return ResolveOrProvisionOIDCUserResult{}, err
		}
		result, resolveErr := s.syncExistingOIDCUser(ctx, tx, projection, input)
		if resolveErr != nil {
			return ResolveOrProvisionOIDCUserResult{}, resolveErr
		}
		if err := tx.Commit(ctx); err != nil {
			return ResolveOrProvisionOIDCUserResult{}, err
		}
		return result, nil
	}

	if _, err := s.ensureWalletAccount(ctx, tx, createdUser.ID, "resource"); err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	subjectHash := sha256.Sum256([]byte(input.Subject))
	audit, err := s.RecordAuditLogTx(ctx, tx, AuditLogInput{
		Category:              "identity",
		Action:                "identity.oidc_user.provisioned",
		ActorGatewayUserID:    createdUser.ID,
		ActorUsername:         createdUser.Username,
		ActorSource:           "oidc",
		ActorRoles:            createdUser.Roles,
		TargetType:            "gateway_user",
		TargetID:              createdUser.ID,
		TargetGatewayUserID:   createdUser.ID,
		TargetGatewayTenantID: createdUser.GatewayTenantID,
		RequestIP:             input.RequestIP,
		UserAgent:             input.UserAgent,
		AfterState: map[string]any{
			"source":      "oidc",
			"tenantKey":   createdUser.TenantKey,
			"userGroupId": createdUser.DefaultUserGroupID,
		},
		Metadata: map[string]any{
			"provisioningMode":    "oidc-jit",
			"externalSubjectHash": hex.EncodeToString(subjectHash[:])[:16],
		},
	})
	if err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	return ResolveOrProvisionOIDCUserResult{
		User:    authUserFromOIDCProjection(createdUser, userGroupKey),
		Created: true,
		AuditID: audit.ID,
	}, nil
}

func oidcProjectionKind(input ResolveOrProvisionOIDCUserInput) string {
	switch input.ContextType {
	case "platform":
		if input.TenantMode == "multi_tenant" &&
			uuid.Validate(input.ApplicationID) == nil &&
			input.TenantID == "" {
			return "platform"
		}
	case "tenant":
		if input.TenantID == "" {
			return ""
		}
		switch input.TenantMode {
		case "multi_tenant":
			if uuid.Validate(input.ApplicationID) == nil &&
				uuid.Validate(input.TenantID) == nil {
				return "multi_tenant"
			}
		case "single_tenant":
			return "single_tenant"
		}
	}
	return ""
}

func (s *Store) syncExistingOIDCUser(ctx context.Context, tx pgx.Tx, projection oidcUserProjection, input ResolveOrProvisionOIDCUserInput) (ResolveOrProvisionOIDCUserResult, error) {
	if projection.userDeleted || projection.user.Status != "active" {
		return ResolveOrProvisionOIDCUserResult{}, ErrOIDCUserDisabled
	}
	if projection.tenantDeleted || projection.tenantStatus != "active" || projection.groupStatus != "active" ||
		projection.user.GatewayTenantID == "" || projection.user.DefaultUserGroupID == "" || projection.userGroupKey == "" {
		return ResolveOrProvisionOIDCUserResult{}, ErrOIDCTenantUnavailable
	}
	if input.GatewayTenantKey != "" && projection.user.TenantKey != input.GatewayTenantKey {
		return ResolveOrProvisionOIDCUserResult{}, ErrOIDCTenantUnavailable
	}
	if projection.user.TenantID != "" && projection.user.TenantID != input.TenantID {
		return ResolveOrProvisionOIDCUserResult{}, ErrOIDCTenantUnavailable
	}

	rolesJSON, err := json.Marshal(input.Roles)
	if err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	updated, err := scanUser(tx.QueryRow(ctx, `
UPDATE gateway_users
SET username = COALESCE(NULLIF($2, ''), username),
    display_name = CASE WHEN metadata->>'manualProfile' = 'true' THEN display_name ELSE COALESCE(NULLIF($3, ''), display_name) END,
    email = CASE WHEN metadata->>'manualProfile' = 'true' THEN email ELSE COALESCE(NULLIF($4, ''), email) END,
    phone = CASE WHEN metadata->>'manualProfile' = 'true' THEN phone ELSE COALESCE(NULLIF($5, ''), phone) END,
    avatar_url = CASE WHEN metadata->>'manualProfile' = 'true' THEN avatar_url ELSE COALESCE(NULLIF($6, ''), avatar_url) END,
    roles = $7::jsonb,
    last_login_at = now(),
    synced_at = now(),
    source_updated_at = now(),
    updated_at = now()
WHERE id = $1::uuid
  AND source = 'oidc'
  AND deleted_at IS NULL
  AND status = 'active'
RETURNING `+userColumns,
		projection.user.ID,
		input.Username,
		input.DisplayName,
		input.Email,
		input.Phone,
		input.AvatarURL,
		string(rolesJSON),
	))
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return ResolveOrProvisionOIDCUserResult{}, ErrOIDCUserDisabled
		}
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	return ResolveOrProvisionOIDCUserResult{User: authUserFromOIDCProjection(updated, projection.userGroupKey)}, nil
}

func loadOIDCUserProjection(ctx context.Context, tx pgx.Tx, subject string) (oidcUserProjection, error) {
	var projection oidcUserProjection
	var roles []byte
	var authProfile []byte
	var metadata []byte
	err := tx.QueryRow(ctx, `
SELECT
  u.id::text, u.user_key, u.source, COALESCE(u.external_user_id, ''), u.username,
  COALESCE(u.display_name, ''), COALESCE(u.email, ''), COALESCE(u.phone, ''), COALESCE(u.avatar_url, ''),
  COALESCE(u.gateway_tenant_id::text, ''), COALESCE(u.tenant_id, ''), COALESCE(u.tenant_key, ''),
  COALESCE(u.default_user_group_id::text, ''), u.roles, u.auth_profile, u.metadata,
  u.status, COALESCE(u.last_login_at::text, ''), COALESCE(u.synced_at::text, ''), COALESCE(u.source_updated_at::text, ''),
  u.created_at, u.updated_at,
  COALESCE(g.group_key, ''), COALESCE(t.status, ''), t.deleted_at IS NOT NULL,
  COALESCE(g.status, ''), u.deleted_at IS NOT NULL
FROM gateway_users u
LEFT JOIN gateway_tenants t ON t.id = u.gateway_tenant_id
LEFT JOIN gateway_user_groups g ON g.id = u.default_user_group_id
WHERE u.source = 'oidc' AND u.external_user_id = $1
FOR UPDATE OF u`, subject).Scan(
		&projection.user.ID,
		&projection.user.UserKey,
		&projection.user.Source,
		&projection.user.ExternalUserID,
		&projection.user.Username,
		&projection.user.DisplayName,
		&projection.user.Email,
		&projection.user.Phone,
		&projection.user.AvatarURL,
		&projection.user.GatewayTenantID,
		&projection.user.TenantID,
		&projection.user.TenantKey,
		&projection.user.DefaultUserGroupID,
		&roles,
		&authProfile,
		&metadata,
		&projection.user.Status,
		&projection.user.LastLoginAt,
		&projection.user.SyncedAt,
		&projection.user.SourceUpdatedAt,
		&projection.user.CreatedAt,
		&projection.user.UpdatedAt,
		&projection.userGroupKey,
		&projection.tenantStatus,
		&projection.tenantDeleted,
		&projection.groupStatus,
		&projection.userDeleted,
	)
	if err != nil {
		return oidcUserProjection{}, err
	}
	projection.user.Roles = decodeStringArray(roles)
	projection.user.AuthProfile = decodeObject(authProfile)
	projection.user.Metadata = decodeObject(metadata)
	return projection, nil
}

func loadOIDCProvisioningTenant(ctx context.Context, tx pgx.Tx, tenantKey string) (string, string, string, error) {
	var tenantID string
	var groupID string
	var groupKey string
	err := tx.QueryRow(ctx, `
SELECT t.id::text, t.default_user_group_id::text, g.group_key
FROM gateway_tenants t
JOIN gateway_user_groups g ON g.id = t.default_user_group_id
WHERE t.tenant_key = $1
  AND t.status = 'active'
  AND t.deleted_at IS NULL
  AND g.status = 'active'`, tenantKey).Scan(&tenantID, &groupID, &groupKey)
	return tenantID, groupID, groupKey, err
}

func normalizeOIDCUserInput(input ResolveOrProvisionOIDCUserInput) ResolveOrProvisionOIDCUserInput {
	input.Issuer = strings.TrimRight(strings.TrimSpace(input.Issuer), "/")
	input.Subject = strings.TrimSpace(input.Subject)
	input.Username = strings.TrimSpace(input.Username)
	input.DisplayName = limitOIDCProfileText(input.DisplayName, 200)
	input.Email = limitOIDCProfileText(input.Email, 320)
	input.Phone = limitOIDCProfileText(input.Phone, 64)
	input.AvatarURL = limitOIDCProfileText(input.AvatarURL, 2048)
	input.ContextType = strings.TrimSpace(input.ContextType)
	input.TenantID = strings.TrimSpace(input.TenantID)
	input.ApplicationID = strings.TrimSpace(input.ApplicationID)
	input.TenantMode = strings.TrimSpace(input.TenantMode)
	input.TenantName = strings.TrimSpace(input.TenantName)
	input.TenantSlug = strings.TrimSpace(input.TenantSlug)
	input.TenantMetadataStatus = strings.TrimSpace(input.TenantMetadataStatus)
	input.TenantMetadataVersion = strings.TrimSpace(input.TenantMetadataVersion)
	input.TenantMetadataETag = strings.TrimSpace(input.TenantMetadataETag)
	input.GatewayTenantKey = strings.TrimSpace(input.GatewayTenantKey)
	input.RequestIP = strings.TrimSpace(input.RequestIP)
	input.UserAgent = strings.TrimSpace(input.UserAgent)
	input.Roles = normalizeOIDCRoles(input.Roles)
	return input
}

func limitOIDCProfileText(value string, limit int) string {
	value = strings.TrimSpace(value)
	runes := []rune(value)
	if len(runes) > limit {
		return string(runes[:limit])
	}
	return value
}

func normalizeOIDCRoles(roles []string) []string {
	result := make([]string, 0, len(roles))
	seen := make(map[string]struct{}, len(roles))
	for _, role := range roles {
		role = strings.TrimSpace(role)
		if role == "" {
			continue
		}
		if _, ok := seen[role]; ok {
			continue
		}
		seen[role] = struct{}{}
		result = append(result, role)
	}
	return result
}

func deriveOIDCUserKey(issuer string, subject string) string {
	issuer = strings.TrimRight(strings.TrimSpace(issuer), "/")
	sum := sha256.Sum256([]byte(issuer + "\x00" + strings.TrimSpace(subject)))
	return fmt.Sprintf("oidc:%x", sum)
}

func authUserFromOIDCProjection(user GatewayUser, userGroupKey string) *auth.User {
	groupKeys := []string(nil)
	if userGroupKey != "" {
		groupKeys = []string{userGroupKey}
	}
	return &auth.User{
		ID:              user.ExternalUserID,
		Username:        user.Username,
		DisplayName:     user.DisplayName,
		Email:           user.Email,
		Phone:           user.Phone,
		AvatarURL:       user.AvatarURL,
		Roles:           user.Roles,
		ContextType:     "tenant",
		TenantID:        user.TenantID,
		GatewayTenantID: user.GatewayTenantID,
		TenantKey:       user.TenantKey,
		Source:          "oidc",
		GatewayUserID:   user.ID,
		UserGroupID:     user.DefaultUserGroupID,
		UserGroupKey:    userGroupKey,
		UserGroupKeys:   groupKeys,
	}
}
