import 'dart:async';

import 'package:flutter/material.dart';

import 'package:collection/collection.dart';

import 'package:pshared/api/requests/change_role.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/permissions/access.dart';
import 'package:pshared/models/permissions/action.dart' as perm;
import 'package:pshared/models/permissions/data/permission.dart';
import 'package:pshared/models/permissions/data/policy.dart';
import 'package:pshared/models/permissions/data/role.dart';
import 'package:pshared/models/permissions/descriptions/policy.dart';
import 'package:pshared/models/permissions/descriptions/role.dart';
import 'package:pshared/models/permissions/effect.dart';
import 'package:pshared/models/resources.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/permissions.dart';
import 'package:pshared/utils/exception.dart';


class PermissionsProvider extends ChangeNotifier {
  Resource<UserAccess> _userAccess = Resource(data: null, isLoading: false, error: null);
  late OrganizationsProvider _organizations;
  bool _isLoaded = false;
  String? _loadedOrgRef;
  //For permissions to auto-load when an organization is set, so the dashboard no longer hangs waiting for permissions to become ready.

  void update(OrganizationsProvider venue) {
    _organizations = venue;
    // Trigger a reload when organization changes or when permissions were never loaded.
    if (_organizations.isOrganizationSet &&
        _loadedOrgRef != _organizations.current.id &&
        !_userAccess.isLoading) {
      unawaited(load());
    }
  }

  // Generic wrapper to perform service calls and reload state
  Future<UserAccess?> _performServiceCall(Future Function() operation) async {
    try {
      await operation();
      return await load();
    } catch (e) {
      _userAccess = _userAccess.copyWith(error: toException(e), isLoading: false);
      notifyListeners();
      return _userAccess.data;
    }
  }

  /// Load the [UserAccess] for the current venue.
 Future<UserAccess?> load() async {
    if (!_organizations.isOrganizationSet) {
      // Organization is not ready yet; skip loading until it becomes available.
      return _userAccess.data;
    }
    _userAccess = _userAccess.copyWith(isLoading: true, error: null);
    notifyListeners();

    try {
      final orgRef = _organizations.current.id;
      final access = await PermissionsService.load(orgRef);
      _userAccess = _userAccess.copyWith(data: access, isLoading: false);

      if (canRead(ResourceType.roles)) {
        final allAccess = await PermissionsService.loadAll(orgRef);
        _userAccess = _userAccess.copyWith(data: allAccess, isLoading: false);
      }
      _isLoaded = true;
      _loadedOrgRef = orgRef;
    } catch (e) {
      _userAccess = _userAccess.copyWith(
        error: e is Exception ? e : Exception(e.toString()),
        isLoading: false,
      );
    }

    notifyListeners();
    return _userAccess.data;
  }

  Future<UserAccess?> changeRole(String accountRef, String newRoleDescRef) async {
    final currentRole = roles.firstWhereOrNull((r) => r.accountRef == accountRef);
    final currentDesc = currentRole != null
      ? roleDescriptions.firstWhereOrNull((d) => d.storable.id == currentRole.descriptionRef)
      : null;

    if (currentRole == null || currentDesc == null || currentDesc.storable.id == newRoleDescRef) {
      return _userAccess.data;
    }
    return _performServiceCall(() => PermissionsService.changeRole(
      _organizations.current.id,
      ChangeRole(accountRef: accountRef, newRoleDescriptionRef: newRoleDescRef),
    ));
  }

  Future<UserAccess?> deleteRoleDescription(String descRef) {
    return _performServiceCall(() => PermissionsService.deleteRoleDescription(descRef));
  }

  Future<RoleDescription?> createRoleDescription({
    required String name,
    String? description,
  }) async {
    if (!_organizations.isOrganizationSet) {
      throw StateError('Organization is not set');
    }
    final normalizedName = name.trim();
    final normalizedDescription = description?.trim();
    final roleDescription = RoleDescription.build(
      organizationRef: _organizations.current.id,
      roleDescription: newDescribable(name: normalizedName, description: normalizedDescription),
    );

    await _performServiceCall(() => PermissionsService.createRoleDescription(roleDescription));
    final matches = roleDescriptions.where(
      (role) =>
        role.organizationRef == _organizations.current.id &&
        role.name == normalizedName &&
        (role.description ?? '') == (normalizedDescription ?? ''),
    ).toList()
      ..sort((a, b) => b.createdAt.compareTo(a.createdAt));

    return matches.isEmpty ? null : matches.first;
  }

