import 'package:flutter/foundation.dart';

import '../../core/cache/info_cache.dart';
import '../../core/config/debug_test.dart';
import '../../core/protocol/opcode_map.dart';
import '../../core/protocol/packet.dart';
import '../../core/storage/app_database.dart';
import '../../core/utils/logger.dart';
import '../../models/contact_info.dart';
import '../api.dart';
import 'messages.dart';

class CachedContact {
  final int id;
  final int accountId;
  final String firstName;
  final String? lastName;
  final int phone;
  final int? photoId;
  final String? baseUrl;
  final String? baseRawUrl;
  final int updateTime;
  final Set<String> options;

  const CachedContact({
    required this.id,
    required this.accountId,
    required this.firstName,
    this.lastName,
    required this.phone,
    this.photoId,
    this.baseUrl,
    this.baseRawUrl,
    required this.updateTime,
    this.options = const {},
  });

  bool get isOfficial => options.contains('OFFICIAL');
  bool get isBot => options.contains('BOT');
  bool get isServiceAccount => options.contains('SERVICE_ACCOUNT');
  bool get isVerified => isOfficial;

  factory CachedContact.fromDbRow(Map<String, dynamic> row) => CachedContact(
    id: row['id'] as int,
    accountId: row['account_id'] as int,
    firstName: row['first_name'] as String,
    lastName: row['last_name'] as String?,
    phone: row['phone'] as int,
    photoId: row['photo_id'] as int?,
    baseUrl: row['base_url'] as String?,
    baseRawUrl: row['base_raw_url'] as String?,
    updateTime: row['update_time'] as int,
    options: _decodeOptions(row['options']),
  );

  static Set<String> _decodeOptions(dynamic raw) {
    if (raw is! String || raw.isEmpty) return const {};
    return raw.split(',').where((s) => s.isNotEmpty).toSet();
  }
}

class PhoneLookupResult {
  final int id;
  final String? name;
  final String? avatarUrl;
  final int phone;

  const PhoneLookupResult({
    required this.id,
    this.name,
    this.avatarUrl,
    this.phone = 0,
  });
}

class ContactPhotos {
  final List<String> urls;
  final int total;

  const ContactPhotos({required this.urls, required this.total});

  static const empty = ContactPhotos(urls: [], total: 0);
}

enum AddContactStatus { added, notFound, error }

class AddContactResult {
  final AddContactStatus status;
  final CachedContact? contact;

  const AddContactResult(this.status, {this.contact});
}

class ContactsModule {
  static final ValueNotifier<int> revision = ValueNotifier<int>(0);

  static Future<PhoneLookupResult?> findByPhone(
    Api api,
    String phone, {
    bool silent = false,
  }) async {
    final normalized = _normalizePhone(phone);
    if (normalized == null) return null;
    final Packet packet;
    try {
      packet = await api.sendRequest(Opcode.contactInfoByPhone, {
        'phone': normalized,
      }, silent: silent);
    } on PacketError {
      return null;
    }
    if (packet.isError) return null;
    final contact = (packet.payload as Map?)?['contact'];
    if (contact is! Map) return null;
    final id = contact['id'];
    if (id is! int) return null;

    primeContactCache(contact);

    final payloadPhone = contact['phone'];
    final resolvedPhone = payloadPhone is int && payloadPhone > 0
        ? payloadPhone
        : int.tryParse(normalized.substring(1)) ?? 0;
    ContactCache.putPhone(id, resolvedPhone);

    String? name;
    final names = contact['names'];
    if (names is List) {
      final n = names.firstWhere((e) => e is Map, orElse: () => null);
      if (n is Map) {
        final first =
            (n['firstName'] as String?) ?? (n['name'] as String?) ?? '';
        final last = (n['lastName'] as String?) ?? '';
        final full = '$first $last'.trim();
        if (full.isNotEmpty) name = full;
      }
    }

    return PhoneLookupResult(
      id: id,
      name: name,
      avatarUrl: contact['baseUrl'] as String?,
      phone: resolvedPhone,
    );
  }

  static String? _normalizePhone(String raw) {
    final digits = raw.replaceAll(RegExp(r'[^\d]'), '');
    if (digits.length < 5) return null;
    return '+$digits';
  }

