package store

import (
	"context"
	"encoding/json"
	"strings"

	"github.com/jackc/pgx/v5"
	"golang.org/x/crypto/bcrypt"
)

type GatewayTenantInput struct {
	TenantKey          string         `json:"tenantKey"`
	Source             string         `json:"source"`
	ExternalTenantID   string         `json:"externalTenantId"`
	Name               string         `json:"name"`
	Description        string         `json:"description"`
	DefaultUserGroupID string         `json:"defaultUserGroupId"`
	PlanKey            string         `json:"planKey"`
	BillingProfile     map[string]any `json:"billingProfile"`
	RateLimitPolicy    map[string]any `json:"rateLimitPolicy"`
	AuthPolicy         map[string]any `json:"authPolicy"`
	Metadata           map[string]any `json:"metadata"`
	Status             string         `json:"status"`
}

type GatewayUserInput struct {
	UserKey            string         `json:"userKey"`
	Source             string         `json:"source"`
	ExternalUserID     string         `json:"externalUserId"`
	Username           string         `json:"username"`
	DisplayName        string         `json:"displayName"`
	Email              string         `json:"email"`
	Phone              string         `json:"phone"`
	AvatarURL          string         `json:"avatarUrl"`
	Password           string         `json:"password"`
	GatewayTenantID    string         `json:"gatewayTenantId"`
	TenantID           string         `json:"tenantId"`
	TenantKey          string         `json:"tenantKey"`
	DefaultUserGroupID string         `json:"defaultUserGroupId"`
	Roles              []string       `json:"roles"`
	AuthProfile        map[string]any `json:"authProfile"`
	Metadata           map[string]any `json:"metadata"`
	Status             string         `json:"status"`
}

type UserGroupInput struct {
	GroupKey               string         `json:"groupKey"`
	Name                   string         `json:"name"`
	Description            string         `json:"description"`
	Source                 string         `json:"source"`
	Priority               int            `json:"priority"`
	RechargeDiscountPolicy map[string]any `json:"rechargeDiscountPolicy"`
	BillingDiscountPolicy  map[string]any `json:"billingDiscountPolicy"`
	RateLimitPolicy        map[string]any `json:"rateLimitPolicy"`
	QuotaPolicy            map[string]any `json:"quotaPolicy"`
	Metadata               map[string]any `json:"metadata"`
	Status                 string         `json:"status"`
}

func (s *Store) CreateTenant(ctx context.Context, input GatewayTenantInput) (GatewayTenant, error) {
	input = normalizeTenantInput(input)
	billingProfile, _ := json.Marshal(emptyObjectIfNil(input.BillingProfile))
	rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
	authPolicy, _ := json.Marshal(emptyObjectIfNil(input.AuthPolicy))
	metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
	return scanTenant(s.pool.QueryRow(ctx, `
INSERT INTO gateway_tenants (
  tenant_key, source, external_tenant_id, name, description, default_user_group_id, plan_key,
  billing_profile, rate_limit_policy, auth_policy, metadata, status
)
VALUES ($1, $2, NULLIF($3, ''), $4, NULLIF($5, ''), NULLIF($6, '')::uuid, NULLIF($7, ''), $8, $9, $10, $11, $12)
RETURNING `+tenantColumns,
		input.TenantKey, input.Source, input.ExternalTenantID, input.Name, input.Description, input.DefaultUserGroupID, input.PlanKey,
		billingProfile, rateLimitPolicy, authPolicy, metadata, input.Status,
	))
}

func (s *Store) UpdateTenant(ctx context.Context, id string, input GatewayTenantInput) (GatewayTenant, error) {
	input = normalizeTenantInput(input)
	billingProfile, _ := json.Marshal(emptyObjectIfNil(input.BillingProfile))
	rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
	authPolicy, _ := json.Marshal(emptyObjectIfNil(input.AuthPolicy))
	metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
	return scanTenant(s.pool.QueryRow(ctx, `
UPDATE gateway_tenants
SET tenant_key = $2,
    source = $3,
    external_tenant_id = NULLIF($4, ''),
    name = $5,
    description = NULLIF($6, ''),
    default_user_group_id = NULLIF($7, '')::uuid,
    plan_key = NULLIF($8, ''),
    billing_profile = $9,
    rate_limit_policy = $10,
    auth_policy = $11,
    metadata = $12,
    status = $13,
    updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL
RETURNING `+tenantColumns,
		id, input.TenantKey, input.Source, input.ExternalTenantID, input.Name, input.Description, input.DefaultUserGroupID,
		input.PlanKey, billingProfile, rateLimitPolicy, authPolicy, metadata, input.Status,
	))
}

