import 'package:flutter/material.dart';

import 'package:collection/collection.dart';

import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/methods_cache.dart';

import 'package:pweb/models/recipient/method_snapshot.dart';
import 'package:pweb/models/state/seed_state.dart';
import 'package:pweb/providers/address_book_recipient_form.dart';
import 'package:pweb/utils/payment/label.dart';
import 'package:pweb/widgets/dialogs/confirmation_dialog.dart';

import 'package:pweb/generated/i18n/app_localizations.dart';


class AddressBookRecipientFormController extends ChangeNotifier {
  static final ListEquality<RecipientMethodSnapshot> _listEquality =
      ListEquality<RecipientMethodSnapshot>();

  final List<PaymentType> _supportedTypes;
  final Map<PaymentType, List<RecipientMethodDraft>> _methods;

  Recipient? _recipient;
  RecipientMethodsCacheProvider? _methodsCache;

  String _initialName = '';
  String _initialEmail = '';
  List<RecipientMethodSnapshot> _initialMethods = const [];
  SeedState _snapshotState = SeedState.idle;
  SeedState _seedState = SeedState.idle;

  AddressBookRecipientFormController({
    required List<PaymentType> supportedTypes,
  }) : _supportedTypes = List.unmodifiable(supportedTypes),
       _methods = {
         for (final type in supportedTypes) type: <RecipientMethodDraft>[],
       };

  List<PaymentType> get supportedTypes => _supportedTypes;
  Map<PaymentType, List<RecipientMethodDraft>> get methods => {
    for (final entry in _methods.entries)
      entry.key: List<RecipientMethodDraft>.unmodifiable(entry.value),
  };
  PaymentType? get preferredType =>
      _supportedTypes.firstWhere(
        (type) => _methods[type]?.isNotEmpty == true,
        orElse: () => _supportedTypes.first,
      );

  bool get hasAnyMethod => _methods.values.any(
    (entries) => entries.any((entry) => entry.data != null || entry.existing != null),
  );