  static Future<CachedContact?> addContact(
    Api api,
    int id,
    String firstName, {
    int phone = 0,
  }) async {
    final resp = await api.sendRequest(Opcode.contactUpdate, {
      'action': 'ADD',
      'contactId': id,
      if (firstName.isNotEmpty) 'firstName': firstName,
    });

    final profile = await AppDatabase.loadActiveProfile();
    if (profile == null) return null;

    final data = resp.payload;
    final contact = (data is Map && data['contact'] is Map)
        ? (data['contact'] as Map).cast<dynamic, dynamic>()
        : null;

    final row = contact != null
        ? _parseContact(contact, profile.id)
        : {
            'id': id,
            'account_id': profile.id,
            'first_name': firstName,
            'last_name': null,
            'phone': 0,
            'photo_id': null,
            'base_url': null,
            'base_raw_url': null,
            'update_time': 0,
            'options': null,
          };

    if (row == null) return null;
    if (phone > 0 && ((row['phone'] as int?) ?? 0) == 0) {
      row['phone'] = phone;
    }
    await AppDatabase.saveContacts([row]);
    if (contact != null) primeContactCache(contact);
    revision.value++;
    return CachedContact.fromDbRow(row);
  }

  static Future<AddContactResult> addContactByPhone(
    Api api, {
    required String phone,
    required String firstName,
    String lastName = '',
  }) async {
    final normalized = _normalizePhone(phone);
    if (normalized == null) {
      return const AddContactResult(AddContactStatus.error);
    }

    final Packet resp;
    try {
      resp = await api.sendRequest(Opcode.contactAddByPhone, {
        'phone': normalized,
        'firstName': firstName,
        'lastName': lastName,
      }, silent: true);
    } on PacketError catch (e) {
      final key = e.errorKey ?? '';
      final notFound =
          key == 'user.not.found' ||
          e.message.toLowerCase().contains('not found');
      return AddContactResult(
        notFound ? AddContactStatus.notFound : AddContactStatus.error,
      );
    } catch (_) {
      return const AddContactResult(AddContactStatus.error);
    }

    final data = resp.payload;
    final contact = (data is Map && data['contact'] is Map)
        ? (data['contact'] as Map).cast<dynamic, dynamic>()
        : null;
    if (contact == null) {
      return const AddContactResult(AddContactStatus.error);
    }

    final profile = await AppDatabase.loadActiveProfile();
    if (profile == null) {
      return const AddContactResult(AddContactStatus.error);
    }

    final row = _parseContact(contact, profile.id);
    if (row == null) {
      return const AddContactResult(AddContactStatus.error);
    }

    await AppDatabase.saveContacts([row]);
    primeContactCache(contact);
    revision.value++;
    return AddContactResult(
      AddContactStatus.added,
      contact: CachedContact.fromDbRow(row),
    );
  }

  static Future<CachedContact?> updateContact(
    Api api, {
    required int contactId,
    required String firstName,
    required String lastName,
  }) async {
    final Packet resp;
    try {
      resp = await api.sendRequest(Opcode.contactUpdate, {
        'contactId': contactId,
        'action': 'UPDATE',
        'firstName': firstName,
        'lastName': lastName,
      });
    } catch (_) {
      return null;
    }

    final profile = await AppDatabase.loadActiveProfile();
    if (profile == null) return null;

    final data = resp.payload;
    final contact = (data is Map && data['contact'] is Map)
        ? (data['contact'] as Map).cast<dynamic, dynamic>()
        : null;
    if (contact == null) return null;

    final row = _parseContact(contact, profile.id);
    if (row == null) return null;

    await AppDatabase.saveContacts([row]);
    primeContactCache(contact);
    ContactInfoFetch.putContact(contactId, contact);
    revision.value++;
    return CachedContact.fromDbRow(row);
  }

  static Future<bool> removeContact(Api api, int contactId) async {
    try {
      await api.sendRequest(Opcode.contactUpdate, {
        'contactId': contactId,
        'action': 'REMOVE',
      });
    } catch (_) {
      return false;
    }

    final profile = await AppDatabase.loadActiveProfile();
    if (profile != null) {
      await AppDatabase.deleteContact(profile.id, contactId);
    }

    ContactCache.remove(contactId);

    ContactInfo? info = ContactInfoFetch.peek(contactId);
    if (info == null) {
      ContactInfoFetch.invalidate(contactId);
      info = await ContactInfoFetch.get(contactId, forceRefresh: true);
    }

    final rawNames = info?.raw['names'];
    if (info != null && rawNames is List) {
      final stripped = rawNames
          .where((n) => !(n is Map && n['type'] == 'CUSTOM'))
          .toList();
      final newRaw = Map<String, dynamic>.from(info.raw)..['names'] = stripped;
      ContactInfoFetch.putContact(contactId, newRaw);
      primeContactCache(newRaw);
    } else {
      ContactInfoFetch.invalidate(contactId);
    }

    revision.value++;
    return true;
  }

  static final Set<int> _blockedIds = <int>{};
  static bool _blockedLoaded = false;

