package db

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"
)

type CreateAPIKeyParams struct {
	Name              string
	KeyPrefix         string
	KeyHash           string
	Description       string
	ScopeMode         ScopeMode
	CanDownload       bool
	CanUpload         bool
	CanDelete         bool
	CanManageProjects bool
	IsActive          bool
	ExpiresAt         *time.Time
	CreatedByUserID   *int64
}

type UpdateAPIKeyParams struct {
	Name              string
	Description       string
	ScopeMode         ScopeMode
	CanDownload       bool
	CanUpload         bool
	CanDelete         bool
	CanManageProjects bool
	ExpiresAt         *time.Time
}

func (r *APIKeyRepository) List(ctx context.Context) ([]APIKeyListItem, error) {
	rows, err := r.q.QueryContext(
		ctx,
		`SELECT
			ak.id,
			ak.name,
			ak.key_prefix,
			ak.key_hash,
			ak.description,
			ak.scope_mode,
			ak.can_download,
			ak.can_upload,
			ak.can_delete,
			ak.can_manage_projects,
			ak.is_active,
			ak.expires_at,
			ak.created_at,
			ak.updated_at,
			ak.last_used_at,
			ak.created_by_user_id,
			COUNT(DISTINCT ap.project_id) AS project_rule_count,
			COUNT(DISTINCT at.tag_id) AS tag_rule_count
		FROM api_keys AS ak
		LEFT JOIN api_key_project_access AS ap ON ap.api_key_id = ak.id
		LEFT JOIN api_key_tag_access AS at ON at.api_key_id = ak.id
		GROUP BY ak.id
		ORDER BY ak.is_active DESC, ak.updated_at DESC, ak.created_at DESC, ak.name COLLATE NOCASE`,
	)
	if err != nil {
		return nil, fmt.Errorf("query api keys: %w", err)
	}
	defer rows.Close()

	items := make([]APIKeyListItem, 0)
	for rows.Next() {
		item, err := scanAPIKeyListItem(rows)
		if err != nil {
			return nil, fmt.Errorf("scan api key list item: %w", err)
		}

		items = append(items, *item)
	}

	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate api keys: %w", err)
	}

	return items, nil
}

func (r *APIKeyRepository) GetByID(ctx context.Context, id int64) (*APIKey, error) {
	key, err := scanAPIKey(r.q.QueryRowContext(
		ctx,
		`SELECT
			id,
			name,
			key_prefix,
			key_hash,
			description,
			scope_mode,
			can_download,
			can_upload,
			can_delete,
			can_manage_projects,
			is_active,
			expires_at,
			created_at,
			updated_at,
			last_used_at,
			created_by_user_id
		FROM api_keys
		WHERE id = ?`,
		id,
	))
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}

		return nil, fmt.Errorf("scan api key by id: %w", err)
	}

	return key, nil
}

func (r *APIKeyRepository) GetByHash(ctx context.Context, keyHash string) (*APIKey, error) {
	key, err := scanAPIKey(r.q.QueryRowContext(
		ctx,
		`SELECT
			id,
			name,
			key_prefix,
			key_hash,
			description,
			scope_mode,
			can_download,
			can_upload,
			can_delete,
			can_manage_projects,
			is_active,
			expires_at,
			created_at,
			updated_at,
			last_used_at,
			created_by_user_id
		FROM api_keys
		WHERE key_hash = ?
		LIMIT 1`,
		strings.TrimSpace(keyHash),
	))
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}

		return nil, fmt.Errorf("scan api key by hash: %w", err)
	}

	return key, nil
}

