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"
)

func (s *Store) resolveOrProvisionOIDCMultiTenantUser(ctx context.Context, input ResolveOrProvisionOIDCUserInput) (ResolveOrProvisionOIDCUserResult, error) {
	if input.Issuer == "" || uuid.Validate(input.ApplicationID) != nil || uuid.Validate(input.TenantID) != nil ||
		input.Subject == "" || input.TenantMetadataStatus != "synced" && input.TenantMetadataStatus != "metadata_pending" {
		return ResolveOrProvisionOIDCUserResult{}, errors.New("invalid multi-tenant OIDC user projection input")
	}
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	defer rollbackTransaction(tx)

	bindingID, gatewayTenantID, tenantKey, groupID, groupKey, accessStatus, err :=
		loadOIDCTenantBinding(ctx, tx, input)
	if errors.Is(err, pgx.ErrNoRows) {
		if !input.ProvisioningEnabled {
			return ResolveOrProvisionOIDCUserResult{}, ErrOIDCUserNotProvisioned
		}
		bindingID, gatewayTenantID, tenantKey, groupID, groupKey, err =
			createOIDCTenantBinding(ctx, tx, input)
		accessStatus = "active"
	}
	if err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	if accessStatus != "active" {
		if input.TenantMetadataStatus != "synced" {
			return ResolveOrProvisionOIDCUserResult{}, ErrOIDCTenantUnavailable
		}
		tag, reactivateErr := tx.Exec(ctx, `UPDATE gateway_oidc_tenant_bindings
			SET access_status='active',metadata_status='synced',last_error_category=NULL,updated_at=now()
			WHERE id=$1::uuid AND access_status='disabled'`, bindingID)
		if reactivateErr != nil {
			return ResolveOrProvisionOIDCUserResult{}, reactivateErr
		}
		if tag.RowsAffected() != 1 {
			return ResolveOrProvisionOIDCUserResult{}, ErrOIDCTenantUnavailable
		}
		accessStatus = "active"
	}
	if input.TenantMetadataStatus == "synced" {
		if _, err := tx.Exec(ctx, `UPDATE gateway_oidc_tenant_bindings SET
			metadata_status='synced',display_name=$2,slug=$3,metadata_version=NULLIF($4,''),
			metadata_etag=NULLIF($5,''),metadata_updated_at=$6,
			last_sync_at=now(),next_sync_at=now()+interval '15 minutes',sync_failure_count=0,last_error_category=NULL,updated_at=now()
			WHERE id=$1::uuid AND access_status='active'`,
			bindingID, input.TenantName, input.TenantSlug, input.TenantMetadataVersion,
			input.TenantMetadataETag, nullableOIDCTenantMetadataTime(input.TenantMetadataUpdatedAt),
		); err != nil {
			return ResolveOrProvisionOIDCUserResult{}, err
		}
		if _, err := tx.Exec(ctx, `UPDATE gateway_tenants SET name=$2,synced_at=now(),source_updated_at=$3,updated_at=now()
			WHERE id=$1::uuid AND source='oidc_v2' AND status='active' AND deleted_at IS NULL`,
			gatewayTenantID, input.TenantName, input.TenantMetadataUpdatedAt,
		); err != nil {
			return ResolveOrProvisionOIDCUserResult{}, err
		}
	}

	user, userBindingID, err := loadOIDCMultiTenantUser(ctx, tx, bindingID, input.Subject)
	created := false
	if errors.Is(err, pgx.ErrNoRows) {
		if !input.ProvisioningEnabled {
			return ResolveOrProvisionOIDCUserResult{}, ErrOIDCUserNotProvisioned
		}
		user, userBindingID, created, err = s.createOIDCMultiTenantUser(
			ctx, tx, bindingID, gatewayTenantID, tenantKey, groupID, input,
		)
	}
	if err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	if user.Status != "active" {
		return ResolveOrProvisionOIDCUserResult{}, ErrOIDCUserDisabled
	}
	rolesJSON, _ := json.Marshal(input.Roles)
	user, 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_v2' AND status='active' AND deleted_at IS NULL
		RETURNING `+userColumns, user.ID, input.Username, input.DisplayName, input.Email,
		input.Phone, input.AvatarURL, string(rolesJSON)))
	if err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	var auditID string
	if created {
		subjectHash := sha256.Sum256([]byte(input.Subject))
		audit, err := s.RecordAuditLogTx(ctx, tx, AuditLogInput{
			Category: "identity", Action: "identity.oidc_user.provisioned",
			ActorGatewayUserID: user.ID, ActorUsername: user.Username, ActorSource: "oidc", ActorRoles: user.Roles,
			TargetType: "gateway_user", TargetID: user.ID, TargetGatewayUserID: user.ID,
			TargetGatewayTenantID: user.GatewayTenantID, RequestIP: input.RequestIP, UserAgent: input.UserAgent,
			AfterState: map[string]any{"source": "oidc_v2", "tenantKey": user.TenantKey, "userGroupId": user.DefaultUserGroupID},
			Metadata: map[string]any{
				"provisioningMode":    "oidc-multi-tenant-jit",
				"externalSubjectHash": hex.EncodeToString(subjectHash[:])[:16],
			},
		})
		if err != nil {
			return ResolveOrProvisionOIDCUserResult{}, err
		}
		auditID = audit.ID
	}
	if err := tx.Commit(ctx); err != nil {
		return ResolveOrProvisionOIDCUserResult{}, err
	}
	return ResolveOrProvisionOIDCUserResult{
		User:    multiTenantAuthUser(user, groupKey, input, userBindingID),
		Created: created, AuditID: auditID,
	}, nil
}

func loadOIDCTenantBinding(ctx context.Context, tx pgx.Tx, input ResolveOrProvisionOIDCUserInput) (string, string, string, string, string, string, error) {
	var bindingID, gatewayTenantID, tenantKey, groupID, groupKey, accessStatus string
	err := tx.QueryRow(ctx, `SELECT binding.id::text,tenant.id::text,tenant.tenant_key,
		tenant.default_user_group_id::text,group_record.group_key,binding.access_status
		FROM gateway_oidc_tenant_bindings binding
		JOIN gateway_tenants tenant ON tenant.id=binding.gateway_tenant_id
		JOIN gateway_user_groups group_record ON group_record.id=tenant.default_user_group_id
		WHERE binding.issuer=$1 AND binding.application_id=$2 AND binding.external_tenant_id=$3
		  AND tenant.status='active' AND tenant.deleted_at IS NULL AND group_record.status='active'
		FOR UPDATE OF binding,tenant`,
		input.Issuer, input.ApplicationID, input.TenantID,
	).Scan(&bindingID, &gatewayTenantID, &tenantKey, &groupID, &groupKey, &accessStatus)
	return bindingID, gatewayTenantID, tenantKey, groupID, groupKey, accessStatus, err
}

func createOIDCTenantBinding(ctx context.Context, tx pgx.Tx, input ResolveOrProvisionOIDCUserInput) (string, string, string, string, string, error) {
	var groupID, groupKey string
	if err := tx.QueryRow(ctx, `SELECT id::text,group_key FROM gateway_user_groups
		WHERE group_key='default' AND status='active'`).Scan(&groupID, &groupKey); err != nil {
		return "", "", "", "", "", ErrOIDCTenantUnavailable
	}
	tenantKey := deriveOIDCMultiTenantKey(input.Issuer, input.ApplicationID, input.TenantID)
	name := input.TenantName
	if name == "" {
		name = "认证中心租户 " + strings.ReplaceAll(input.TenantID, "-", "")[:8]
	}
	metadata, _ := json.Marshal(map[string]any{"provisioningMode": "oidc-multi-tenant-jit", "metadataStatus": input.TenantMetadataStatus})
	var gatewayTenantID string
	err := tx.QueryRow(ctx, `INSERT INTO gateway_tenants(
		tenant_key,source,external_tenant_id,name,default_user_group_id,metadata,status,synced_at,source_updated_at
	) VALUES($1,'oidc_v2',NULL,$2,$3::uuid,$4::jsonb,'active',
		CASE WHEN $5='synced' THEN now() ELSE NULL END,$6)
	ON CONFLICT(tenant_key) DO UPDATE SET updated_at=gateway_tenants.updated_at
	RETURNING id::text`, tenantKey, name, groupID, string(metadata), input.TenantMetadataStatus, nullableOIDCTenantMetadataTime(input.TenantMetadataUpdatedAt)).Scan(&gatewayTenantID)
	if err != nil {
		return "", "", "", "", "", err
	}
	var bindingID string
	err = tx.QueryRow(ctx, `INSERT INTO gateway_oidc_tenant_bindings(
		issuer,application_id,external_tenant_id,gateway_tenant_id,access_status,metadata_status,
		display_name,slug,metadata_version,metadata_etag,metadata_updated_at,last_sync_at,next_sync_at
	) VALUES($1,$2,$3,$4::uuid,'active',$5,NULLIF($6,''),NULLIF($7,''),NULLIF($8,''),NULLIF($9,''),
		$10,
		CASE WHEN $5='synced' THEN now() ELSE NULL END,
		CASE WHEN $5='synced' THEN now()+interval '15 minutes' ELSE now()+interval '30 seconds' END)
	ON CONFLICT(issuer,application_id,external_tenant_id) DO UPDATE SET updated_at=gateway_oidc_tenant_bindings.updated_at
	RETURNING id::text`,
		input.Issuer, input.ApplicationID, input.TenantID, gatewayTenantID, input.TenantMetadataStatus,
		input.TenantName, input.TenantSlug, input.TenantMetadataVersion, input.TenantMetadataETag,
		nullableOIDCTenantMetadataTime(input.TenantMetadataUpdatedAt),
	).Scan(&bindingID)
	return bindingID, gatewayTenantID, tenantKey, groupID, groupKey, err
}

func loadOIDCMultiTenantUser(ctx context.Context, tx pgx.Tx, bindingID, subject string) (GatewayUser, string, error) {
	var userBindingID, gatewayUserID string
	if err := tx.QueryRow(ctx, `SELECT binding.id::text,binding.gateway_user_id::text
		FROM gateway_oidc_user_bindings binding
		JOIN gateway_users ON gateway_users.id=binding.gateway_user_id
		WHERE binding.tenant_binding_id=$1::uuid AND binding.subject=$2
		  AND gateway_users.deleted_at IS NULL
		FOR UPDATE OF binding,gateway_users`, bindingID, subject).Scan(&userBindingID, &gatewayUserID); err != nil {
		return GatewayUser{}, "", err
	}
	user, err := scanUser(tx.QueryRow(ctx, `SELECT `+userColumns+`
		FROM gateway_users WHERE id=$1::uuid`, gatewayUserID))
	return user, userBindingID, err
}

func (s *Store) createOIDCMultiTenantUser(ctx context.Context, tx pgx.Tx, bindingID, gatewayTenantID, tenantKey, groupID string, input ResolveOrProvisionOIDCUserInput) (GatewayUser, string, bool, error) {
	userKey := deriveOIDCMultiTenantUserKey(input.Issuer, input.ApplicationID, input.TenantID, input.Subject)
	username := input.Username
	if username == "" {
		username = "oidc-" + strings.TrimPrefix(userKey, "oidc2:")[:12]
	}
	rolesJSON, _ := json.Marshal(input.Roles)
	metadata, _ := json.Marshal(map[string]any{"provisioningMode": "oidc-multi-tenant-jit"})
	user, 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_v2',NULL,$2,NULLIF($3,''),NULLIF($4,''),NULLIF($5,''),NULLIF($6,''),
		$7::uuid,$8,$9,$10::uuid,$11::jsonb,'{}'::jsonb,$12::jsonb,
		'active',now(),now(),now())
	ON CONFLICT(user_key) DO UPDATE SET updated_at=gateway_users.updated_at
	RETURNING `+userColumns,
		userKey, username, input.DisplayName, input.Email, input.Phone, input.AvatarURL,
		gatewayTenantID, input.TenantID, tenantKey, groupID, string(rolesJSON), string(metadata),
	))
	if err != nil {
		return GatewayUser{}, "", false, err
	}
	var userBindingID string
	tag, err := tx.Exec(ctx, `INSERT INTO gateway_oidc_user_bindings(tenant_binding_id,subject,gateway_user_id)
		VALUES($1::uuid,$2,$3::uuid) ON CONFLICT(tenant_binding_id,subject) DO NOTHING`,
		bindingID, input.Subject, user.ID)
	if err != nil {
		return GatewayUser{}, "", false, err
	}
	created := tag.RowsAffected() == 1
	if err := tx.QueryRow(ctx, `SELECT id::text FROM gateway_oidc_user_bindings
		WHERE tenant_binding_id=$1::uuid AND subject=$2`, bindingID, input.Subject).Scan(&userBindingID); err != nil {
		return GatewayUser{}, "", false, err
	}
	if _, err := s.ensureWalletAccount(ctx, tx, user.ID, "resource"); err != nil {
		return GatewayUser{}, "", false, err
	}
	return user, userBindingID, created, nil
}

func deriveOIDCMultiTenantKey(issuer, applicationID, tenantID string) string {
	sum := sha256.Sum256([]byte(strings.TrimRight(issuer, "/") + "\x00" + applicationID + "\x00" + tenantID))
	return fmt.Sprintf("oidc2-tenant-%x", sum[:16])
}

func nullableOIDCTenantMetadataTime(value time.Time) any {
	if value.IsZero() {
		return nil
	}
	return value
}

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

func multiTenantAuthUser(user GatewayUser, groupKey string, input ResolveOrProvisionOIDCUserInput, userBindingID string) *auth.User {
	result := authUserFromOIDCProjection(user, groupKey)
	result.ID = input.Subject
	result.TenantName = input.TenantName
	result.Issuer = input.Issuer
	result.ApplicationID = input.ApplicationID
	result.OIDCClientID = input.OIDCClientID
	result.OIDCUserBindingID = userBindingID
	return result
}