  static void clearBlockedCache() {
    _blockedIds.clear();
    _blockedLoaded = false;
  }

  static const int _blockedPageSize = 100;
  static const int _blockedMaxPages = 20;

  static Future<bool> isBlocked(Api api, int contactId) async {
    if (!_blockedLoaded) await _loadBlockedIds(api);
    return _blockedIds.contains(contactId);
  }

  static Future<void> _loadBlockedIds(Api api) async {
    final ids = <int>{};
    try {
      for (var page = 0; page < _blockedMaxPages; page++) {
        final map = await api.sendRequestMap(Opcode.contactList, {
          'status': 'BLOCKED',
          'count': _blockedPageSize,
          'from': page * _blockedPageSize,
        });
        final contacts = map?['contacts'];
        if (contacts is! List) return;
        ids.addAll(
          contacts.whereType<Map>().map((c) => c['id']).whereType<int>(),
        );
        if (contacts.length < _blockedPageSize) break;
      }
    } catch (e) {
      logger.w('Не удалось получить список заблокированных: $e');
      return;
    }
    _blockedIds
      ..clear()
      ..addAll(ids);
    _blockedLoaded = true;
  }

  static Future<bool> setBlocked(Api api, int contactId, bool blocked) async {
    try {
      final packet = await api.sendRequest(Opcode.contactUpdate, {
        'contactId': contactId,
        'action': blocked ? 'BLOCK' : 'UNBLOCK',
      });
      if (packet.isError) return false;
    } catch (e) {
      logger.w('setBlocked $contactId: $e');
      return false;
    }

    if (blocked) {
      _blockedIds.add(contactId);
    } else {
      _blockedIds.remove(contactId);
    }
    ContactInfoFetch.invalidate(contactId);
    revision.value++;
    return true;
  }

  static Future<void> syncFromLoginPayload(
    Map<dynamic, dynamic> data,
    int accountId,
  ) async {
    final contacts = data['contacts'];
    if (contacts is! List || contacts.isEmpty) return;

    final rows = <Map<String, dynamic>>[];
    for (final raw in contacts.whereType<Map>()) {
      final contact = raw.cast<dynamic, dynamic>();
      final row = _parseContact(contact, accountId);
      if (row != null) rows.add(row);
      primeContactCache(contact);
    }

    if (rows.isNotEmpty) {
      await AppDatabase.saveContacts(rows);
      revision.value++;
    }
    logger.i(
      'Контакты: получено ${contacts.length}, сохранено ${rows.length} (акк $accountId)',
    );
  }

  static Future<void> syncFromServer(Api api, int accountId) async {
    final map = await api.sendRequestMap(Opcode.contactsGet, {
      'contactsSync': 0,
    });
    if (map == null) return;
    await syncFromLoginPayload(map.cast<dynamic, dynamic>(), accountId);
  }

  static Future<ProfileData?> fetchSelfProfile(Api api, int accountId) async {
    final map = await api.sendRequestMap(Opcode.contactInfo, {
      'contactIds': [accountId],
    });
    final contacts = map?['contacts'];
    if (contacts is! List) return null;
    for (final raw in contacts.whereType<Map>()) {
      if (raw['id'] != accountId) continue;
      final contact = raw.cast<dynamic, dynamic>();
      primeContactCache(contact);
      return ProfileData.fromServerMap(contact);
    }
    return null;
  }

  static void primeContactCache(Map<dynamic, dynamic> contact) {
    final id = contact['id'];
    if (id is! int) return;

    final phone = contact['phone'];
    if (phone is int) ContactCache.putPhone(id, phone);

    final names = contact['names'];
    if (names is List && names.isNotEmpty) {
      final nameRaw = _preferredNameEntry(names);
      if (nameRaw != null) {
        final firstName = (nameRaw['firstName'] as String?) ?? '';
        final lastName = nameRaw['lastName'] as String?;
        final fullName = (lastName != null && lastName.isNotEmpty)
            ? '$firstName $lastName'
            : firstName;
        if (fullName.isNotEmpty) ContactCache.put(id, fullName);
      }
    }

    final baseUrl = contact['baseUrl'] as String?;
    if (baseUrl != null && baseUrl.isNotEmpty) {
      ContactCache.putAvatar(id, baseUrl);
    }
  }

  static final Map<int, ContactPhotos> _photosHead = {};

  static ContactPhotos? cachedPhotos(int contactId) => _photosHead[contactId];

