import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:supabase_flutter/supabase_flutter.dart';
import '../config/supabase_config.dart';
import '../models/category.dart';
import '../models/profile.dart';
import '../models/profile_privacy_settings.dart';
import '../models/post.dart';
import '../models/comment.dart';
import '../models/tone_analysis.dart' hide ToneAnalysis;
import '../models/search_results.dart';
import '../models/notification.dart';
import '../models/beacon.dart';

/// API service for calling sojorn Edge Functions
class ApiService {
  final SupabaseClient _supabase;

  ApiService(this._supabase);

  /// Helper to make Edge Function calls
  /// Trust the Supabase SDK to handle auth, tokens, and headers automatically
  Future<Map<String, dynamic>> _callFunction(
    String functionName, {
    HttpMethod method = HttpMethod.post,
    Map<String, dynamic>? queryParams,
    Object? body,
  }) async {
    final response = await _supabase.functions.invoke(
      functionName,
      method: method,
      queryParameters: queryParams,
      body: body,
    );

    // Parse response data
    final data = response.data;
    if (data is Map<String, dynamic>) {
      return data;
    }
    if (data is Map) {
      return Map<String, dynamic>.from(data);
    }
    if (data is String && data.isNotEmpty) {
      final parsed = jsonDecode(data);
      if (parsed is Map<String, dynamic>) {
        return parsed;
      }
    }
    throw Exception('Unexpected response from $functionName');
  }


  List<Map<String, dynamic>> _normalizeListResponse(dynamic response) {
    try {
      // Supabase Flutter SDK returns data directly, not wrapped in PostgrestResponse
      if (response == null) return [];
      
      // Handle different response types
      List<dynamic> rawData;
      if (response is List<dynamic>) {
        rawData = response;
      } else if (response is Map<String, dynamic>) {
        // If it's a single object, wrap it in a list
        return [response];
      } else {
        return [];
      }

      return rawData
          .where((item) => item is Map<String, dynamic>)
          .map((item) => Map<String, dynamic>.from(item as Map))
          .toList();
    } catch (e) {
      print('_normalizeListResponse error: $e');
      return [];
    }
  }

  static const String _postSelect = '''
    id,
    body,
    author_id,
    category_id,
    tone_label,
    cis_score,
    status,
    created_at,
    edited_at,
    deleted_at,
    is_edited,
    allow_chain,
    chain_parent_id,
    image_url,
    chain_parent:posts (
      id,
      body,
      created_at,
      author:profiles!posts_author_id_fkey (
        id,
        handle,
        display_name
      )
    ),
    metrics:post_metrics!post_metrics_post_id_fkey (
      like_count,
      save_count,
      view_count
    ),
    author:profiles!posts_author_id_fkey (
      id,
      handle,
      display_name,
      trust_state (
        user_id,
        harmony_score,
        tier,
        posts_today
      )
    )
  ''';

  // ============================================================================
  // CATEGORIES
  // ============================================================================

  /// Fetch all categories
  Future<List<Category>> getCategories() async {
    final response = await _supabase
        .from('categories')
        .select()
        .order('name', ascending: true);
    return (response as List).map((json) => Category.fromJson(json)).toList();
  }

  /// Fetch categories the user has enabled
  Future<List<Category>> getEnabledCategories() async {
    final categories = await getCategories();
    final enabledIds = await _getEnabledCategoryIds();

    if (enabledIds.isEmpty) {
      return categories.where((category) => !category.defaultOff).toList();
    }

    return categories
        .where((category) => enabledIds.contains(category.id))
        .toList();
  }

  /// Check if the current user has a profile
  Future<bool> hasProfile() async {
    final user = _supabase.auth.currentUser;
    if (user == null) {
      return false;
    }

    try {
      final response = await _supabase
          .from('profiles')
          .select('id')
          .eq('id', user.id)
          .limit(1);

      return (response as List).isNotEmpty;
    } catch (e) {
      // If we get an error (like RLS blocking or JWT issues), assume no profile
      // This allows the signup flow to work even if RLS is strict
      print('hasProfile error (treating as false): $e');
      return false;
    }
  }

