generate method

Future<void> generate()

Implementation

Future<void> generate() async {
  final providersDir = p.join(outputDirectory);
  final fileName = 'feed_provider.dart';
  final filePath = p.join(providersDir, fileName);

  final buffer = StringBuffer();

  buffer.writeln('');
  buffer.writeln(r"""
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: constant_identifier_names

import 'dart:convert';

import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sqlite_async/sqlite3_common.dart';
import 'package:sqlite_async/sqlite_async.dart'; // Ensure SqliteDatabase is imported
import 'package:tether_libs/client_manager/client_manager.dart';
import 'package:tether_libs/client_manager/manager/client_manager_base.dart'; // Import TetherClientReturn
import 'package:tether_libs/client_manager/manager/client_manager_filter_builder.dart';
import 'package:tether_libs/client_manager/manager/client_manager_models.dart';
import 'package:tether_libs/client_manager/manager/client_manager_transform_builder.dart';
import 'package:tether_libs/models/supabase_select_builder_base.dart';
import 'package:tether_libs/models/tether_model.dart';
import 'package:tether_libs/utils/logger.dart';

import '../database.dart';
import '../managers/feed_item_reference_manager.g.dart';

/// Creates a `Row` instance from a standard `Map<String, dynamic>`.
///
/// This is useful for creating mock data or converting nested JSON objects
/// into a `Row` that model factories can consume.
Row _createRowFromMap(Map<String, dynamic> map) {
final columnNames = map.keys.toList();
final values = map.values.toList();

final mockResultSet = ResultSet(
  columnNames,
  null, // tableNames are not needed for this conversion
  [values], // A list containing our single row of values
);

// The ResultSet is a list of Rows. The Row we want is the first one.
return mockResultSet[0];
}

typedef QueryBuilderFactory<TModel extends TetherModel<TModel>>
  = ClientManagerFilterBuilder<TModel> Function();

/// A class that wraps the feed data with additional state information
class FeedStreamState<TModel extends TetherModel<TModel>> {
final List<TModel> data;
final String? error;
final int? count;
final bool isLoading;

const FeedStreamState({
  required this.data,
  this.error,
  this.count,
  this.isLoading = false,
});

bool get hasMore {
  if (data.isEmpty) return true; // If no data, assume more items

  if (count == null) return true; // If count is unknown, assume more items
  return data.length < count!;
}

bool get hasError => error != null && error!.isNotEmpty;

FeedStreamState<TModel> copyWith({
  List<TModel>? data,
  String? error,
  int? count,
  bool? isLoading,
}) {
  return FeedStreamState<TModel>(
    data: data ?? this.data,
    error: error ?? this.error,
    count: count ?? this.count,
    isLoading: isLoading ?? this.isLoading,
  );
}

@override
String toString() {
  return 'FeedStreamState(data: ${data.length} items, error: $error, count: $count, isLoading: $isLoading)';
}
}

class FeedStreamNotifierSettings<TModel extends TetherModel<TModel>>
  extends Equatable {
final String feedKey;
final TetherColumn? searchColumn;
final String? searchRpcName; // <-- Add this line
final int pageSize;
final ClientManager<TModel> clientManager;
final SelectBuilderBase selectArgs;
final TModel Function(Row row) fromJsonFactory;
final ClientManagerFilterBuilder<TModel> Function(
  ClientManagerFilterBuilder<TModel> baseQuery,
  Ref ref,
)? queryCustomizer;
final TetherColumn? defaultOrderColumn;
final bool defaultOrderAscending;
final bool defaultOrderNullsFirst;
final bool fetchOnBuild;
final TetherColumn? primaryKeyColumn;

const FeedStreamNotifierSettings({
  required this.feedKey,
  this.searchColumn,
  this.searchRpcName, // <-- Add this line
  required this.clientManager,
  required this.selectArgs,
  required this.fromJsonFactory,
  this.pageSize = 20,
  this.queryCustomizer,
  this.defaultOrderColumn,
  this.defaultOrderAscending = true,
  this.defaultOrderNullsFirst = false,
  this.fetchOnBuild = true,
  this.primaryKeyColumn,
});

@override
List<Object?> get props => [
      feedKey,
      searchColumn,
      searchRpcName, // <-- Add this line
      pageSize,
      clientManager,
      selectArgs,
      fromJsonFactory,
      queryCustomizer,
      defaultOrderColumn,
      defaultOrderAscending,
      defaultOrderNullsFirst,
      fetchOnBuild,
      primaryKeyColumn,
    ];
}

class FeedStreamNotifier<TModel extends TetherModel<TModel>>
  extends StreamNotifier<FeedStreamState<TModel>> {
late FeedStreamNotifierSettings<TModel> _currentSettings;

final FeedStreamNotifierSettings<TModel> arg;

int currentPage = 0;
String terms = '';
bool _isDisposed = false;
final Logger _logger = Logger('SearchStreamNotifier');

// Track the total count from remote
int? _totalRemoteCount;
// Track loading state
bool _isLoading = false;
// Track error state
String? _error;

// Add this to track the current search operation
int _currentSearchId = 0;

// New state for excluded IDs
List<String>? _excludedIds;

// For dynamic filters applied on top of queryCustomizer
ClientManagerFilterBuilder<TModel> Function(
  ClientManagerFilterBuilder<TModel> query,
)? _dynamicFilterApplicator;

// New ordering state variables
TetherColumn? _orderColumn;
bool _orderAscending = true;
bool _orderNullsFirst = false;

FeedStreamNotifier(this.arg);

ClientManagerTransformBuilder<TModel> _getEffectiveQueryBuilder() {
  // 1. Start with the absolute base query from settings.
  ClientManagerFilterBuilder<TModel> baseQuery =
      _currentSettings.clientManager.query.select(
    _currentSettings.selectArgs,
  );

  // 2. Apply the "base query modifications" (static customizer) from settings
  if (_currentSettings.queryCustomizer != null) {
    baseQuery = _currentSettings.queryCustomizer!(
      baseQuery,
      ref,
    );
  }

  // Start the final query from the customized base query
  ClientManagerFilterBuilder<TModel> filterQuery = baseQuery;

  // 3. Apply dynamic filters if any are set
  if (_dynamicFilterApplicator != null) {
    filterQuery = _dynamicFilterApplicator!(filterQuery);
  }

  // 3.5 Apply excluded IDs if any are set
  if (_excludedIds != null &&
      _excludedIds!.isNotEmpty &&
      _currentSettings.primaryKeyColumn != null) {
    filterQuery = filterQuery.not(
        _currentSettings.primaryKeyColumn!, 'in', _excludedIds!);
  }

  // 4. Apply search terms if search is configured and terms are present
  if (_currentSettings.searchColumn != null && terms.isNotEmpty) {
    // textSearch is available on ClientManagerFilterBuilder
    filterQuery =
        filterQuery.textSearch(_currentSettings.searchColumn!, terms);
  }

  // 5. Apply ordering if set. The result is a TransformBuilder, so we assign it to a new variable.
  ClientManagerTransformBuilder<TModel> transformQuery = filterQuery;

  // When a search is active, always prioritize ordering by relevance.
  if (_currentSettings.searchColumn != null && terms.isNotEmpty) {
    transformQuery = transformQuery.order(
      _currentSettings.searchColumn!,
      ascending: false,
    );
  } else if (_orderColumn != null) {
    transformQuery = transformQuery.order(_orderColumn!,
        ascending: _orderAscending, nullsFirst: _orderNullsFirst);
  }

  return transformQuery;
}

// Helper to get the query structure for feedStream's JSON sub-select
ClientManagerFilterBuilder<TModel> _getBaseQueryForFeedStreamSchema() {
  var baseQuery = _currentSettings.clientManager.query.select(
    _currentSettings.selectArgs,
  );
  if (_currentSettings.queryCustomizer != null) {
    baseQuery = _currentSettings.queryCustomizer!(
      baseQuery,
      ref,
    );
  }
  return baseQuery;
}

/// Calculate how many items we currently have in the feed
int get _currentItemCount => currentPage * _currentSettings.pageSize;

/// Check if there are more items available to fetch
bool get _hasMoreItems {
  if (_totalRemoteCount == null) {
    return true; // Unknown count, assume more available
  }
  return _currentItemCount < _totalRemoteCount!;
}

@override
Stream<FeedStreamState<TModel>> build() async* {
  _currentSettings = arg;
  currentPage = 0;
  terms = '';
  _dynamicFilterApplicator = null;
  _totalRemoteCount = null;
  _isDisposed = false;
  _isLoading = false;
  _error = null;
  _excludedIds = null; // Initialize new state

  // Initialize ordering from settings
  _orderColumn = arg.defaultOrderColumn;
  _orderAscending = arg.defaultOrderAscending;
  _orderNullsFirst = arg.defaultOrderNullsFirst;

  ref.onDispose(() {
    _isDisposed = true;
  });

  // Yield initial loading state
  yield FeedStreamState<TModel>(
    data: <TModel>[],
    error: null,
    count: null,
    isLoading: true,
  );

  try {
    final feedStream = _buildFeedStreamState();

    final count = await _getCount();
    if (_isDisposed) return;

    // Refresh if count is 0 or fetchOnBuild is true
    if (arg.fetchOnBuild && count == 0) {
      _logger.info(
        'FeedStreamNotifier: Initial count is 0 for feedKey: ${_currentSettings.feedKey}. Refreshing feed.',
      );
      await refreshFeed(initialize: true);
    } else if (arg.fetchOnBuild) {
      _logger.info(
        'FeedStreamNotifier: Initial count is > 0 for feedKey: ${_currentSettings.feedKey}. Refreshing feed.',
      );
      await refreshFeed(initialize: true);
    } else if (count == 0) {
      _logger.info(
        'FeedStreamNotifier: Initial count is 0 for feedKey: ${_currentSettings.feedKey}. Refreshing feed.',
      );
      await refreshFeed(initialize: true);
    }

    yield* feedStream;
  } catch (e) {
    if (!_isDisposed) {
      _isLoading = false;
      _error = e.toString();
      yield FeedStreamState<TModel>(
        data: <TModel>[],
        error: _error,
        count: _totalRemoteCount,
        isLoading: false,
      );
    }
  }
}

Stream<FeedStreamState<TModel>> _buildFeedStreamState() async* {
  try {
    final appDatabase = ref.read(databaseProvider);
    final sqliteDb = appDatabase.db as SqliteDatabase;

    // Use the base query (with static customizer) for determining table name and selector for SQL
    final baseQueryForSchema = _getBaseQueryForFeedStreamSchema();
    final modelTableName = baseQueryForSchema.tableName;
    final SelectBuilderBase? selector = baseQueryForSchema.selectorStatement;

    if (selector == null) {
      _logger.severe(
        "SearchStreamNotifier: selectorStatement is null in feedStream for ${modelTableName}. This indicates a setup error with base query.",
      );
      yield FeedStreamState<TModel>(
        data: <TModel>[],
        error: "Setup error: selectorStatement is null",
        count: _totalRemoteCount,
        isLoading: false,
      );
      return;
    }

    final SqlStatement jsonSelectSubQueryStatement =
        selector.buildSelectWithNestedData();

    if (selector.currentTableInfo.primaryKeys.isEmpty) {
      _logger.severe(
        "SearchStreamNotifier: Table ${selector.currentTableInfo.originalName} has no primary keys defined. Cannot build feedStream SQL.",
      );
      yield FeedStreamState<TModel>(
        data: <TModel>[],
        error: "Setup error: No primary keys defined",
        count: _totalRemoteCount,
        isLoading: false,
      );
      return;
    }
    final String modelPrimaryKeyCol =
        selector.currentTableInfo.primaryKeys.first.originalName;
    final String aliasInSubQuery =
        jsonSelectSubQueryStatement.fromAlias ?? 't_fallback';

    // Note: The SQL query for the stream is maintaining the feed_item_references display_order,
    // not applying the custom ordering. This is because the feed structure maintains its own order.
    // The custom ordering affects how items are fetched from the server and inserted into the feed.
    final sql = '''
    SELECT
      fir.id AS feed_item_ref_id,
      fir.feed_key,
      fir.item_source_table,
      fir.item_source_id,
      fir.display_order,
      CASE
          WHEN fir.item_source_table = ? THEN (
              SELECT ${jsonSelectSubQueryStatement.selectColumns}
              FROM "${jsonSelectSubQueryStatement.tableName}" AS "$aliasInSubQuery"
              WHERE "$aliasInSubQuery"."$modelPrimaryKeyCol" = fir.item_source_id
          )
          ELSE NULL
      END AS item_json_data
    FROM
      feed_item_references fir
    WHERE
      fir.feed_key = ?
    ORDER BY
      fir.display_order ASC;
  ''';

    // _logger.info(
    //   sql,
    // );

    yield* sqliteDb.watch(
      sql,
      parameters: [
        modelTableName,
        _currentSettings.feedKey,
      ],
      // throttle: const Duration(milliseconds: 100),
      triggerOnTables: [modelTableName, 'feed_item_references'],
    ).map<FeedStreamState<TModel>>((ResultSet rawData) {
      // Process the raw data into a list of models
      if (_isDisposed) {
        return FeedStreamState<TModel>(
          data: <TModel>[],
          error: _error,
          count: _totalRemoteCount,
          isLoading: _isLoading,
        );
      }

      _logger.info(
        'SearchStreamNotifier: Emitted ${rawData.length} rows for feedKey: ${_currentSettings.feedKey}',
      );

      final List<TModel> models = rawData
          .map((row) {
            final jsonData = row['item_json_data'];

            if (jsonData == null || jsonData is String && jsonData.isEmpty) {
              _logger.warning(
                'SearchStreamNotifier: item_json_data is null or empty for row: $row',
              );
              return null; // Skip this row
            }

            Map<String, dynamic> parsedJson;
            if (jsonData is String) {
              try {
                parsedJson = jsonDecode(jsonData) as Map<String, dynamic>;
              } catch (e, s) {
                _logger.warning(
                  'SearchStreamNotifier: JSONDecode error for item_json_data: $e, data: $jsonData $s',
                );
                return null;
              }
            } else if (jsonData is Map<String, dynamic>) {
              parsedJson = jsonData;
            } else {
              _logger.warning(
                'SearchStreamNotifier: Unexpected format for item_json_data: ${jsonData.runtimeType}',
              );
              return null;
            }
            try {
              final newRow = _createRowFromMap(parsedJson);
              return _currentSettings.fromJsonFactory(newRow);
            } catch (e, s) {
              _logger.severe(
                'SearchStreamNotifier: Error in fromJsonFactory for $modelTableName: $e, json: $parsedJson StackTrace: $s',
              );
              return null;
            }
          })
          .whereType<TModel>()
          .toList();

      _logger.info(
        'SearchStreamNotifier: Processed ${models.length} valid models for feedKey: ${_currentSettings.feedKey}',
      );

      return FeedStreamState<TModel>(
        data: models,
        error: _error,
        count: _totalRemoteCount,
        isLoading: _isLoading,
      );
    });
  } catch (e, s) {
    _logger.severe(
      'SearchStreamNotifier: Error in _buildFeedStreamState: $e StackTrace: $s',
    );
    yield FeedStreamState<TModel>(
      data: <TModel>[],
      error: e.toString(),
      count: _totalRemoteCount,
      isLoading: false,
    );
  }
}

Future<int> _getCount() async {
  final feedManager = ref.read(feedItemReferenceManagerProvider);
  return await feedManager.getCount(feedKey: _currentSettings.feedKey);
}

Future<void> refreshFeed({bool initialize = false}) async {
  // REMOVED this block to allow new searches to interrupt old ones.
  // if (_isLoading) {
  //   _logger.info('Already refreshing, skipping.');
  //   return;
  // }

  // Increment and capture the search ID for this specific refresh operation.
  final searchId = ++_currentSearchId;

  try {
    _isLoading = true;
    if (state.value != null) {
      state = AsyncData(
        state.value!.copyWith(isLoading: true, error: null),
      );
    }

    final feedManager = ref.read(feedItemReferenceManagerProvider);

    if (initialize) {
      await feedManager.clearFeed(feedKey: _currentSettings.feedKey);
      currentPage = 0;
    }

    final TetherClientReturn<TModel> result = await _fetch(
      rangeStart: 0,
      rangeEnd: _currentSettings.pageSize - 1,
    );

    // Before applying the result, check if this is still the latest search.
    // If not, another search has been initiated, so we discard this result.
    if (_isDisposed || searchId != _currentSearchId) {
      _logger.info(
        'Search result for ID $searchId is stale, discarding. Current ID is $_currentSearchId.',
      );
      // Even if stale, we might need to turn off the loading indicator
      // if this stale operation was the one that turned it on and no new one has.
      // However, the finally block handles this better.
      return;
    }

    final newItems = result.data;
    _totalRemoteCount = result.count;

    if (newItems.isNotEmpty) {
      final feedItems = newItems.map((item) {
        return FeedItemReference(
          itemSourceTable: _currentSettings.clientManager.localTableName,
          itemSourceId: item.localId.toString(),
          displayOrder: 0,
        );
      }).toList();

      await feedManager.setFeedItems(
        feedKey: _currentSettings.feedKey,
        items: feedItems,
      );
      currentPage = 0;
    }
    // No need for an `else if (initialize)` block to clear the feed,
    // as the clearing is now handled at the beginning of the method.
  } catch (e, s) {
    // Also check for staleness in the catch block
    if (_isDisposed || searchId != _currentSearchId) {
      _logger.info(
        'Error for stale search ID $searchId, ignoring. Current ID is $_currentSearchId.',
      );
      return;
    }
    _logger.severe('Error refreshing feed $e $s');
    _error = e.toString();
    state = AsyncData(state.value!.copyWith(error: _error));
  } finally {
    // Only update loading state if this was the last operation.
    if (searchId == _currentSearchId) {
      _isLoading = false;
      if (state.value != null) {
        state = AsyncData(state.value!.copyWith(isLoading: false));
      }
    } else {
      // If this is a stale operation, we don't change the loading state
      // as a newer operation is in control.
      _logger.info(
        'Stale refresh (ID $searchId) finished, not changing loading state as current ID is $_currentSearchId.',
      );
    }
  }
}

Future<void> fetchMoreItems() async {
  debugPrint(
      'SearchStreamNotifier: fetchMoreItems called for feedKey: ${_currentSettings.feedKey}');
  if (_isLoading) {
    _logger.info('Already fetching more items, skipping.');
    return;
  }
  if (!_hasMoreItems) {
    _logger.info('No more items to fetch.');
    return;
  }

  final nextPage = currentPage + 1;
  try {
    _isLoading = true;
    // Visually indicate that we are loading more items.
    state = AsyncData(state.value!.copyWith(isLoading: true));

    final TetherClientReturn<TModel> result = await _fetch(
      rangeStart: nextPage * _currentSettings.pageSize,
      rangeEnd: (nextPage + 1) * _currentSettings.pageSize - 1,
    );

    if (result.error != null) {
      debugPrint(
          'SearchStreamNotifier: Error fetching more items for feedKey: ${_currentSettings.feedKey}, Error: ${result.error}');
    }

    debugPrint(
        'SearchStreamNotifier: Fetched ${result.data.length} more items for feedKey: ${_currentSettings.feedKey}, is Disposed: $_isDisposed');

    if (_isDisposed) return;

    final newItems = result.data;
    _totalRemoteCount = result.count;

    if (newItems.isNotEmpty) {
      // The missing step: add the new items to the end of the feed references.
      final feedManager = ref.read(feedItemReferenceManagerProvider);
      final newReferences = newItems
          .map((item) => FeedItemReference(
                itemSourceTable:
                    _currentSettings.clientManager.localTableName,
                itemSourceId: item.localId.toString(),
                displayOrder: 0, // This is ignored by addItemsToEnd
              ))
          .toList();

      await feedManager.addItemsToEnd(
        feedKey: _currentSettings.feedKey,
        itemsToAdd: newReferences,
      );
      currentPage = nextPage;
    }
    // The stream will now automatically pick up the changes.
  } catch (e, s) {
    _logger.severe('Error fetching more items $e, $s');
    if (!_isDisposed) {
      state = AsyncData(state.value!.copyWith(error: e.toString()));
    }
  } finally {
    _isLoading = false;
    // Ensure the final state reflects that loading is complete.
    if (!_isDisposed && state.value?.isLoading == true) {
      state = AsyncData(state.value!.copyWith(isLoading: false));
    }
  }
}

Future<TetherClientReturn<TModel>> _fetch({
  int rangeStart = 0,
  int rangeEnd = 20,
}) async {
  // FTS Hybrid Query Logic
  // MODIFICATION: Changed `terms.isNotEmpty` to `_currentSettings.searchRpcName != null`
  // This ensures that if an RPC is specified, we ALWAYS use it, even for empty search terms.
  // The backend RPC is responsible for handling the empty string case.
  if (_currentSettings.searchRpcName != null) {
    if (_currentSettings.primaryKeyColumn == null) {
      final errorMsg =
          "SearchStreamNotifier: primaryKeyColumn must be set when using searchRpcName for feedKey '${_currentSettings.feedKey}'.";
      _logger.severe(errorMsg);
      throw Exception(errorMsg);
    }
    final pageSize = rangeEnd - rangeStart + 1;
    final offset = rangeStart;

    // Step 1: Call the RPC to get ranked IDs, rank, and total count.
    final rpcResponse = await _currentSettings.clientManager.client
        .rpc(_currentSettings.searchRpcName!, params: {
      'search_query': terms,
      'result_limit': pageSize,
      'result_offset': offset,
    });

    _logger.info('${_currentSettings.feedKey} RPC Response: $rpcResponse');

    final List<dynamic> rpcData = rpcResponse as List<dynamic>;

    _logger.info('${_currentSettings.feedKey} RPC Data: $rpcData');

    if (rpcData.isEmpty) {
      return TetherClientReturn(data: [], count: 0);
    }

    // Extract IDs for the next query and ranks for sorting.
    final idToRankMap = {for (var e in rpcData) e['id']: e['rank'] as num};
    final ids = rpcData.map((e) => e['id']).toList();

    // The total count should be returned by the RPC on each item.
    final totalCount = (rpcData.first['total_count'] as int?) ?? 0;

    // Step 2: Fetch the full data for these IDs using a standard query.
    // Apply the query customizer first.
    var query = _currentSettings.clientManager.query
        .select(_currentSettings.selectArgs);
    if (_currentSettings.queryCustomizer != null) {
      query = _currentSettings.queryCustomizer!(query, ref);
    }

    // Then, apply the filter for the IDs returned by the RPC.
    query = query.inFilter(_currentSettings.primaryKeyColumn!, ids);
    final dataResponse = await query;

    // THIS IS THE FIX:
    // The `await query` returns before the background database writes are
    // guaranteed to be complete. We need to wait for the write queue to
    // finish before we can be sure the data is in the local DB.
    final appDatabase = ref.read(databaseProvider);
    await (appDatabase.db as SqliteDatabase).get('SELECT 1');

    debugPrint(
        '${_currentSettings.feedKey} Data response: ${dataResponse.count} items');

    // Step 3: Re-sort the fetched data according to the rank from the RPC.
    final sortedData = dataResponse.data
      ..sort((a, b) {
        final rankA = idToRankMap[a.localId] ?? 0;
        final rankB = idToRankMap[b.localId] ?? 0;
        // Higher rank is better, so sort descending.
        return rankB.compareTo(rankA);
      });

    return TetherClientReturn(data: sortedData, count: totalCount);
  } else {
    // Existing Logic for non-FTS queries or empty search terms
    final effectiveQuery = _getEffectiveQueryBuilder();
    final result =
        await effectiveQuery.range(rangeStart, rangeEnd).remoteOnly();

    // APPLY THE SAME FIX HERE for non-RPC fetches
    final appDatabase = ref.read(databaseProvider);
    await (appDatabase.db as SqliteDatabase).get('SELECT 1');

    return result;
  }
}

void search(String newTerms, {List<String>? excludeIds}) {
  if (_isDisposed) return;
  _logger.info('Searching for: $newTerms');

  // If the new search term is the same as the current one, do nothing.
  if (terms == newTerms && listEquals(_excludedIds, excludeIds)) {
    _logger.info('Search terms are the same, skipping.');
    return;
  }

  // Invalidate the previous search operation by incrementing the ID.
  // Any ongoing refreshFeed will now be ignored when it completes.
  _currentSearchId++;

  // Reset pagination and update terms
  currentPage = 0;
  terms = newTerms;
  _excludedIds = excludeIds; // Store the excluded IDs

  // If the search term is empty, we should still refresh to clear results
  // and show the base unfiltered feed.
  if (newTerms.isEmpty) {
    _logger.info(
        'Search term is empty, refreshing feed to show unfiltered results.');
    refreshFeed(initialize: true);
    return;
  }

  // If a search RPC name is provided, use it for a remote search.
  if (_currentSettings.searchRpcName != null) {
    _logger.info('Using search RPC: ${_currentSettings.searchRpcName}');
    refreshFeed(initialize: true);
    return;
  }

  // Fallback to column-based search if no RPC is specified.
  if (_currentSettings.searchColumn != null) {
    _logger.info(
        'Using column-based search on: ${_currentSettings.searchColumn}');
    // Apply a dynamic filter for the local search.
    applyDynamicFilters((query) {
      // Use 'ilike' for case-insensitive partial matching.
      return query.ilike(_currentSettings.searchColumn!, '%$newTerms%');
    });
  } else {
    _logger.info('No search column or RPC specified, cannot perform search.');
  }
}

/// Sets a list of IDs to exclude from the feed results.
Future<void> setExcludedIds(List<String>? newIds) async {
  _logger.info(
    "Setting excluded IDs for feedKey '${_currentSettings.feedKey}'.",
  );
  _excludedIds = newIds;
  currentPage = 0;
  _totalRemoteCount = null; // Reset count when filters change
  _error = null; // Clear any previous errors
  await refreshFeed(initialize: true);
}

/// Applies a new set of dynamic filters to the feed.
/// The [filterApplicator] function takes the current base query (after static customizers)
/// and should return a new query with the dynamic filters applied.
Future<void> applyDynamicFilters(
  ClientManagerFilterBuilder<TModel> Function(
    ClientManagerFilterBuilder<TModel> baseQueryWithoutDynamicFilters,
  ) filterApplicator,
) async {
  _logger.info(
    "Applying dynamic filters for feedKey '${_currentSettings.feedKey}'.",
  );
  _dynamicFilterApplicator = filterApplicator;
  currentPage = 0;
  _totalRemoteCount = null; // Reset count when filters change
  _error = null; // Clear any previous errors
  // It's usually best to re-initialize the feed when filters change significantly.
  // Also, consider if search terms should be cleared or maintained.
  // For now, maintaining search terms.
  await refreshFeed(initialize: true);
}

/// Clears any previously applied dynamic filters, reverting to the base query
/// (which includes the static queryCustomizer from settings).
Future<void> clearDynamicFilters() async {
  if (_dynamicFilterApplicator != null) {
    _logger.info(
      "Clearing dynamic filters for feedKey '${_currentSettings.feedKey}'.",
    );
    _dynamicFilterApplicator = null;
    currentPage = 0;
    _totalRemoteCount = null; // Reset count when filters change
    _error = null; // Clear any previous errors
    // Also consider if search terms should be cleared.
    await refreshFeed(initialize: true);
  } else {
    _logger.info(
      "No dynamic filters to clear for feedKey '${_currentSettings.feedKey}'.",
    );
  }
}

/// Apply a new ordering to the feed items.
///
/// This will fetch items from the remote source in the specified order
/// and rebuild the feed with those items.
///
/// [column] - The column to order by
/// [ascending] - Whether to order in ascending (true) or descending (false) order
/// [nullsFirst] - Whether NULL values should be first (true) or last (false) when ordering
Future<void> applyOrdering({
  required TetherColumn column,
  bool ascending = true,
  bool nullsFirst = false,
}) async {
  _logger.info(
    "Applying ordering for feedKey '${_currentSettings.feedKey}': ${column.dartName} ${ascending ? 'ASC' : 'DESC'}, nullsFirst: ${nullsFirst}",
  );
  _orderColumn = column;
  _orderAscending = ascending;
  _orderNullsFirst = nullsFirst;
  currentPage = 0;
  _totalRemoteCount = null; // Reset count when ordering changes
  _error = null; // Clear any previous errors
  await refreshFeed(initialize: true);
}

/// Clears the custom ordering, reverting to the default ordering (if any)
Future<void> clearOrdering() async {
  _logger.info(
    "Clearing ordering for feedKey '${_currentSettings.feedKey}'.",
  );
  _orderColumn = _currentSettings.defaultOrderColumn;
  _orderAscending = _currentSettings.defaultOrderAscending;
  _orderNullsFirst = _currentSettings.defaultOrderNullsFirst;
  currentPage = 0;
  _totalRemoteCount = null; // Reset count when clearing ordering
  _error = null; // Clear any previous errors
  await refreshFeed(initialize: true);
}

/// Reorders the items in the feed locally based on a new list of item IDs.
/// This is intended to be called after a successful remote reordering operation.
Future<void> reorderFeed(List<String> orderedItemIds) async {
  if (_isDisposed) return;
  _logger.info(
    "Reordering feed locally for feedKey '${_currentSettings.feedKey}'.",
  );

  try {
    final feedManager = ref.read(feedItemReferenceManagerProvider);
    await feedManager.reorderFeedItems(
      feedKey: _currentSettings.feedKey,
      orderedSourceIds: orderedItemIds,
    );
    // The stream watching 'feed_item_references' will automatically pick up
    // the changes and rebuild the UI with the new order.
  } catch (e, s) {
    _logger.severe(
      "Error during local reorder for feedKey '${_currentSettings.feedKey}': $e, $s",
    );
    // Optionally, handle the error in the UI
    _error = 'Failed to update local order: $e';
    state = AsyncData(state.value!.copyWith(error: _error));
  }
}

/// Getter to expose the current total count for UI components
int? get totalRemoteCount => _totalRemoteCount;

/// Getter to expose whether more items are available for UI components
bool get hasMoreItems => _hasMoreItems;

/// Getter to expose the current loading state
bool get isLoading => _isLoading;

/// Getter to expose the current error state
String? get error => _error;
}




""");
  buffer.writeln('');

  try {
    final file = File(filePath);
    await file.parent.create(recursive: true);
    await file.writeAsString(buffer.toString());
    _logger.info('Generated feed provider: $filePath');
  } catch (e) {
    _logger.severe('Error writing feed provider file $filePath: $e');
  }
}