func (r *APIKeyRepository) Create(ctx context.Context, params CreateAPIKeyParams) (*APIKey, error) {
	isActive := 0
	if params.IsActive {
		isActive = 1
	}

	result, err := r.q.ExecContext(
		ctx,
		`INSERT INTO api_keys (
			name,
			key_prefix,
			key_hash,
			description,
			scope_mode,
			can_download,
			can_upload,
			can_delete,
			can_manage_projects,
			is_active,
			expires_at,
			created_by_user_id
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		strings.TrimSpace(params.Name),
		strings.TrimSpace(params.KeyPrefix),
		strings.TrimSpace(params.KeyHash),
		strings.TrimSpace(params.Description),
		params.ScopeMode,
		boolToInt(params.CanDownload),
		boolToInt(params.CanUpload),
		boolToInt(params.CanDelete),
		boolToInt(params.CanManageProjects),
		isActive,
		nullableTimestampValue(params.ExpiresAt),
		params.CreatedByUserID,
	)
	if err != nil {
		if isUniqueConstraintError(err) {
			return nil, errors.Join(ErrConflict, fmt.Errorf("insert api key: %w", err))
		}

		return nil, fmt.Errorf("insert api key: %w", err)
	}

	apiKeyID, err := result.LastInsertId()
	if err != nil {
		return nil, fmt.Errorf("load inserted api key id: %w", err)
	}

	return r.GetByID(ctx, apiKeyID)
}

func (r *APIKeyRepository) Update(ctx context.Context, apiKeyID int64, params UpdateAPIKeyParams) (*APIKey, error) {
	result, err := r.q.ExecContext(
		ctx,
		`UPDATE api_keys
		SET name = ?,
			description = ?,
			scope_mode = ?,
			can_download = ?,
			can_upload = ?,
			can_delete = ?,
			can_manage_projects = ?,
			expires_at = ?
		WHERE id = ?`,
		strings.TrimSpace(params.Name),
		strings.TrimSpace(params.Description),
		params.ScopeMode,
		boolToInt(params.CanDownload),
		boolToInt(params.CanUpload),
		boolToInt(params.CanDelete),
		boolToInt(params.CanManageProjects),
		nullableTimestampValue(params.ExpiresAt),
		apiKeyID,
	)
	if err != nil {
		return nil, fmt.Errorf("update api key: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return nil, fmt.Errorf("read updated api key rows: %w", err)
	}

	if rowsAffected == 0 {
		return nil, ErrNotFound
	}

	return r.GetByID(ctx, apiKeyID)
}

func (r *APIKeyRepository) SetActive(ctx context.Context, apiKeyID int64, isActive bool) error {
	result, err := r.q.ExecContext(
		ctx,
		`UPDATE api_keys SET is_active = ? WHERE id = ?`,
		boolToInt(isActive),
		apiKeyID,
	)
	if err != nil {
		return fmt.Errorf("update api key active state: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("read updated api key rows: %w", err)
	}

	if rowsAffected == 0 {
		return ErrNotFound
	}

	return nil
}

func (r *APIKeyRepository) TouchLastUsedAt(ctx context.Context, apiKeyID int64, usedAt time.Time) error {
	result, err := r.q.ExecContext(
		ctx,
		`UPDATE api_keys SET last_used_at = ? WHERE id = ?`,
		formatTimestamp(usedAt),
		apiKeyID,
	)
	if err != nil {
		return fmt.Errorf("update api key last_used_at: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("read updated api key rows: %w", err)
	}

	if rowsAffected == 0 {
		return ErrNotFound
	}

	return nil
}

func (r *APIKeyRepository) ListProjectAccess(ctx context.Context, apiKeyID int64) ([]Project, error) {
	rows, err := r.q.QueryContext(
		ctx,
		`SELECT
			p.id,
			p.name,
			p.slug,
			p.description,
			p.is_active,
			p.created_at,
			p.updated_at
		FROM projects AS p
		INNER JOIN api_key_project_access AS ap ON ap.project_id = p.id
		WHERE ap.api_key_id = ?
		ORDER BY p.name COLLATE NOCASE`,
		apiKeyID,
	)
	if err != nil {
		return nil, fmt.Errorf("query api key project access: %w", err)
	}
	defer rows.Close()

	projects := make([]Project, 0)
	for rows.Next() {
		project, err := scanProject(rows)
		if err != nil {
			return nil, fmt.Errorf("scan api key project access: %w", err)
		}

		projects = append(projects, *project)
	}

	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate api key project access: %w", err)
	}

	return projects, nil
}

func (r *APIKeyRepository) ListTagAccess(ctx context.Context, apiKeyID int64) ([]Tag, error) {
	rows, err := r.q.QueryContext(
		ctx,
		`SELECT
			t.id,
			t.name,
			t.slug,
			t.description,
			t.created_at,
			t.updated_at
		FROM tags AS t
		INNER JOIN api_key_tag_access AS at ON at.tag_id = t.id
		WHERE at.api_key_id = ?
		ORDER BY t.name COLLATE NOCASE`,
		apiKeyID,
	)
	if err != nil {
		return nil, fmt.Errorf("query api key tag access: %w", err)
	}
	defer rows.Close()

	tags := make([]Tag, 0)
	for rows.Next() {
		tag, err := scanTag(rows)
		if err != nil {
			return nil, fmt.Errorf("scan api key tag access: %w", err)
		}

		tags = append(tags, *tag)
	}

	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate api key tag access: %w", err)
	}

	return tags, nil
}

func (r *APIKeyRepository) ReplaceProjectAccess(ctx context.Context, apiKeyID int64, projectIDs []int64) error {
	if err := r.ClearProjectAccess(ctx, apiKeyID); err != nil {
		return err
	}

	for _, projectID := range normalizeIDList(projectIDs) {
		if _, err := r.q.ExecContext(
			ctx,
			`INSERT OR IGNORE INTO api_key_project_access (api_key_id, project_id) VALUES (?, ?)`,
			apiKeyID,
			projectID,
		); err != nil {
			return fmt.Errorf("insert api key project access: %w", err)
		}
	}

	return nil
}

func (r *APIKeyRepository) ReplaceTagAccess(ctx context.Context, apiKeyID int64, tagIDs []int64) error {
	if err := r.ClearTagAccess(ctx, apiKeyID); err != nil {
		return err
	}

	for _, tagID := range normalizeIDList(tagIDs) {
		if _, err := r.q.ExecContext(
			ctx,
			`INSERT OR IGNORE INTO api_key_tag_access (api_key_id, tag_id) VALUES (?, ?)`,
			apiKeyID,
			tagID,
		); err != nil {
			return fmt.Errorf("insert api key tag access: %w", err)
		}
	}

	return nil
}

func (r *APIKeyRepository) ClearProjectAccess(ctx context.Context, apiKeyID int64) error {
	if _, err := r.q.ExecContext(ctx, `DELETE FROM api_key_project_access WHERE api_key_id = ?`, apiKeyID); err != nil {
		return fmt.Errorf("delete api key project access: %w", err)
	}

	return nil
}

func (r *APIKeyRepository) ClearTagAccess(ctx context.Context, apiKeyID int64) error {
	if _, err := r.q.ExecContext(ctx, `DELETE FROM api_key_tag_access WHERE api_key_id = ?`, apiKeyID); err != nil {
		return fmt.Errorf("delete api key tag access: %w", err)
	}

	return nil
}

func (r *APIKeyRepository) ListAccessibleProjects(ctx context.Context, apiKeyID int64, scopeMode ScopeMode) ([]Project, error) {
	rows, err := r.q.QueryContext(
		ctx,
		`SELECT
			p.id,
			p.name,
			p.slug,
			p.description,
			p.is_active,
			p.created_at,
			p.updated_at
		FROM projects AS p
		WHERE p.is_active = 1 AND (
			? = 'all_projects'
			OR (
				? = 'project_allow_list'
				AND EXISTS (
					SELECT 1
					FROM api_key_project_access AS ap
					WHERE ap.api_key_id = ?
						AND ap.project_id = p.id
				)
			)
			OR (
				? = 'project_deny_list'
				AND NOT EXISTS (
					SELECT 1
					FROM api_key_project_access AS ap
					WHERE ap.api_key_id = ?
						AND ap.project_id = p.id
				)
			)
			OR (
				? = 'tag_allow_list'
				AND EXISTS (
					SELECT 1
					FROM project_tags AS pt
					INNER JOIN api_key_tag_access AS at ON at.tag_id = pt.tag_id
					WHERE at.api_key_id = ?
						AND pt.project_id = p.id
				)
			)
			OR (
				? = 'tag_deny_list'
				AND NOT EXISTS (
					SELECT 1
					FROM project_tags AS pt
					INNER JOIN api_key_tag_access AS at ON at.tag_id = pt.tag_id
					WHERE at.api_key_id = ?
						AND pt.project_id = p.id
				)
			)
		)
		ORDER BY p.name COLLATE NOCASE`,
		scopeMode,
		scopeMode, apiKeyID,
		scopeMode, apiKeyID,
		scopeMode, apiKeyID,
		scopeMode, apiKeyID,
	)
	if err != nil {
		return nil, fmt.Errorf("query accessible projects: %w", err)
	}
	defer rows.Close()

	projects := make([]Project, 0)
	for rows.Next() {
		project, err := scanProject(rows)
		if err != nil {
			return nil, fmt.Errorf("scan accessible project: %w", err)
		}

		projects = append(projects, *project)
	}

	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate accessible projects: %w", err)
	}

	return projects, nil
}

func (r *APIKeyRepository) HasProjectAccess(ctx context.Context, apiKeyID int64, scopeMode ScopeMode, projectID int64) (bool, error) {
	var exists int
	if err := r.q.QueryRowContext(
		ctx,
		`SELECT EXISTS(
			SELECT 1
			FROM projects AS p
			WHERE p.id = ?
				AND p.is_active = 1
				AND (
					? = 'all_projects'
					OR (
						? = 'project_allow_list'
						AND EXISTS (
							SELECT 1
							FROM api_key_project_access AS ap
							WHERE ap.api_key_id = ?
								AND ap.project_id = p.id
						)
					)
					OR (
						? = 'project_deny_list'
						AND NOT EXISTS (
							SELECT 1
							FROM api_key_project_access AS ap
							WHERE ap.api_key_id = ?
								AND ap.project_id = p.id
						)
					)
					OR (
						? = 'tag_allow_list'
						AND EXISTS (
							SELECT 1
							FROM project_tags AS pt
							INNER JOIN api_key_tag_access AS at ON at.tag_id = pt.tag_id
							WHERE at.api_key_id = ?
								AND pt.project_id = p.id
						)
					)
					OR (
						? = 'tag_deny_list'
						AND NOT EXISTS (
							SELECT 1
							FROM project_tags AS pt
							INNER JOIN api_key_tag_access AS at ON at.tag_id = pt.tag_id
							WHERE at.api_key_id = ?
								AND pt.project_id = p.id
						)
					)
				)
		)`,
		projectID,
		scopeMode,
		scopeMode, apiKeyID,
		scopeMode, apiKeyID,
		scopeMode, apiKeyID,
		scopeMode, apiKeyID,
	).Scan(&exists); err != nil {
		return false, fmt.Errorf("query api key project access: %w", err)
	}

	return exists == 1, nil
}

func scanAPIKey(scanner rowScanner) (*APIKey, error) {
	var (
		key                APIKey
		scopeMode          string
		canDownload        int
		canUpload          int
		canDelete          int
		canManageProjects  int
		isActive           int
		expiresAtRaw       sql.NullString
		createdAtRaw       string
		updatedAtRaw       string
		lastUsedAtRaw      sql.NullString
		createdByUserIDRaw sql.NullInt64
	)

	if err := scanner.Scan(
		&key.ID,
		&key.Name,
		&key.KeyPrefix,
		&key.KeyHash,
		&key.Description,
		&scopeMode,
		&canDownload,
		&canUpload,
		&canDelete,
		&canManageProjects,
		&isActive,
		&expiresAtRaw,
		&createdAtRaw,
		&updatedAtRaw,
		&lastUsedAtRaw,
		&createdByUserIDRaw,
	); err != nil {
		return nil, err
	}

	createdAt, err := parseTimestamp(createdAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse api key created_at: %w", err)
	}

	updatedAt, err := parseTimestamp(updatedAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse api key updated_at: %w", err)
	}

	expiresAt, err := parseNullableTimestamp(expiresAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse api key expires_at: %w", err)
	}

	lastUsedAt, err := parseNullableTimestamp(lastUsedAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse api key last_used_at: %w", err)
	}

	key.ScopeMode = ScopeMode(scopeMode)
	key.CanDownload = canDownload == 1
	key.CanUpload = canUpload == 1
	key.CanDelete = canDelete == 1
	key.CanManageProjects = canManageProjects == 1
	key.IsActive = isActive == 1
	key.ExpiresAt = expiresAt
	key.CreatedAt = createdAt
	key.UpdatedAt = updatedAt
	key.LastUsedAt = lastUsedAt
	if createdByUserIDRaw.Valid {
		key.CreatedByUserID = &createdByUserIDRaw.Int64
	}

	return &key, nil
}

func scanAPIKeyListItem(scanner rowScanner) (*APIKeyListItem, error) {
	var (
		item               APIKeyListItem
		scopeMode          string
		canDownload        int
		canUpload          int
		canDelete          int
		canManageProjects  int
		isActive           int
		expiresAtRaw       sql.NullString
		createdAtRaw       string
		updatedAtRaw       string
		lastUsedAtRaw      sql.NullString
		createdByUserIDRaw sql.NullInt64
	)

	if err := scanner.Scan(
		&item.APIKey.ID,
		&item.APIKey.Name,
		&item.APIKey.KeyPrefix,
		&item.APIKey.KeyHash,
		&item.APIKey.Description,
		&scopeMode,
		&canDownload,
		&canUpload,
		&canDelete,
		&canManageProjects,
		&isActive,
		&expiresAtRaw,
		&createdAtRaw,
		&updatedAtRaw,
		&lastUsedAtRaw,
		&createdByUserIDRaw,
		&item.ProjectRuleCount,
		&item.TagRuleCount,
	); err != nil {
		return nil, err
	}

	createdAt, err := parseTimestamp(createdAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse api key list created_at: %w", err)
	}

	updatedAt, err := parseTimestamp(updatedAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse api key list updated_at: %w", err)
	}

	expiresAt, err := parseNullableTimestamp(expiresAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse api key list expires_at: %w", err)
	}

	lastUsedAt, err := parseNullableTimestamp(lastUsedAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse api key list last_used_at: %w", err)
	}

	item.APIKey.ScopeMode = ScopeMode(scopeMode)
	item.APIKey.CanDownload = canDownload == 1
	item.APIKey.CanUpload = canUpload == 1
	item.APIKey.CanDelete = canDelete == 1
	item.APIKey.CanManageProjects = canManageProjects == 1
	item.APIKey.IsActive = isActive == 1
	item.APIKey.ExpiresAt = expiresAt
	item.APIKey.CreatedAt = createdAt
	item.APIKey.UpdatedAt = updatedAt
	item.APIKey.LastUsedAt = lastUsedAt
	if createdByUserIDRaw.Valid {
		item.APIKey.CreatedByUserID = &createdByUserIDRaw.Int64
	}

	return &item, nil
}

func boolToInt(value bool) int {
	if value {
		return 1
	}

	return 0
}

func nullableTimestampValue(value *time.Time) any {
	if value == nil {
		return nil
	}

	return formatTimestamp(value.UTC())
}

func normalizeIDList(values []int64) []int64 {
	seen := make(map[int64]struct{}, len(values))
	normalized := make([]int64, 0, len(values))

	for _, value := range values {
		if value <= 0 {
			continue
		}

		if _, exists := seen[value]; exists {
			continue
		}

		seen[value] = struct{}{}
		normalized = append(normalized, value)
	}

	sort.Slice(normalized, func(i, j int) bool {
		return normalized[i] < normalized[j]
	})

	return normalized
}