func (s *Store) DeleteTenant(ctx context.Context, id string) error {
	result, err := s.pool.Exec(ctx, `
UPDATE gateway_tenants
SET deleted_at = now(),
    status = 'deleted',
    tenant_key = tenant_key || ':deleted:' || left(id::text, 8),
    external_tenant_id = NULL,
    updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL`, id)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return pgx.ErrNoRows
	}
	return nil
}

func (s *Store) CreateGatewayUser(ctx context.Context, input GatewayUserInput) (GatewayUser, error) {
	input = normalizeUserInput(input)
	passwordHash := ""
	if strings.TrimSpace(input.Password) != "" {
		hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
		if err != nil {
			return GatewayUser{}, err
		}
		passwordHash = string(hash)
	}
	roles, _ := json.Marshal(input.Roles)
	authProfile, _ := json.Marshal(emptyObjectIfNil(input.AuthProfile))
	metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
	return scanUser(s.pool.QueryRow(ctx, `
INSERT INTO gateway_users (
  user_key, source, external_user_id, username, display_name, email, phone, avatar_url, password_hash,
  gateway_tenant_id, tenant_id, tenant_key, default_user_group_id, roles, auth_profile, metadata, status
)
VALUES (
  $1, $2, NULLIF($3, ''), $4, NULLIF($5, ''), NULLIF($6, ''), NULLIF($7, ''), NULLIF($8, ''), NULLIF($9, ''),
  NULLIF($10, '')::uuid, NULLIF($11, ''), NULLIF($12, ''), NULLIF($13, '')::uuid, $14, $15, $16, $17
)
RETURNING `+userColumns,
		input.UserKey, input.Source, input.ExternalUserID, input.Username, input.DisplayName, input.Email, input.Phone, input.AvatarURL, passwordHash,
		input.GatewayTenantID, input.TenantID, input.TenantKey, input.DefaultUserGroupID, roles, authProfile, metadata, input.Status,
	))
}

func (s *Store) UpdateGatewayUser(ctx context.Context, id string, input GatewayUserInput) (GatewayUser, error) {
	input = normalizeUserInput(input)
	passwordHash := ""
	if strings.TrimSpace(input.Password) != "" {
		hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
		if err != nil {
			return GatewayUser{}, err
		}
		passwordHash = string(hash)
	}
	roles, _ := json.Marshal(input.Roles)
	authProfile, _ := json.Marshal(emptyObjectIfNil(input.AuthProfile))
	metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
	return scanUser(s.pool.QueryRow(ctx, `
UPDATE gateway_users
SET user_key = $2,
    source = $3,
    external_user_id = NULLIF($4, ''),
    username = $5,
    display_name = NULLIF($6, ''),
    email = NULLIF($7, ''),
    phone = NULLIF($8, ''),
    avatar_url = NULLIF($9, ''),
    password_hash = COALESCE(NULLIF($10, ''), password_hash),
    gateway_tenant_id = NULLIF($11, '')::uuid,
    tenant_id = NULLIF($12, ''),
    tenant_key = NULLIF($13, ''),
    default_user_group_id = NULLIF($14, '')::uuid,
    roles = $15,
    auth_profile = $16,
    metadata = $17,
    status = $18,
    updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL
RETURNING `+userColumns,
		id, input.UserKey, input.Source, input.ExternalUserID, input.Username, input.DisplayName, input.Email, input.Phone,
		input.AvatarURL, passwordHash, input.GatewayTenantID, input.TenantID, input.TenantKey, input.DefaultUserGroupID,
		roles, authProfile, metadata, input.Status,
	))
}

func (s *Store) DeleteGatewayUser(ctx context.Context, id string) error {
	result, err := s.pool.Exec(ctx, `
UPDATE gateway_users
SET deleted_at = now(),
    status = 'deleted',
    user_key = user_key || ':deleted:' || left(id::text, 8),
    external_user_id = CASE WHEN source = 'oidc' THEN external_user_id ELSE NULL END,
    email = NULL,
    updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL`, id)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return pgx.ErrNoRows
	}
	return nil
}

