package store

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

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

type AccessRuleInput struct {
	SubjectType        string         `json:"subjectType"`
	SubjectID          string         `json:"subjectId"`
	ResourceType       string         `json:"resourceType"`
	ResourceID         string         `json:"resourceId"`
	Effect             string         `json:"effect"`
	Priority           int            `json:"priority"`
	MinPermissionLevel int            `json:"minPermissionLevel"`
	Conditions         map[string]any `json:"conditions"`
	Metadata           map[string]any `json:"metadata"`
	Status             string         `json:"status"`
}

type AccessRuleResourceInput struct {
	ResourceType       string         `json:"resourceType"`
	ResourceID         string         `json:"resourceId"`
	Priority           int            `json:"priority"`
	MinPermissionLevel int            `json:"minPermissionLevel"`
	Conditions         map[string]any `json:"conditions"`
	Metadata           map[string]any `json:"metadata"`
	Status             string         `json:"status"`
}

type AccessRuleBatchInput struct {
	SubjectType     string                    `json:"subjectType"`
	SubjectID       string                    `json:"subjectId"`
	Effect          string                    `json:"effect"`
	UpsertResources []AccessRuleResourceInput `json:"upsertResources"`
	DeleteResources []AccessRuleResourceInput `json:"deleteResources"`
}