  /// Check if the user has completed category selection
  Future<bool> hasCategorySelection() async {
    try {
      final enabledIds = await _getEnabledCategoryIds();
      return enabledIds.isNotEmpty;
    } catch (e) {
      // If we get an error, assume no category selection
      print('hasCategorySelection error (treating as false): $e');
      return false;
    }
  }

  /// Persist user category settings for all categories
  Future<void> setUserCategorySettings({
    required List<Category> categories,
    required Set<String> enabledCategoryIds,
  }) async {
    final user = _supabase.auth.currentUser;
    if (user == null) {
      throw Exception('Not authenticated');
    }

    final payload = categories
        .map(
          (category) => {
            'user_id': user.id,
            'category_id': category.id,
            'enabled': enabledCategoryIds.contains(category.id),
          },
        )
        .toList();

    await _supabase.from('user_category_settings').upsert(payload);
  }

  Future<Set<String>> _getEnabledCategoryIds() async {
    try {
      final response = await _supabase
          .from('user_category_settings')
          .select('category_id')
          .eq('enabled', true);

      return (response as List)
          .map((row) => row['category_id'] as String)
          .toSet();
    } catch (e) {
      // If query fails, return empty set
      print('_getEnabledCategoryIds error: $e');
      return {};
    }
  }

  // ============================================================================
  // USER MANAGEMENT
  // ============================================================================

  /// Create profile (after Supabase Auth signup)
  Future<Profile> createProfile({
    required String handle,
    required String displayName,
    String? bio,
  }) async {
    final response = await _callFunction(
      'signup',
      method: 'POST',
      body: {
        'handle': handle,
        'display_name': displayName,
        if (bio != null) 'bio': bio,
      },
    );

    if (_isSuccessStatus(response)) {
      final data = jsonDecode(response.body);
      return Profile.fromJson(data['profile']);
    } else {
      throw Exception(_extractError(response, 'Failed to create profile'));
    }
  }

  /// Get profile by handle
  Future<Map<String, dynamic>> getProfile({String? handle}) async {
    final response = await _callFunction(
      'profile',
      queryParams: handle != null ? {'handle': handle} : null,
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return {
        'profile': Profile.fromJson(data['profile']),
        'stats': ProfileStats.fromJson(data['stats']),
        'is_following': data['is_following'] as bool? ?? false,
      };
    } else {
      throw Exception(_extractError(response, 'Failed to get profile'));
    }
  }

  /// Fetch posts authored by the given profile
  Future<List<Post>> getProfilePosts({
    required String authorId,
    int limit = 20,
    int offset = 0,
  }) async {
    final safeOffset = offset < 0 ? 0 : offset;
    final safeLimit = limit.clamp(1, 100).toInt();
    final rangeEnd = safeOffset + safeLimit - 1;

    try {
      final response = await _supabase
          .from('posts')
          .select('''
            id,
            body,
            author_id,
            category_id,
            tone_label,
            cis_score,
            status,
            created_at,
            edited_at,
            deleted_at,
            allow_chain,
            chain_parent_id,
            image_url,
            chain_parent:posts (
              id,
              body,
              created_at,
              author:profiles!posts_author_id_fkey (
                id,
                handle,
                display_name
              )
            ),
            metrics:post_metrics!post_metrics_post_id_fkey (
              like_count,
              save_count,
              view_count
            ),
            author:profiles!posts_author_id_fkey (
              id,
              handle,
              display_name,
              trust_state (
                user_id,
                harmony_score,
                tier,
                posts_today
              )
            )
          ''')
          .eq('author_id', authorId)
          .order('created_at', ascending: false)
          .range(safeOffset, rangeEnd);

      final rows = _normalizeListResponse(response);
      return rows.map(Post.fromJson).toList();
    } catch (error) {
      final message = error is PostgrestException
          ? error.message
          : 'Failed to load profile posts';
      print('Error fetching profile posts: $error');
      throw Exception('Failed to load profile posts: $message');
    }
  }