func (s *Store) CreateUserGroup(ctx context.Context, input UserGroupInput) (UserGroup, error) {
	input = normalizeUserGroupInput(input)
	normalizedPolicy, err := NormalizeAndValidateRateLimitPolicy(input.RateLimitPolicy)
	if err != nil {
		return UserGroup{}, err
	}
	input.RateLimitPolicy = normalizedPolicy
	rechargeDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.RechargeDiscountPolicy))
	billingDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.BillingDiscountPolicy))
	rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
	quotaPolicy, _ := json.Marshal(emptyObjectIfNil(input.QuotaPolicy))
	metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
	return scanUserGroup(s.pool.QueryRow(ctx, `
INSERT INTO gateway_user_groups (
  group_key, name, description, source, priority, recharge_discount_policy, billing_discount_policy,
  rate_limit_policy, quota_policy, metadata, status
)
VALUES ($1, $2, NULLIF($3, ''), $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING `+userGroupColumns,
		input.GroupKey, input.Name, input.Description, input.Source, input.Priority, rechargeDiscountPolicy, billingDiscountPolicy,
		rateLimitPolicy, quotaPolicy, metadata, input.Status,
	))
}

func (s *Store) UpdateUserGroup(ctx context.Context, id string, input UserGroupInput) (UserGroup, error) {
	input = normalizeUserGroupInput(input)
	normalizedPolicy, err := NormalizeAndValidateRateLimitPolicy(input.RateLimitPolicy)
	if err != nil {
		return UserGroup{}, err
	}
	input.RateLimitPolicy = normalizedPolicy
	rechargeDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.RechargeDiscountPolicy))
	billingDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.BillingDiscountPolicy))
	rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
	quotaPolicy, _ := json.Marshal(emptyObjectIfNil(input.QuotaPolicy))
	metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
	return scanUserGroup(s.pool.QueryRow(ctx, `
UPDATE gateway_user_groups
SET group_key = $2,
    name = $3,
    description = NULLIF($4, ''),
    source = $5,
    priority = $6,
    recharge_discount_policy = $7,
    billing_discount_policy = $8,
    rate_limit_policy = $9,
    quota_policy = $10,
    metadata = $11,
    status = $12,
    updated_at = now()
WHERE id = $1::uuid
RETURNING `+userGroupColumns,
		id, input.GroupKey, input.Name, input.Description, input.Source, input.Priority,
		rechargeDiscountPolicy, billingDiscountPolicy, rateLimitPolicy, quotaPolicy, metadata, input.Status,
	))
}

func (s *Store) DeleteUserGroup(ctx context.Context, id string) error {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer rollbackTransaction(tx)

	if _, err := tx.Exec(ctx, `
DELETE FROM gateway_access_rules
WHERE subject_type = 'user_group' AND subject_id = $1::uuid`, id); err != nil {
		return err
	}
	result, err := tx.Exec(ctx, `DELETE FROM gateway_user_groups WHERE id = $1::uuid`, id)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return pgx.ErrNoRows
	}
	return tx.Commit(ctx)
}

const tenantColumns = `
id::text, tenant_key, source, COALESCE(external_tenant_id, ''), name, COALESCE(description, ''),
COALESCE(default_user_group_id::text, ''), COALESCE(plan_key, ''), billing_profile, rate_limit_policy,
auth_policy, metadata, status, COALESCE(synced_at::text, ''), COALESCE(source_updated_at::text, ''),
created_at, updated_at`

const userColumns = `
id::text, user_key, source, COALESCE(external_user_id, ''), username,
COALESCE(display_name, ''), COALESCE(email, ''), COALESCE(phone, ''), COALESCE(avatar_url, ''),
COALESCE(gateway_tenant_id::text, ''), COALESCE(tenant_id, ''), COALESCE(tenant_key, ''),
COALESCE(default_user_group_id::text, ''), roles, auth_profile, metadata,
status, COALESCE(last_login_at::text, ''), COALESCE(synced_at::text, ''), COALESCE(source_updated_at::text, ''),
created_at, updated_at`

const userGroupColumns = `
id::text, group_key, name, COALESCE(description, ''), source, priority,
recharge_discount_policy, billing_discount_policy, rate_limit_policy, quota_policy, metadata,
status, created_at, updated_at`