  static Future<ContactPhotos> fetchPhotos(
    Api api,
    int contactId, {
    int from = 0,
    int count = 25,
  }) async {
    final map = await api.sendRequestMap(Opcode.contactPhotos, {
      'contactId': contactId,
      'from': from,
      'count': count,
    });
    if (map == null) return ContactPhotos.empty;
    final rawUrls = map['urls'];
    final urls = rawUrls is List
        ? rawUrls.whereType<String>().toList()
        : <String>[];
    final total = map['total'] is int ? map['total'] as int : urls.length;
    final photos = ContactPhotos(urls: urls, total: total);
    if (from == 0) _photosHead[contactId] = photos;
    return photos;
  }

  static Future<List<CachedContact>> getContacts(int accountId) async {
    final rows = await AppDatabase.loadContacts(accountId);
    return rows.map(CachedContact.fromDbRow).toList();
  }

  static Future<CachedContact?> getContact(int accountId, int id) async {
    final row = await AppDatabase.loadContact(accountId, id);
    return row == null ? null : CachedContact.fromDbRow(row);
  }

  static const List<String> _debugFirstNames = [
    'Алиса', 'Борис', 'Вера', 'Глеб', 'Дарья', 'Егор', 'Жанна', 'Захар',
    'Ирина', 'Кирилл', 'Лия', 'Максим', 'Нина', 'Олег', 'Полина', 'Роман',
    'София', 'Тимур', 'Ульяна', 'Фёдор', 'Ханна', 'Цветана', 'Чеслав', 'Шура',
  ];

  static const List<String> _debugLastNames = [
    'Иванов', 'Петров', 'Сидоров', 'Кузнецов', 'Смирнов', 'Попов', 'Волков',
    'Соколов', 'Морозов', 'Новиков', 'Фёдоров', 'Козлов',
  ];

  static List<CachedContact> debugContacts() {
    final count = DebugTest.contactCount;
    final out = <CachedContact>[];
    for (var i = 0; i < count; i++) {
      final first = _debugFirstNames[i % _debugFirstNames.length];
      final last =
          _debugLastNames[(i ~/ _debugFirstNames.length) %
              _debugLastNames.length];
      out.add(
        CachedContact(
          id: 900000000 + i,
          accountId: DebugTest.debugAccountId,
          firstName: '$first ${i + 1}',
          lastName: last,
          phone: 79000000000 + i,
          baseUrl: 'https://i.pravatar.cc/150?u=komet_debug_$i',
          updateTime: 1,
          options: i % 6 == 0 ? const {'OFFICIAL'} : const {},
        ),
      );
    }
    return out;
  }

  /// Прогревает in-memory ContactCache из локальных контактов.
  /// Нужно вызывать на cold start: иначе кэш пуст до следующего логина.
  static Future<void> primeCacheFromDb(int accountId) async {
    final contacts = await getContacts(accountId);
    for (final c in contacts) {
      ContactCache.putPhone(c.id, c.phone);
      final fullName = (c.lastName != null && c.lastName!.isNotEmpty)
          ? '${c.firstName} ${c.lastName}'
          : c.firstName;
      if (fullName.isNotEmpty) ContactCache.put(c.id, fullName);
      if (c.baseUrl != null && c.baseUrl!.isNotEmpty) {
        ContactCache.putAvatar(c.id, c.baseUrl);
      }
    }
  }

  static Map? _preferredNameEntry(List names) {
    Map? oneme;
    Map? any;
    for (final n in names) {
      if (n is! Map) continue;
      any ??= n;
      final type = n['type'];
      if (type == 'CUSTOM') return n;
      if (type == 'ONEME') oneme ??= n;
    }
    return oneme ?? any;
  }

  static Map<String, dynamic>? _parseContact(
    Map<dynamic, dynamic> contact,
    int accountId,
  ) {
    final id = contact['id'];
    if (id is! int) return null;

    String firstName = '';
    String? lastName;

    final names = contact['names'];
    if (names is List && names.isNotEmpty) {
      final nameRaw = _preferredNameEntry(names);
      if (nameRaw == null) return null;
      firstName = (nameRaw['firstName'] as String?) ?? '';
      lastName = nameRaw['lastName'] as String?;
    }

    final optionsRaw = contact['options'];
    String? optionsStr;
    if (optionsRaw is List) {
      optionsStr = optionsRaw.whereType<String>().join(',');
    }

    return {
      'id': id,
      'account_id': accountId,
      'first_name': firstName,
      'last_name': lastName,
      'phone': (contact['phone'] as int?) ?? 0,
      'photo_id': contact['photoId'] as int?,
      'base_url': contact['baseUrl'] as String?,
      'base_raw_url': contact['baseRawUrl'] as String?,
      'update_time': (contact['updateTime'] as int?) ?? 0,
      'options': optionsStr,
    };
  }
}