  Future<void> saveForm({
    required BuildContext context,
    required GlobalKey<FormState> formKey,
    required AddressBookRecipientFormProvider formState,
    required String name,
    required String email,
    ValueChanged<Recipient?>? onSaved,
  }) async {
    final l10n = AppLocalizations.of(context)!;
    if (!formKey.currentState!.validate() || !hasAnyMethod) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(l10n.recipientFormRule)),
      );
      return;
    }

    try {
      final saved = await formState.save(
        name: name,
        email: email,
        methodNames: _methodNames(context),
        methodDrafts: allDrafts(),
      );
      onSaved?.call(saved);
    } catch (_) {
      if (!context.mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(l10n.notificationError(l10n.noErrorInformation))),
      );
    }
  }

  Future<void> handleBack({
    required BuildContext context,
    required GlobalKey<FormState> formKey,
    required AddressBookRecipientFormProvider formState,
    required String name,
    required String email,
    ValueChanged<Recipient?>? onSaved,
  }) async {
    if (!context.mounted) return;
    if (!hasUnsavedChanges(name: name, email: email)) {
      onSaved?.call(null);
      return;
    }

    final l10n = AppLocalizations.of(context)!;
    final shouldSave = await showConfirmationDialog(
      context: context,
      title: l10n.unsavedChangesTitle,
      message: l10n.unsavedChangesMessage,
      confirmLabel: l10n.save,
      cancelLabel: l10n.discard,
    );
    if (!context.mounted) return;

    if (shouldSave) {
      await saveForm(
        context: context,
        formKey: formKey,
        formState: formState,
        name: name,
        email: email,
        onSaved: onSaved,
      );
    } else {
      onSaved?.call(null);
    }
  }

  void update({
    required Recipient? recipient,
    required RecipientMethodsCacheProvider methodsCache,
  }) {
    if (!identical(_methodsCache, methodsCache)) {
      _methodsCache?.removeListener(_handleCacheChange);
      _methodsCache = methodsCache;
      _methodsCache?.addListener(_handleCacheChange);
    }

    if (_recipient?.id != recipient?.id) {
      _reset(recipient);
    }

    _maybeSeedFromCache();
  }

  bool hasUnsavedChanges({
    required String name,
    required String email,
  }) {
    if (_recipient == null) return false;
    final methodsCache = _methodsCache;
    if (methodsCache == null) return false;

    _captureIfReady();
    final nameChanged = name.trim() != _initialName.trim();
    final emailChanged = email.trim() != _initialEmail.trim();
    if (_snapshotState != SeedState.seeded) {
      return nameChanged || emailChanged;
    }
    final current = _snapshotFrom();
    final methodsChanged = !_listEquality.equals(_initialMethods, current);

    return nameChanged || emailChanged || methodsChanged;
  }

  List<RecipientMethodDraft> allDrafts() =>
      _methods.values.expand((entries) => entries).toList();

  int? addMethod(PaymentType type) {
    final entries = _methods[type];
    if (entries == null) return null;
    entries.add(RecipientMethodDraft(type: type));
    notifyListeners();
    return entries.length - 1;
  }

  void removeMethod(PaymentType type, int index) {
    final entries = _methods[type];
    if (entries == null) return;
    if (index < 0 || index >= entries.length) return;
    entries.removeAt(index);
    notifyListeners();
  }

  void updateMethod(PaymentType type, int index, PaymentMethodData data) {
    final entries = _methods[type];
    if (entries == null) return;
    if (index < 0 || index >= entries.length) return;
    entries[index].data = data;
    notifyListeners();
  }

  void _handleCacheChange() {
    _maybeSeedFromCache();
  }

  void _reset(Recipient? recipient) {
    _recipient = recipient;
    _initialName = recipient?.name ?? '';
    _initialEmail = recipient?.email ?? '';
    _initialMethods = const [];
    _snapshotState = SeedState.idle;
    _seedState = SeedState.idle;
    _resetMethods();
    notifyListeners();
  }

  void _resetMethods() {
    for (final entries in _methods.values) {
      entries.clear();
    }
  }

  void _maybeSeedFromCache() {
    final recipient = _recipient;
    final methodsCache = _methodsCache;
    if (recipient == null || methodsCache == null) return;
    if (_seedState == SeedState.seeded) return;
    if (!methodsCache.hasMethodsFor(recipient.id)) return;
    _seedState = SeedState.seeded;
    _seedMethodsFromExisting(methodsCache.methodsForRecipient(recipient.id));
  }

  void _seedMethodsFromExisting(List<PaymentMethod> existing) {
    _resetMethods();
    for (final method in existing) {
      final type = method.type;
      final entries = _methods[type];
      if (entries == null) continue;
      entries.add(
        RecipientMethodDraft(
          type: type,
          existing: method,
          data: method.data,
        ),
      );
    }
    _initialMethods = _snapshotFrom();
    _snapshotState = SeedState.seeded;
    notifyListeners();
  }

  void _captureIfReady() {
    if (_snapshotState == SeedState.seeded) return;
    final recipient = _recipient;
    final methodsCache = _methodsCache;
    if (recipient == null || methodsCache == null) return;
    if (!methodsCache.hasMethodsFor(recipient.id)) return;

    _initialMethods = _snapshotFrom();
    _snapshotState = SeedState.seeded;
  }

  List<RecipientMethodSnapshot> _snapshotFrom() {
    final snapshots = <RecipientMethodSnapshot>[];
    for (final type in _supportedTypes) {
      final entries = _methods[type] ?? const <RecipientMethodDraft>[];
      for (final entry in entries) {
        snapshots.add(RecipientMethodSnapshot.fromDraft(entry));
      }
    }
    return snapshots;
  }

  Map<PaymentType, String> _methodNames(BuildContext context) => {
    for (final type in _supportedTypes) type: getPaymentTypeLabel(context, type),
  };

  @override
  void dispose() {
    _methodsCache?.removeListener(_handleCacheChange);
    super.dispose();
  }
}