func (s *Store) ListAccessRules(ctx context.Context) ([]AccessRule, error) {
	rows, err := s.pool.Query(ctx, `
SELECT `+accessRuleColumns+`
FROM gateway_access_rules
ORDER BY resource_type ASC, priority ASC, subject_type ASC, created_at DESC`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]AccessRule, 0)
	for rows.Next() {
		item, err := scanAccessRule(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func (s *Store) ListAPIKeyAccessRules(ctx context.Context, user *auth.User) ([]AccessRule, error) {
	gatewayUserID := localGatewayUserID(user)
	if gatewayUserID == "" {
		return nil, ErrLocalUserRequired
	}
	rows, err := s.pool.Query(ctx, `
SELECT `+apiKeyAccessRuleColumns+`
FROM gateway_access_rules ar
JOIN gateway_api_keys k ON k.id = ar.subject_id
WHERE ar.subject_type = 'api_key'
  AND k.gateway_user_id = $1::uuid
  AND k.deleted_at IS NULL
ORDER BY ar.resource_type ASC, ar.priority ASC, ar.created_at DESC`, gatewayUserID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]AccessRule, 0)
	for rows.Next() {
		item, err := scanAccessRule(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func (s *Store) CreateAccessRule(ctx context.Context, input AccessRuleInput) (AccessRule, error) {
	input = normalizeAccessRuleInput(input)
	conditions, _ := json.Marshal(emptyObjectIfNil(input.Conditions))
	metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
	return scanAccessRule(s.pool.QueryRow(ctx, `
INSERT INTO gateway_access_rules (
  subject_type, subject_id, resource_type, resource_id, effect, priority,
  min_permission_level, conditions, metadata, status
)
VALUES ($1, $2::uuid, $3, $4::uuid, $5, $6, $7, $8::jsonb, $9::jsonb, $10)
RETURNING `+accessRuleColumns,
		input.SubjectType, input.SubjectID, input.ResourceType, input.ResourceID, input.Effect,
		input.Priority, input.MinPermissionLevel, string(conditions), string(metadata), input.Status,
	))
}

func (s *Store) UpdateAccessRule(ctx context.Context, id string, input AccessRuleInput) (AccessRule, error) {
	input = normalizeAccessRuleInput(input)
	conditions, _ := json.Marshal(emptyObjectIfNil(input.Conditions))
	metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
	return scanAccessRule(s.pool.QueryRow(ctx, `
UPDATE gateway_access_rules
SET subject_type = $2,
    subject_id = $3::uuid,
    resource_type = $4,
    resource_id = $5::uuid,
    effect = $6,
    priority = $7,
    min_permission_level = $8,
    conditions = $9::jsonb,
    metadata = $10::jsonb,
    status = $11,
    updated_at = now()
WHERE id = $1::uuid
RETURNING `+accessRuleColumns,
		id, input.SubjectType, input.SubjectID, input.ResourceType, input.ResourceID, input.Effect,
		input.Priority, input.MinPermissionLevel, string(conditions), string(metadata), input.Status,
	))
}

func (s *Store) DeleteAccessRule(ctx context.Context, id string) error {
	result, err := s.pool.Exec(ctx, `DELETE FROM gateway_access_rules WHERE id = $1::uuid`, id)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return pgx.ErrNoRows
	}
	return nil
}

func (s *Store) BatchAccessRules(ctx context.Context, input AccessRuleBatchInput) ([]AccessRule, error) {
	input = normalizeAccessRuleBatchInput(input)
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer rollbackTransaction(tx)

	for _, resource := range dedupeAccessRuleResources(input.DeleteResources) {
		resource = normalizeAccessRuleResource(resource, input.Effect)
		if resource.ResourceType == "" || resource.ResourceID == "" {
			continue
		}
		if _, err := tx.Exec(ctx, `
DELETE FROM gateway_access_rules
WHERE subject_type = $1
  AND subject_id = $2::uuid
  AND effect = $3
  AND resource_type = $4
  AND resource_id = $5::uuid`,
			input.SubjectType, input.SubjectID, input.Effect, resource.ResourceType, resource.ResourceID,
		); err != nil {
			return nil, err
		}
	}

	for _, resource := range dedupeAccessRuleResources(input.UpsertResources) {
		resource = normalizeAccessRuleResource(resource, input.Effect)
		if resource.ResourceType == "" || resource.ResourceID == "" {
			continue
		}
		oppositeEffect := "deny"
		if input.Effect == "deny" {
			oppositeEffect = "allow"
		}
		if _, err := tx.Exec(ctx, `
DELETE FROM gateway_access_rules
WHERE subject_type = $1
  AND subject_id = $2::uuid
  AND effect = $3
  AND resource_type = $4
  AND resource_id = $5::uuid`,
			input.SubjectType, input.SubjectID, oppositeEffect, resource.ResourceType, resource.ResourceID,
		); err != nil {
			return nil, err
		}
		conditions, _ := json.Marshal(emptyObjectIfNil(resource.Conditions))
		metadata, _ := json.Marshal(emptyObjectIfNil(resource.Metadata))
		if _, err := tx.Exec(ctx, `
INSERT INTO gateway_access_rules (
  subject_type, subject_id, resource_type, resource_id, effect, priority,
  min_permission_level, conditions, metadata, status
)
VALUES ($1, $2::uuid, $3, $4::uuid, $5, $6, $7, $8::jsonb, $9::jsonb, $10)
ON CONFLICT (subject_type, subject_id, resource_type, resource_id, effect)
DO UPDATE SET priority = EXCLUDED.priority,
              min_permission_level = EXCLUDED.min_permission_level,
              conditions = EXCLUDED.conditions,
              metadata = EXCLUDED.metadata,
              status = EXCLUDED.status,
              updated_at = now()`,
			input.SubjectType, input.SubjectID, resource.ResourceType, resource.ResourceID, input.Effect,
			resource.Priority, resource.MinPermissionLevel, string(conditions), string(metadata), resource.Status,
		); err != nil {
			return nil, err
		}
	}

	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	return s.ListAccessRules(ctx)
}

func (s *Store) BatchAPIKeyAccessRules(ctx context.Context, input AccessRuleBatchInput, user *auth.User) ([]AccessRule, error) {
	gatewayUserID := localGatewayUserID(user)
	if gatewayUserID == "" {
		return nil, ErrLocalUserRequired
	}
	input = normalizeAccessRuleBatchInput(input)
	if input.SubjectType != "api_key" {
		return nil, pgx.ErrNoRows
	}
	var exists bool
	if err := s.pool.QueryRow(ctx, `
SELECT EXISTS (
  SELECT 1
  FROM gateway_api_keys
  WHERE id = $1::uuid
    AND gateway_user_id = $2::uuid
    AND deleted_at IS NULL
)`, input.SubjectID, gatewayUserID).Scan(&exists); err != nil {
		return nil, err
	}
	if !exists {
		return nil, pgx.ErrNoRows
	}
	if err := s.ensureAPIKeyAccessRuleResourcesAllowed(ctx, user, input.SubjectID, input.UpsertResources); err != nil {
		return nil, err
	}
	if _, err := s.BatchAccessRules(ctx, input); err != nil {
		return nil, err
	}
	return s.ListAPIKeyAccessRules(ctx, user)
}

func (s *Store) filterCandidatesByAccessRules(ctx context.Context, user *auth.User, candidates []RuntimeModelCandidate) ([]RuntimeModelCandidate, error) {
	return s.filterRuntimeCandidatesByLayeredAccess(ctx, user, candidates)
}

func (s *Store) ListAccessiblePlatformModels(ctx context.Context, user *auth.User) ([]PlatformModel, error) {
	return s.listPlatformModelsForAccessRules(ctx, user, nil)
}

// ListAPIKeyAssignablePlatformModels returns the enabled models that the
// current user may delegate to their API keys. API-key rules are deliberately
// excluded here: they restrict individual credentials and must not shrink the
// resource pool that the owning user can manage.
func (s *Store) ListAPIKeyAssignablePlatformModels(ctx context.Context, user *auth.User) ([]PlatformModel, error) {
	if localGatewayUserID(user) == "" {
		return nil, ErrLocalUserRequired
	}
	return s.listPlatformModelsForAccessRules(ctx, user, map[string]bool{"api_key": true})
}

func (s *Store) listPlatformModelsForAccessRules(ctx context.Context, user *auth.User, excludedSubjectTypes map[string]bool) ([]PlatformModel, error) {
	accessUser, err := s.resolveCurrentAccessUser(ctx, user)
	if err != nil {
		return nil, err
	}
	models, _, err := s.enabledPlatformModels(ctx)
	if err != nil {
		return nil, err
	}
	if excludedSubjectTypes["api_key"] {
		return s.filterPlatformModelsByBaselineAccess(ctx, accessUser, models)
	}
	return s.filterPlatformModelsByLayeredAccess(ctx, accessUser, models)
}

func (s *Store) ensureAPIKeyAccessRuleResourcesAllowed(ctx context.Context, user *auth.User, apiKeyID string, resources []AccessRuleResourceInput) error {
	resources = dedupeAccessRuleResources(resources)
	if len(resources) == 0 {
		return nil
	}
	allowed, err := s.accessibleAccessRuleResources(ctx, user, apiKeyID)
	if err != nil {
		return err
	}
	for _, resource := range resources {
		if !allowed[resource.ResourceType+":"+resource.ResourceID] {
			return ErrAccessRuleResourceDenied
		}
	}
	return nil
}

func (s *Store) accessibleAccessRuleResources(ctx context.Context, user *auth.User, apiKeyID string) (map[string]bool, error) {
	models, _, err := s.ListAPIKeyAssignablePlatformModelsForKey(ctx, user, apiKeyID)
	if err != nil {
		return nil, err
	}
	allowed := map[string]bool{}
	for _, model := range models {
		allowed["platform:"+model.PlatformID] = true
		allowed["platform_model:"+model.ID] = true
		if model.BaseModelID != "" {
			allowed["base_model:"+model.BaseModelID] = true
		}
	}
	return allowed, nil
}

func (s *Store) resolveCurrentAccessUser(ctx context.Context, user *auth.User) (*auth.User, error) {
	if user == nil {
		return nil, nil
	}
	gatewayUserID := localGatewayUserID(user)
	if gatewayUserID == "" {
		return user, nil
	}
	next := *user
	var userGroupID string
	var userGroupKey string
	var err error
	if strings.TrimSpace(user.APIKeyID) != "" {
		err = s.pool.QueryRow(ctx, `
SELECT COALESCE(k.user_group_id::text, u.default_user_group_id::text, ''), COALESCE(g.group_key, '')
FROM gateway_users u
JOIN gateway_api_keys k ON k.gateway_user_id = u.id
LEFT JOIN gateway_user_groups g ON g.id = COALESCE(k.user_group_id, u.default_user_group_id)
WHERE u.id = $1::uuid
  AND k.id = $2::uuid
  AND u.status = 'active'
  AND u.deleted_at IS NULL
  AND k.status = 'active'
	  AND k.deleted_at IS NULL`, gatewayUserID, user.APIKeyID).Scan(&userGroupID, &userGroupKey)
	} else {
		err = s.pool.QueryRow(ctx, `
SELECT COALESCE(u.default_user_group_id::text, ''), COALESCE(g.group_key, '')
FROM gateway_users u
LEFT JOIN gateway_user_groups g ON g.id = u.default_user_group_id
WHERE u.id = $1::uuid
  AND u.status = 'active'
  AND u.deleted_at IS NULL`, gatewayUserID).Scan(&userGroupID, &userGroupKey)
	}
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return &next, nil
		}
		return nil, err
	}
	next.UserGroupID = userGroupID
	next.UserGroupKey = userGroupKey
	next.UserGroupKeys = nil
	if userGroupKey != "" {
		next.UserGroupKeys = []string{userGroupKey}
	}
	return &next, nil
}

func accessRuleSubjects(user *auth.User) map[string]bool {
	subjects := map[string]bool{}
	if user == nil {
		return subjects
	}
	add := func(subjectType string, id string) {
		id = strings.TrimSpace(id)
		if id != "" {
			subjects[subjectType+":"+id] = true
		}
	}
	add("user", firstNonEmpty(user.GatewayUserID, user.ID))
	add("tenant", firstNonEmpty(user.GatewayTenantID, user.TenantID))
	add("api_key", user.APIKeyID)
	add("user_group", user.UserGroupID)
	for _, groupKey := range user.UserGroupKeys {
		add("user_group", groupKey)
	}
	return subjects
}

const accessRuleColumns = `
id::text, subject_type, subject_id::text, resource_type, resource_id::text, effect,
priority, min_permission_level, conditions, metadata, status, created_at, updated_at`

const apiKeyAccessRuleColumns = `
ar.id::text, ar.subject_type, ar.subject_id::text, ar.resource_type, ar.resource_id::text, ar.effect,
ar.priority, ar.min_permission_level, ar.conditions, ar.metadata, ar.status, ar.created_at, ar.updated_at`

func scanAccessRule(row scanner) (AccessRule, error) {
	var item AccessRule
	var conditions []byte
	var metadata []byte
	if err := row.Scan(
		&item.ID,
		&item.SubjectType,
		&item.SubjectID,
		&item.ResourceType,
		&item.ResourceID,
		&item.Effect,
		&item.Priority,
		&item.MinPermissionLevel,
		&conditions,
		&metadata,
		&item.Status,
		&item.CreatedAt,
		&item.UpdatedAt,
	); err != nil {
		return AccessRule{}, err
	}
	item.Conditions = decodeObject(conditions)
	item.Metadata = decodeObject(metadata)
	return item, nil
}

func normalizeAccessRuleInput(input AccessRuleInput) AccessRuleInput {
	input.SubjectType = strings.TrimSpace(input.SubjectType)
	input.SubjectID = strings.TrimSpace(input.SubjectID)
	input.ResourceType = strings.TrimSpace(input.ResourceType)
	input.ResourceID = strings.TrimSpace(input.ResourceID)
	input.Effect = firstNonEmpty(strings.TrimSpace(input.Effect), "allow")
	input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
	if input.Priority == 0 {
		input.Priority = 100
	}
	if input.MinPermissionLevel < 0 {
		input.MinPermissionLevel = 0
	}
	return input
}

func normalizeAccessRuleBatchInput(input AccessRuleBatchInput) AccessRuleBatchInput {
	input.SubjectType = strings.TrimSpace(input.SubjectType)
	input.SubjectID = strings.TrimSpace(input.SubjectID)
	input.Effect = firstNonEmpty(strings.TrimSpace(input.Effect), "allow")
	return input
}

func normalizeAccessRuleResource(input AccessRuleResourceInput, effect string) AccessRuleResourceInput {
	input.ResourceType = strings.TrimSpace(input.ResourceType)
	input.ResourceID = strings.TrimSpace(input.ResourceID)
	input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
	if input.Priority == 0 {
		if effect == "deny" {
			input.Priority = 10
		} else {
			input.Priority = 100
		}
	}
	if input.MinPermissionLevel < 0 {
		input.MinPermissionLevel = 0
	}
	return input
}

func dedupeAccessRuleResources(resources []AccessRuleResourceInput) []AccessRuleResourceInput {
	seen := map[string]bool{}
	out := make([]AccessRuleResourceInput, 0, len(resources))
	for _, resource := range resources {
		resource.ResourceType = strings.TrimSpace(resource.ResourceType)
		resource.ResourceID = strings.TrimSpace(resource.ResourceID)
		key := resource.ResourceType + ":" + resource.ResourceID
		if resource.ResourceType == "" || resource.ResourceID == "" || seen[key] {
			continue
		}
		seen[key] = true
		out = append(out, resource)
	}
	return out
}