type scanner interface {
	Scan(dest ...any) error
}

func scanTenant(row scanner) (GatewayTenant, error) {
	var item GatewayTenant
	var billingProfile []byte
	var rateLimitPolicy []byte
	var authPolicy []byte
	var metadata []byte
	if err := row.Scan(
		&item.ID, &item.TenantKey, &item.Source, &item.ExternalTenantID, &item.Name, &item.Description,
		&item.DefaultUserGroupID, &item.PlanKey, &billingProfile, &rateLimitPolicy, &authPolicy, &metadata,
		&item.Status, &item.SyncedAt, &item.SourceUpdatedAt, &item.CreatedAt, &item.UpdatedAt,
	); err != nil {
		return GatewayTenant{}, err
	}
	item.BillingProfile = decodeObject(billingProfile)
	item.RateLimitPolicy = decodeObject(rateLimitPolicy)
	item.AuthPolicy = decodeObject(authPolicy)
	item.Metadata = decodeObject(metadata)
	return item, nil
}

func scanUser(row scanner) (GatewayUser, error) {
	var item GatewayUser
	var roles []byte
	var authProfile []byte
	var metadata []byte
	if err := row.Scan(
		&item.ID, &item.UserKey, &item.Source, &item.ExternalUserID, &item.Username, &item.DisplayName,
		&item.Email, &item.Phone, &item.AvatarURL, &item.GatewayTenantID, &item.TenantID, &item.TenantKey,
		&item.DefaultUserGroupID, &roles, &authProfile, &metadata, &item.Status, &item.LastLoginAt,
		&item.SyncedAt, &item.SourceUpdatedAt, &item.CreatedAt, &item.UpdatedAt,
	); err != nil {
		return GatewayUser{}, err
	}
	item.Roles = decodeStringArray(roles)
	item.AuthProfile = decodeObject(authProfile)
	item.Metadata = decodeObject(metadata)
	return item, nil
}

func scanUserGroup(row scanner) (UserGroup, error) {
	var item UserGroup
	var rechargeDiscountPolicy []byte
	var billingDiscountPolicy []byte
	var rateLimitPolicy []byte
	var quotaPolicy []byte
	var metadata []byte
	if err := row.Scan(
		&item.ID, &item.GroupKey, &item.Name, &item.Description, &item.Source, &item.Priority,
		&rechargeDiscountPolicy, &billingDiscountPolicy, &rateLimitPolicy, &quotaPolicy, &metadata,
		&item.Status, &item.CreatedAt, &item.UpdatedAt,
	); err != nil {
		return UserGroup{}, err
	}
	item.RechargeDiscountPolicy = decodeObject(rechargeDiscountPolicy)
	item.BillingDiscountPolicy = decodeObject(billingDiscountPolicy)
	item.RateLimitPolicy = decodeObject(rateLimitPolicy)
	item.QuotaPolicy = decodeObject(quotaPolicy)
	item.Metadata = decodeObject(metadata)
	return item, nil
}

func normalizeTenantInput(input GatewayTenantInput) GatewayTenantInput {
	input.TenantKey = strings.TrimSpace(input.TenantKey)
	input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
	input.Name = strings.TrimSpace(input.Name)
	input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
	return input
}

func normalizeUserInput(input GatewayUserInput) GatewayUserInput {
	input.Username = strings.TrimSpace(input.Username)
	input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
	input.UserKey = firstNonEmpty(strings.TrimSpace(input.UserKey), input.Source+":"+input.Username)
	input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
	input.Roles = normalizeGatewayRoles(input.Roles)
	return input
}

func normalizeGatewayRoles(roles []string) []string {
	for _, role := range roles {
		switch strings.TrimSpace(role) {
		case "manager":
			return []string{"manager"}
		case "admin":
			return []string{"admin"}
		case "operator":
			return []string{"operator"}
		case "creator":
			return []string{"creator"}
		case "user":
			return []string{"user"}
		}
	}
	return []string{"user"}
}

func normalizeUserGroupInput(input UserGroupInput) UserGroupInput {
	input.GroupKey = strings.TrimSpace(input.GroupKey)
	input.Name = strings.TrimSpace(input.Name)
	input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
	input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
	if input.Priority == 0 {
		input.Priority = 100
	}
	return input
}