  /// Fetch a single post by id
  Future<Post> getPostById(String postId) async {
    try {
      final response = await _supabase
          .from('posts')
          .select(_postSelect)
          .eq('id', postId)
          .single();

      // Handle both Map and List response formats
      final Map<String, dynamic> data;
      if (response is Map<String, dynamic>) {
        data = response;
      } else if (response is List && response.isNotEmpty) {
        data = Map<String, dynamic>.from(response[0] as Map);
      } else {
        data = {};
      }

      return Post.fromJson(data);
    } catch (error) {
      final message = error is PostgrestException
          ? error.message
          : 'Failed to load post';
      throw Exception('Failed to load post: $message');
    }
  }

  /// Update own profile
  Future<Profile> updateProfile({
    String? handle,
    String? displayName,
    String? bio,
    String? location,
    String? website,
    List<String>? interests,
    String? avatarUrl,
    String? coverUrl,
  }) async {
    final response = await _callFunction(
      'profile',
      method: 'PATCH',
      body: {
        if (handle != null) 'handle': handle,
        if (displayName != null) 'display_name': displayName,
        if (bio != null) 'bio': bio,
        if (location != null) 'location': location,
        if (website != null) 'website': website,
        if (interests != null) 'interests': interests,
        if (avatarUrl != null) 'avatar_url': avatarUrl,
        if (coverUrl != null) 'cover_url': coverUrl,
      },
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return Profile.fromJson(data['profile']);
    } else {
      throw Exception(_extractError(response, 'Failed to update profile'));
    }
  }

  /// Fetch or initialize privacy settings for the current user
  Future<ProfilePrivacySettings> getPrivacySettings() async {
    final userId = _supabase.auth.currentUser?.id;
    if (userId == null) {
      throw Exception('Not authenticated');
    }

    final response = await _supabase
        .from('profile_privacy_settings')
        .select()
        .eq('user_id', userId)
        .limit(1);

    final rows = _normalizeListResponse(response);
    if (rows.isNotEmpty) {
      return ProfilePrivacySettings.fromJson(rows.first);
    }

    final now = DateTime.now().toIso8601String();
    final defaults = ProfilePrivacySettings.defaults(userId);
    final inserted = await _supabase
        .from('profile_privacy_settings')
        .insert({
          ...defaults.toJson(),
          'created_at': now,
          'updated_at': now,
        })
        .select()
        .single();

    return ProfilePrivacySettings.fromJson(inserted);
  }

  /// Update privacy settings for the current user
  Future<ProfilePrivacySettings> updatePrivacySettings(
    ProfilePrivacySettings settings,
  ) async {
    final userId = _supabase.auth.currentUser?.id;
    if (userId == null) {
      throw Exception('Not authenticated');
    }

    final payload = settings.toJson()
      ..['user_id'] = userId
      ..['updated_at'] = DateTime.now().toIso8601String();

    final response = await _supabase
        .from('profile_privacy_settings')
        .upsert(payload)
        .select()
        .single();

    return ProfilePrivacySettings.fromJson(response);
  }

  /// Fetch posts the user has appreciated
  Future<List<Post>> getAppreciatedPosts({
    required String userId,
    int limit = 20,
    int offset = 0,
  }) async {
    final safeLimit = limit.clamp(1, 100).toInt();
    final safeOffset = offset < 0 ? 0 : offset;
    final rangeEnd = safeOffset + safeLimit - 1;

    final response = await _supabase
        .from('post_likes')
        .select('post:posts ($_postSelect)')
        .eq('user_id', userId)
        .order('created_at', ascending: false)
        .range(safeOffset, rangeEnd);

    // Handle embedded response format
    final List<dynamic> rawData = response as List<dynamic>;
    return rawData
        .map((row) => row['post'] as Map<String, dynamic>?)
        .where((post) => post != null)
        .map((post) {
          final payload = Map<String, dynamic>.from(post!);
          payload['user_liked'] = true;
          return Post.fromJson(payload);
        })
        .toList();
  }