  Future<UserAccess?> createPermissions(List<Policy> policies) {
    return _performServiceCall(() => PermissionsService.createPolicies(policies));
  }

  Future<UserAccess?> deletePermissions(List<Policy> policies) {
    return _performServiceCall(() => PermissionsService.deletePolicies(policies));
  }

  Future<UserAccess?> changePermissions(List<Policy> add, List<Policy> remove) {
    return _performServiceCall(() => PermissionsService.changePolicies(add, remove));
  }

  // -- Data getters --
  Set<ResourceType> extractResourceTypes(Iterable<PolicyDescription> descriptions) => descriptions.expand((policy) => policy.resourceTypes ?? <ResourceType>[]).toSet();

  Set<ResourceType> get resources => Set.unmodifiable(extractResourceTypes(policyDescriptions));

  Set<ResourceType> getRoleResources(String roleDescRef) => Set.unmodifiable(
    extractResourceTypes(
      getRolePermissions(roleDescRef)
        .map((p) => getPolicyDescription(p.policy.descriptionRef))
        .whereType<PolicyDescription>(),
    ),
  );

  String? getPolicyDescriptionRef(ResourceType resource) => policyDescriptions.firstWhereOrNull((p) => p.resourceTypes?.contains(resource) ?? false)?.storable.id;

  List<PolicyDescription> get policyDescriptions => List.unmodifiable(_userAccess.data?.descriptions.policies ?? []);
  List<RoleDescription> get roleDescriptions => List.unmodifiable(_userAccess.data?.descriptions.roles ?? []);
  List<Permission> get permissions => List.unmodifiable(_userAccess.data?.permissions.permissions ?? []);
  List<Policy> get policies => List.unmodifiable(_userAccess.data?.permissions.policies ?? []);
  List<Role> get roles => List.unmodifiable(_userAccess.data?.permissions.roles ?? []);

  Role? getRole(String accountRef) => roles.firstWhereOrNull((r) => r.accountRef == accountRef);
  RoleDescription? getRoleDescription(String descRef) => roleDescriptions.firstWhereOrNull((d) => d.storable.id == descRef);
  List<Role> getRoles(String accountRef) => roles.where((r) => r.accountRef == accountRef).toList();
  List<Policy> getRolePolicies(String roleRef) => policies.where((p) => p.roleDescriptionRef == roleRef).toList();
  List<Permission> getRolePermissions(String descRef) => permissions.where((p) => p.policy.roleDescriptionRef == descRef).toList();
  PolicyDescription? getPolicyDescription(String policyRef) => policyDescriptions.firstWhereOrNull((p) => p.storable.id == policyRef);

  // -- Permission checks --

  bool get isLoading => _userAccess.isLoading;
  bool get isReady => !_userAccess.isLoading && error == null && _isLoaded;
  Exception? get error => _userAccess.error;

  bool _hasMatchingPermission(
    PolicyDescription pd,
    Effect effect,
    perm.Action? action, {
    Object? objectRef,
  }) => permissions.firstWhereOrNull(
    (p) =>
      p.policy.descriptionRef == pd.storable.id &&
      p.policy.effect.effect == effect &&
      (action == null || p.policy.effect.action == action) &&
      (p.policy.objectRef == null || p.policy.objectRef == objectRef),
  ) != null;

  bool canAccessResource(
    ResourceType resource, {
    perm.Action? action,
    Object? objectRef,
  }) {
    if (!_organizations.isOrganizationSet) return false;
    final orgId = _organizations.current.id;
    final pd = policyDescriptions.firstWhereOrNull(
      (policy) =>
        (policy.resourceTypes?.contains(resource) ?? false) &&
        (policy.organizationRef == null || policy.organizationRef == orgId),
    );
    if (pd == null) return false;
    if (_hasMatchingPermission(pd, Effect.deny, action, objectRef: objectRef)) return false;
    return _hasMatchingPermission(pd, Effect.allow, action, objectRef: objectRef);
  }

  void reset() {
    _userAccess = Resource(data: null, isLoading: false, error: null);
    _isLoaded = false;
    _loadedOrgRef = null;
    notifyListeners();
  }

  Future<void> resetAsync() async {
    reset();
  }

  bool canRead(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.read, objectRef: objectRef);
  bool canUpdate(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.update, objectRef: objectRef);
  bool canDelete(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.delete, objectRef: objectRef);
  bool canCreate(ResourceType r) => canAccessResource(r, action: perm.Action.create);
}
