import 'package:flutter/material.dart';

import 'package:collection/collection.dart';

import 'package:pshared/models/permissions/bound/storable.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/template.dart';
import 'package:pshared/utils/exception.dart';


List<T> mergeLists<T>({
  required List<T> lhs,
  required List<T> rhs,
  required Comparable Function(T) getKey, // Extracts ID dynamically
  required int Function(T, T) compare,
  required T Function(T, T) merge,
}) {
  final result = <T>[];
  final map = {for (var item in lhs) getKey(item): item};

  for (var updated in rhs) {
    final key = getKey(updated);
    map[key] = merge(map[key] ?? updated, updated);
  }

  result.addAll(map.values);
  result.sort(compare);
  return result;
}

/// A generic provider that wraps a [BasicService] instance
/// to manage state (loading, error, data) without re‑implementing service logic.
class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier {
  final BasicService<T> service;
  bool _isLoaded = false;

  Resource<List<T>> _resource = Resource(data: []);
  Resource<List<T>> get resource => _resource;

  List<T> get items => List.unmodifiable(_resource.data ?? []);
  bool get isLoading => _resource.isLoading;
  Object? get error => _resource.error;
  bool get isReady => (error == null) && _isLoaded;

  bool get isCurrentSet => _currentObjectRef != null;
  String? _currentObjectRef; // Stores the currently selected project ref
  T? get currentObject => _resource.data?.firstWhereOrNull(
    (object) => object.id == _currentObjectRef,
  );

  T? getItemByRef(String id) => items.firstWhereOrNull((item) => item.id == id);

  GenericProvider({required this.service});


  bool Function(T)? _filterPredicate;

  List<T> get filteredItems => _filterPredicate != null ? items.where(_filterPredicate!).toList() : items;

  void setFilterPredicate(bool Function(T)? predicate) {
    _filterPredicate = predicate;
    notifyListeners();
  }

  void clearFilter() => setFilterPredicate(null);

  void _setResource(Resource<List<T>> newResource) {
    _resource = newResource;
    notifyListeners();
  }

  Future<List<T>> loadFuture(Future<List<T>> future) async {
    _setResource(_resource.copyWith(isLoading: true));
    try {
      final list = await future;
      _isLoaded = true;
      _setResource(Resource(data: list, isLoading: false));
      return list;
    } catch (e) {
      _setResource(
        _resource.copyWith(isLoading: false, error: toException(e)),
      );
      rethrow;
    }
  }

  Future<void> load(
    String organizationRef, 
    String? parentRef, {
    int? limit,
    int? offset,
    bool? Function()? fetchArchived,
  }) async {
    if (parentRef != null) {
      await loadFuture(
        service.list(
          organizationRef, 
          parentRef, 
          limit: limit, 
          offset: offset, 
          fetchArchived: fetchArchived == null ? null : fetchArchived(),
        ),
      );
    }
  }

  Future<void> loadItem(String itemRef) async {
    await loadFuture((() async => [await service.get(itemRef)])());
  }

  List<T> merge(List<T> rhs) => mergeLists<T>(
    lhs: items,
    rhs: rhs,
    getKey: (item) => item.id,  // Key extractor
    compare: (a, b) => a.id.compareTo(b.id), // Sorting logic
    merge: (existing, updated) => updated, // Replace with the updated version
  );

  Future<T> get(String objectRef) async {
    _setResource(_resource.copyWith(isLoading: true));
    try {
      final item = await service.get(objectRef);
      _setResource(Resource(data: merge([item]), isLoading: false));
      return item;
    } catch (e) {
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<T> createObject(String organizationRef, Map<String, dynamic> request) async {
    _setResource(_resource.copyWith(isLoading: true));
    try {
      final newObject = await service.create(organizationRef, request);
      _setResource(Resource(data: [...items, ...newObject], isLoading: false));
      return newObject.first;
    } catch (e) {
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<void> update(Map<String, dynamic> request) async {
    _setResource(_resource.copyWith(isLoading: true));
    try {
      final list = await service.update(request);
      _setResource(Resource(data: merge(list), isLoading: false));
    } catch (e) {
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<void> delete(String objectRef, {Map<String, dynamic>? request}) async {
    _setResource(_resource.copyWith(isLoading: true));

    try {
      await service.delete(objectRef, request: request);
      if (_currentObjectRef == objectRef) {
        _currentObjectRef = null;
      }

      _setResource(Resource(
        data: _resource.data?.where((p) => p.id != objectRef).toList(),
        isLoading: false,
      ));
    } catch (e) {
      _setResource(Resource(data: _resource.data, isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<void> toggleArchived(T item, bool currentState, {bool? cascade}) => setArchived(
    organizationRef: item.organizationRef, 
    objectRef: item.id, 
    newIsArchived: !currentState,
    cascade: cascade ?? true,
  );

  Future<void> setArchived({
    required String organizationRef, 
    required String objectRef, 
    required bool newIsArchived, 
    bool? cascade,
  }) async {
    _setResource(_resource.copyWith(isLoading: true));

    try {
      await service.archive(
        organizationRef: organizationRef,
        objectRef: objectRef, 
        newIsArchived: newIsArchived,
        cascade: cascade,
      );
      if (_currentObjectRef == objectRef) {
        _currentObjectRef = null;
      }

      _setResource(Resource(
        data: _resource.data?.where((p) => p.id != objectRef).toList(),
        isLoading: false,
      ));
    } catch (e) {
      _setResource(Resource(data: _resource.data, isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  bool setCurrentObject(String? objectRef) {
    if (_currentObjectRef == objectRef) {
      // No change, skip notification
      return true;
    }

    if (objectRef == null) {
      _currentObjectRef = null;
      notifyListeners();
      return true;
    }

    if (_resource.data?.any((p) => p.id == objectRef) ?? false) {
      _currentObjectRef = objectRef;
      notifyListeners();
      return true;
    }

    return false; // Object not found
  }

}