  /// Fetch posts the user has saved
  Future<List<Post>> getSavedPosts({
    required String userId,
    int limit = 20,
    int offset = 0,
  }) async {
    final safeLimit = limit.clamp(1, 100).toInt();
    final safeOffset = offset < 0 ? 0 : offset;
    final rangeEnd = safeOffset + safeLimit - 1;

    final response = await _supabase
        .from('post_saves')
        .select('post:posts ($_postSelect)')
        .eq('user_id', userId)
        .order('created_at', ascending: false)
        .range(safeOffset, rangeEnd);

    // Handle embedded response format
    final List<dynamic> rawData = response as List<dynamic>;
    return rawData
        .map((row) => row['post'] as Map<String, dynamic>?)
        .where((post) => post != null)
        .map((post) {
          final payload = Map<String, dynamic>.from(post!);
          payload['user_saved'] = true;
          return Post.fromJson(payload);
        })
        .toList();
  }

  /// Fetch posts by an author that have chain responses
  Future<List<Post>> getChainedPostsForAuthor({
    required String authorId,
    int limit = 20,
    int offset = 0,
  }) async {
    final safeLimit = limit.clamp(1, 100).toInt();
    final safeOffset = offset < 0 ? 0 : offset;
    final rangeEnd = safeOffset + safeLimit - 1;

    final response = await _supabase
        .from('posts')
        .select('$_postSelect, chain_children:posts!inner(id)')
        .eq('author_id', authorId)
        .order('created_at', ascending: false)
        .range(safeOffset, rangeEnd);

    final rows = _normalizeListResponse(response);
    return rows.map(Post.fromJson).toList();
  }

  /// Follow a user
  Future<void> followUser(String userId) async {
    final response = await _callFunction(
      'follow',
      method: 'POST',
      body: {'user_id': userId},
    );

    if (response.statusCode != 200) {
      throw Exception(
        jsonDecode(response.body)['error'] ?? 'Failed to follow user',
      );
    }
  }

  /// Unfollow a user
  Future<void> unfollowUser(String userId) async {
    final response = await _callFunction(
      'follow',
      method: 'DELETE',
      body: {'user_id': userId},
    );

    if (response.statusCode != 200) {
      throw Exception(
        jsonDecode(response.body)['error'] ?? 'Failed to unfollow user',
      );
    }
  }

  /// Block a user
  Future<void> blockUser(String userId) async {
    final response = await _callFunction(
      'block',
      method: 'POST',
      body: {'user_id': userId},
    );

    if (response.statusCode != 200) {
      throw Exception(
        jsonDecode(response.body)['error'] ?? 'Failed to block user',
      );
    }
  }

  /// Unblock a user
  Future<void> unblockUser(String userId) async {
    final response = await _callFunction(
      'block',
      method: 'DELETE',
      body: {'user_id': userId},
    );

    if (response.statusCode != 200) {
      throw Exception(
        jsonDecode(response.body)['error'] ?? 'Failed to unblock user',
      );
    }
  }

  // ============================================================================
  // CONTENT PUBLISHING
  // ============================================================================

  /// Publish a post
  ///
  /// Supports both regular posts and beacons through unified API.
  /// For beacons, set [isBeacon] to true and provide [beaconType], [lat], and [long].
  Future<Post> publishPost({
    required String categoryId,
    required String body,
    String bodyFormat = 'plain',
    bool allowChain = true,
    String? chainParentId,
    String? imageUrl,
    // Beacon-specific parameters
    bool isBeacon = false,
    BeaconType? beaconType,
    double? lat,
    double? long,
  }) async {
    final response = await _callFunction(
      'publish-post',
      method: 'POST',
      body: {
        'category_id': categoryId,
        'body': body,
        'body_format': bodyFormat,
        'allow_chain': allowChain,
        if (chainParentId != null) 'chain_parent_id': chainParentId,
        if (imageUrl != null) 'image_url': imageUrl,
        // Beacon fields
        if (isBeacon) 'is_beacon': true,
        if (beaconType != null) 'beacon_type': beaconType.value,
        if (lat != null) 'beacon_lat': lat,
        if (long != null) 'beacon_long': long,
      },
    );

    if (_isSuccessStatus(response)) {
      final data = jsonDecode(response.body);
      return Post.fromJson(data['post']);
    } else {
      throw Exception(_extractError(response, 'Failed to publish post'));
    }
  }

  /// Publish a comment (mutual-follow only)
  Future<Comment> publishComment({
    required String postId,
    required String body,
  }) async {
    final response = await _callFunction(
      'publish-comment',
      method: 'POST',
      body: {'post_id': postId, 'body': body},
    );

    if (_isSuccessStatus(response)) {
      final data = jsonDecode(response.body);
      return Comment.fromJson(data['comment']);
    } else {
      throw Exception(_extractError(response, 'Failed to publish comment'));
    }
  }

  // ============================================================================
  // TONE ANALYSIS
  // ============================================================================

  /// Analyze the tone of content using AI moderation
  ///
  /// Uses intent-based detection to allow authentic expression while
  /// rejecting slurs and hostile attacks.
  ///
  /// [text] The text to analyze
  /// [imageUrl] Optional image URL for multi-modal analysis
  ///
  /// Returns a [ToneCheckResult]. If the analysis fails (API error,
  /// timeout, etc.), returns a default "neutral" result to allow the post.
  Future<ToneCheckResult> checkTone(
    String text, {
    String? imageUrl,
  }) async {
    try {
      final response = await _supabase.functions.invoke(
        'tone-check',
        method: HttpMethod.post,
        body: {
          'text': text,
          if (imageUrl != null) 'imageUrl': imageUrl,
        },
      );

      final data = response.data;
      if (data is Map<String, dynamic>) {
        return ToneCheckResult.fromJson(data);
      }
      if (data is Map) {
        return ToneCheckResult.fromJson(Map<String, dynamic>.from(data));
      }
      if (data is String && data.isNotEmpty) {
        final parsed = jsonDecode(data);
        if (parsed is Map<String, dynamic>) {
          return ToneCheckResult.fromJson(parsed);
        }
      }

      // Fallback to neutral if response format is invalid
      return ToneCheckResult(
        tone: ToneCategory.neutral,
        cis: 0.8,
        flags: [],
        reason: 'Analysis complete',
      );
    } catch (e) {
      print('Tone analysis error: $e');
      // FALLBACK: Allow post with neutral rating when AI is unavailable
      // This prevents blocking users when OpenAI has issues
      return ToneCheckResult(
        tone: ToneCategory.neutral,
        cis: 0.8,
        flags: [],
        reason: 'Content approved (moderation temporarily unavailable)',
      );
    }
  }

  // ============================================================================
  // ENGAGEMENT
  // ============================================================================

  /// Appreciate a post
  Future<void> appreciatePost(String postId) async {
    final response = await _callFunction(
      'appreciate',
      method: 'POST',
      body: {'post_id': postId},
    );

    if (response.statusCode != 200) {
      throw Exception(_extractError(response, 'Failed to appreciate post'));
    }
  }

  /// Remove appreciation
  Future<void> unappreciatePost(String postId) async {
    final response = await _callFunction(
      'appreciate',
      method: 'DELETE',
      body: {'post_id': postId},
    );

    if (response.statusCode != 200) {
      throw Exception(_extractError(response, 'Failed to remove appreciation'));
    }
  }

  /// Save a post
  Future<void> savePost(String postId) async {
    final response = await _callFunction(
      'save',
      method: 'POST',
      body: {'post_id': postId},
    );

    if (response.statusCode != 200) {
      throw Exception(_extractError(response, 'Failed to save post'));
    }
  }

  /// Unsave a post
  Future<void> unsavePost(String postId) async {
    final response = await _callFunction(
      'save',
      method: 'DELETE',
      body: {'post_id': postId},
    );

    if (response.statusCode != 200) {
      throw Exception(_extractError(response, 'Failed to unsave post'));
    }
  }

  /// Report content or user
  Future<void> reportContent({
    required String targetType,
    required String targetId,
    required String reason,
  }) async {
    final response = await _callFunction(
      'report',
      method: 'POST',
      body: {
        'target_type': targetType,
        'target_id': targetId,
        'reason': reason,
      },
    );

    if (!_isSuccessStatus(response)) {
      throw Exception(_extractError(response, 'Failed to submit report'));
    }
  }

  // ============================================================================
  // FEEDS
  // ============================================================================

  /// Get personal feed (chronological from follows)
  Future<List<Post>> getPersonalFeed({int limit = 50, int offset = 0}) async {
    final response = await _callFunction(
      'feed-personal',
      method: 'GET',
      queryParams: {'limit': limit.toString(), 'offset': offset.toString()},
    );

    if (_isSuccessStatus(response)) {
      final data = jsonDecode(response.body);
      return (data['posts'] as List)
          .map((json) => Post.fromJson(json))
          .toList();
    } else {
      throw Exception(_extractError(response, 'Failed to get personal feed'));
    }
  }

  /// Get sojorn feed (algorithmic FYP)
  Future<List<Post>> getsojornFeed({int limit = 50, int offset = 0}) async {
    final response = await _callFunction(
      'feed-sojorn',
      method: 'GET',
      queryParams: {'limit': limit.toString(), 'offset': offset.toString()},
    );

    if (_isSuccessStatus(response)) {
      final data = jsonDecode(response.body);
      return (data['posts'] as List)
          .map((json) => Post.fromJson(json))
          .toList();
    } else {
      throw Exception(_extractError(response, 'Failed to get sojorn feed'));
    }
  }

  /// Get trending posts by category
  Future<List<Post>> getTrending({
    String category = 'general',
    int limit = 20,
  }) async {
    final response = await _callFunction(
      'trending',
      queryParams: {'category': category, 'limit': limit.toString()},
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return (data['posts'] as List)
          .map((json) => Post.fromJson(json))
          .toList();
    } else {
      throw Exception(_extractError(response, 'Failed to get trending'));
    }
  }

  // ============================================================================
  // POST MANAGEMENT (EDIT/DELETE)
  // ============================================================================

  /// Edit a post (within 2-minute window)
  /// Requires passing moderation check
  Future<void> editPost({
    required String postId,
    required String content,
  }) async {
    final response = await _callFunction(
      'manage-post',
      method: 'POST',
      body: {
        'action': 'edit',
        'post_id': postId,
        'content': content,
      },
    );

    if (!_isSuccessStatus(response)) {
      throw Exception(_extractError(response, 'Failed to edit post'));
    }
  }

  /// Soft delete a post
  Future<void> deletePost(String postId) async {
    final response = await _callFunction(
      'manage-post',
      method: 'POST',
      body: {
        'action': 'delete',
        'post_id': postId,
      },
    );

    if (!_isSuccessStatus(response)) {
      throw Exception(_extractError(response, 'Failed to delete post'));
    }
  }

  // ============================================================================
  // CHAIN RESPONSES
  // ============================================================================

  /// Fetch chain responses for a post (chronological)
  Future<List<Post>> getChainPosts({
    required String parentPostId,
    int limit = 50,
  }) async {
    final safeLimit = limit.clamp(1, 100).toInt();

    final response = await _supabase
        .from('posts')
        .select('''
          id,
          body,
          author_id,
          category_id,
          tone_label,
          cis_score,
          status,
          created_at,
          edited_at,
          deleted_at,
          allow_chain,
          chain_parent_id,
          image_url,
          chain_parent:posts (
            id,
            body,
            created_at,
            author:profiles!posts_author_id_fkey (
              id,
              handle,
              display_name
            )
          ),
          metrics:post_metrics!post_metrics_post_id_fkey (
            like_count,
            save_count,
            view_count
          ),
          author:profiles!posts_author_id_fkey (
            id,
            handle,
            display_name,
            trust_state (
              user_id,
              harmony_score,
              tier,
              posts_today
            )
          )
        ''')
        .eq('chain_parent_id', parentPostId)
        .order('created_at', ascending: true)
        .limit(safeLimit);

    final rows = _normalizeListResponse(response);
    return rows.map(Post.fromJson).toList();
  }

  /// Deactivate user account
  Future<void> deactivateAccount() async {
    final response = await _callFunction(
      'deactivate-account',
      method: 'POST',
    );

    if (response.statusCode != 200) {
      throw Exception(_extractError(response, 'Failed to deactivate account'));
    }
  }

  /// Reactivate a deactivated account
  Future<void> reactivateAccount() async {
    final response = await _callFunction(
      'deactivate-account/reactivate',
      method: 'POST',
    );

    if (response.statusCode != 200) {
      throw Exception(_extractError(response, 'Failed to reactivate account'));
    }
  }

  /// Request permanent account deletion (30-day waiting period)
  Future<Map<String, dynamic>> requestAccountDeletion() async {
    final response = await _callFunction(
      'delete-account',
      method: 'POST',
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return data;
    } else {
      throw Exception(_extractError(response, 'Failed to request account deletion'));
    }
  }

  /// Cancel pending account deletion request
  Future<void> cancelAccountDeletion() async {
    final response = await _callFunction(
      'delete-account/cancel',
      method: 'POST',
    );

    if (response.statusCode != 200) {
      throw Exception(_extractError(response, 'Failed to cancel account deletion'));
    }
  }

  // ============================================================================
  // SEARCH
  // ============================================================================

  /// Search for users and hashtags simultaneously
  /// Uses the database RPC function for efficient searching
  Future<SearchResults> search(String query) async {
    if (query.trim().isEmpty) {
      return SearchResults(users: [], tags: [], posts: []);
    }

    try {
      final response = await _supabase
          .rpc('search_sojorn', params: {'p_query': query.trim()});

      if (response is Map<String, dynamic>) {
        return SearchResults.fromJson(response);
      } else if (response is String && response.isNotEmpty) {
        final parsed = jsonDecode(response);
        if (parsed is Map<String, dynamic>) {
          return SearchResults.fromJson(parsed);
        }
      }

      // Fallback for empty results
      return SearchResults(users: [], tags: [], posts: []);
    } catch (e) {
      print('Search error: $e');
      // Return empty results on error
      return SearchResults(users: [], tags: [], posts: []);
    }
  }

  // ============================================================================
  // NOTIFICATIONS
  // ============================================================================

  /// Get notifications for the current user
  /// [limit] Maximum number of notifications to fetch (default: 20)
  /// [offset] Number of notifications to skip for pagination (default: 0)
  /// [unreadOnly] If true, only fetch unread notifications (default: false)
  Future<List<AppNotification>> getNotifications({
    int limit = 20,
    int offset = 0,
    bool unreadOnly = false,
  }) async {
    final response = await _callFunction(
      'notifications',
      method: 'GET',
      queryParams: {
        'limit': limit.toString(),
        'offset': offset.toString(),
        if (unreadOnly) 'unread_only': 'true',
      },
    );

    if (_isSuccessStatus(response)) {
      final data = jsonDecode(response.body);
      if (data is List) {
        return data.map((json) => AppNotification.fromJson(json)).toList();
      }
      return [];
    } else {
      throw Exception(_extractError(response, 'Failed to get notifications'));
    }
  }

  /// Mark specific notifications as read
  /// [notificationIds] List of notification IDs to mark as read
  Future<void> markNotificationsAsRead(List<String> notificationIds) async {
    final response = await _callFunction(
      'notifications',
      method: 'PATCH',
      body: {'notification_ids': notificationIds},
    );

    if (!_isSuccessStatus(response)) {
      throw Exception(_extractError(response, 'Failed to mark notifications as read'));
    }
  }

  /// Mark all notifications as read
  Future<void> markAllNotificationsAsRead() async {
    final response = await _callFunction(
      'notifications',
      method: 'PATCH',
      body: {'mark_all_read': true},
    );

    if (!_isSuccessStatus(response)) {
      throw Exception(_extractError(response, 'Failed to mark all notifications as read'));
    }
  }

  /// Get the count of unread notifications
  Future<int> getUnreadNotificationCount() async {
    final userId = _supabase.auth.currentUser?.id;
    if (userId == null) {
      return 0;
    }

    try {
      final response = await _supabase
          .rpc('get_unread_notification_count', params: {'p_user_id': userId});

      if (response is int) {
        return response;
      }
      return 0;
    } catch (e) {
      print('Error getting unread notification count: $e');
      return 0;
    }
  }
}
