import 'dart:async';
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;

import '../../../core/config/app_environment_provider.dart';
import '../../../core/network/api_error.dart';
import '../../../core/network/http_client_provider.dart';
import '../../directory/domain/employee.dart';
import '../../auth/data/auth_session_store.dart';
import '../domain/organization_models.dart';

class OrgContextApiClient {
  const OrgContextApiClient({
    required this.httpClient,
    required this.baseUri,
    required this.tenantSlug,
    this.sessionStore,
    this.timeout = const Duration(seconds: 15),
    this.cacheTtl = const Duration(minutes: 5),
  });

  static final Map<String, _OrgContextCacheEntry> _cache = {};

  final http.Client httpClient;
  final Uri baseUri;
  final String tenantSlug;
  final AuthSessionStore? sessionStore;
  final Duration timeout;
  final Duration cacheTtl;

  Future<OrgChartSnapshot> fetchOrgContext({
    bool forceRefresh = false,
    String? tenantSlug,
  }) async {
    final credential = await _activeCredential(tenantSlug: tenantSlug);
    final cacheKey = credential.cacheKey;
    final now = DateTime.now().toUtc();
    final cached = _cache[cacheKey];
    if (!forceRefresh && cached != null && cached.expiresAt.isAfter(now)) {
      return cached.future;
    }

    final future = _fetchOrgContext(credential);
    _cache[cacheKey] = _OrgContextCacheEntry(
      future: future,
      expiresAt: now.add(cacheTtl),
    );
    try {
      return await future;
    } catch (_) {
      if (identical(_cache[cacheKey]?.future, future)) {
        _cache.remove(cacheKey);
      }
      rethrow;
    }
  }

  static void clearCacheForTesting() {
    _cache.clear();
  }

  Future<OrgChartSnapshot> _fetchOrgContext(
    _OrgContextCredentialSnapshot credential,
  ) async {
    final response = await httpClient
        .get(
          _resolve(
            credential.baseUri,
            '/api/v1/integrations/org-context',
            queryParameters: {
              'tenantSlug': credential.tenantSlug,
              'includeUsers': 'true',
              'includeUserIds': 'true',
            },
          ),
          headers: _headers(credential),
        )
        .timeout(timeout);

    return _toSnapshot(_decodeOk(response));
  }

  Future<_OrgContextCredentialSnapshot> _activeCredential({
    String? tenantSlug,
  }) async {
    final session = await _session();
    final requestedTenantSlug = tenantSlug?.trim();

    return _OrgContextCredentialSnapshot(
      baseUri: baseUri,
      tenantSlug: requestedTenantSlug == null || requestedTenantSlug.isEmpty
          ? this.tenantSlug
          : requestedTenantSlug,
      appSessionToken: session?.token ?? '',
    );
  }

  Future<StoredAuthSession?> _session() async {
    final store = sessionStore;
    if (store == null) {
      debugPrint('OrgContextApiClient._session no sessionStore');
      return null;
    }
    final session = await store.load();
    if (session == null || session.isExpired) {
      debugPrint(
        'OrgContextApiClient._session unavailable session=${session != null} expired=${session?.isExpired ?? true}',
      );
      return null;
    }
    debugPrint(
      'OrgContextApiClient._session tokenLength=${session.token.length} expiresAt=${session.expiresAt.toUtc().toIso8601String()}',
    );
    return session;
  }

  Map<String, String> _headers(_OrgContextCredentialSnapshot credential) {
    final appSessionToken = credential.appSessionToken.trim();
    final headers = {
      'accept': 'application/json',
      if (appSessionToken.isNotEmpty) 'Authorization': 'Bearer $appSessionToken',
      if (appSessionToken.isNotEmpty) 'X-App-Session': appSessionToken,
    };
    debugPrint(
      'OrgContextApiClient._headers headers=$headers tenantSlug=${credential.tenantSlug} base=${credential.baseUri}',
    );
    return headers;
  }

  Map<String, dynamic> _decodeOk(http.Response response) {
    final decoded = _decodeObject(response.body);
    if (response.statusCode < 200 || response.statusCode >= 300) {
      throw ApiException(
        statusCode: response.statusCode,
        apiError: ApiError.fromJson(decoded),
      );
    }
    return decoded;
  }

  Uri _resolve(
    Uri baseUri,
    String path, {
    Map<String, String?> queryParameters = const {},
  }) {
    final normalizedBase = baseUri.path.endsWith('/')
        ? baseUri
        : baseUri.replace(path: '${baseUri.path}/');
    final uri = normalizedBase.resolve(path.replaceFirst(RegExp(r'^/'), ''));
    final filteredQuery = Map<String, String>.fromEntries(
      queryParameters.entries
          .where((entry) {
            return entry.value != null && entry.value!.trim().isNotEmpty;
          })
          .map((entry) => MapEntry(entry.key, entry.value!)),
    );
    return uri.replace(queryParameters: filteredQuery);
  }

  Map<String, dynamic> _decodeObject(String body) {
    final decoded = jsonDecode(body);
    if (decoded is Map<String, dynamic>) {
      return decoded;
    }
    throw const FormatException('Expected JSON object response');
  }

  OrgChartSnapshot _toSnapshot(Map<String, dynamic> json) {
    final issuedAt = _parseDate(json['issuedAt']) ?? DateTime.now().toUtc();
    final tenantById = <String, TenantSummary>{};
    final ownMemberCountByTenantId = <String, int>{};

    final tree = json['tree'];
    if (tree is Map<String, dynamic>) {
      _collectTenantTree(tree, tenantById);
    }

    final tenantItems = <Map<String, dynamic>>[];
    for (final item in json['tenants'] as List<dynamic>? ?? const []) {
      if (item is! Map<String, dynamic>) {
        continue;
      }
      tenantItems.add(item);
      final tenant = _tenantFromJson(item);
      if (tenant.id.isNotEmpty) {
        tenantById[tenant.id] = tenant;
        ownMemberCountByTenantId[tenant.id] = _memberList(item).length;
      }
    }

    final employeesByMembership = <String, Employee>{};
    for (final item in tenantItems) {
      final tenant = _tenantFromJson(item);
      if (tenant.id.isNotEmpty) {
        tenantById[tenant.id] = tenant;
      }
      for (final employee in _employeesFromTenant(item, tenant)) {
        if (employee.id.isNotEmpty) {
          employeesByMembership['${tenant.id}:${employee.id}'] = employee;
        }
      }
    }

    final countedTenantById = _recountTenantMemberCounts(
      tenantById: tenantById,
      ownMemberCountByTenantId: ownMemberCountByTenantId,
    );
    final tenants = countedTenantById.values.toList()
      ..sort((a, b) {
        final parentCompare = (a.parentId ?? '').compareTo(b.parentId ?? '');
        if (parentCompare != 0) {
          return parentCompare;
        }
        return a.name.compareTo(b.name);
      });

    final employees = employeesByMembership.values.toList();
    return OrgChartSnapshot(
      tenants: tenants,
      employees: employees,
      generatedAt: issuedAt.toUtc(),
      cache: const OrgChartCacheInfo(source: 'baron-org-context', hit: false),
    );
  }

  Map<String, TenantSummary> _recountTenantMemberCounts({
    required Map<String, TenantSummary> tenantById,
    required Map<String, int> ownMemberCountByTenantId,
  }) {
    final childrenByParentId = <String, List<TenantSummary>>{};
    for (final tenant in tenantById.values) {
      final parentId = tenant.parentId;
      if (parentId == null || parentId.trim().isEmpty) {
        continue;
      }
      childrenByParentId
          .putIfAbsent(parentId, () => <TenantSummary>[])
          .add(tenant);
    }

    final totalCountByTenantId = <String, int>{};

    int totalFor(String tenantId) {
      final cached = totalCountByTenantId[tenantId];
      if (cached != null) {
        return cached;
      }

      final ownCount =
          ownMemberCountByTenantId[tenantId] ??
          tenantById[tenantId]?.memberCount ??
          0;
      final total = (childrenByParentId[tenantId] ?? const <TenantSummary>[])
          .fold<int>(ownCount, (sum, child) => sum + totalFor(child.id));
      totalCountByTenantId[tenantId] = total;
      return total;
    }

    return {
      for (final entry in tenantById.entries)
        entry.key: _copyTenantWithCounts(
          entry.value,
          memberCount:
              ownMemberCountByTenantId[entry.key] ?? entry.value.memberCount,
          totalMemberCount: totalFor(entry.key),
        ),
    };
  }

  TenantSummary _copyTenantWithCounts(
    TenantSummary tenant, {
    required int memberCount,
    required int totalMemberCount,
  }) {
    return TenantSummary(
      id: tenant.id,
      name: tenant.name,
      slug: tenant.slug,
      type: tenant.type,
      parentId: tenant.parentId,
      memberCount: memberCount,
      totalMemberCount: totalMemberCount,
    );
  }

  void _collectTenantTree(
    Map<String, dynamic> json,
    Map<String, TenantSummary> tenantById,
  ) {
    final tenant = _tenantFromJson(json);
    if (tenant.id.isNotEmpty) {
      tenantById[tenant.id] = tenant;
    }

    final children = json['children'];
    if (children is List<dynamic>) {
      for (final child in children) {
        if (child is Map<String, dynamic>) {
          _collectTenantTree(child, tenantById);
        }
      }
    }
  }

  TenantSummary _tenantFromJson(Map<String, dynamic> json) {
    final memberCount = _readInt(json['memberCount']);
    final totalMemberCount = _readInt(json['totalMemberCount']) ?? memberCount;
    return TenantSummary(
      id: _readString(json, ['id']),
      name: _readString(json, ['name']),
      slug: _readString(json, ['slug']),
      type: _readString(json, ['type', 'orgUnitType']),
      parentId: _nullableString(json['parentId']),
      memberCount: memberCount ?? _memberList(json).length,
      totalMemberCount: totalMemberCount ?? _memberList(json).length,
    );
  }

  List<Employee> _employeesFromTenant(
    Map<String, dynamic> tenantJson,
    TenantSummary tenant,
  ) {
    final members = _memberList(tenantJson);
    return members.indexed.map((entry) {
      final index = entry.$1;
      final member = entry.$2;
      final rawPhone = _readString(member, [
        'phoneNumber',
        'phone',
        'mobile',
        'mobilePhone',
      ]);
      final email = _nullableString(member['email']);
      final memberId = _readString(member, [
        'id',
        'userId',
        'employeeId',
        'loginId',
      ]);
      final id = memberId.isNotEmpty
          ? memberId
          : [
              tenant.id,
              email,
              _readString(member, ['name']),
              rawPhone,
            ].whereType<String>().where((value) => value.isNotEmpty).join(':');

      return Employee(
        id: id,
        name: _readString(member, ['name']),
        phoneNumber: rawPhone,
        phoneDisplay: _formatPhone(rawPhone),
        email: email,
        tenantId: tenant.id,
        tenantName: tenant.name,
        tenantSlug: tenant.slug,
        department: _nullableString(member['department']),
        grade: _nullableString(member['grade']),
        position: _nullableString(member['position']),
        jobTitle: _nullableString(member['jobTitle']),
        status: _nullableString(member['status']),
        profileImageUrl: _nullableString(member['profileImageUrl']),
        sortOrder: _readInt(member['sortOrder']) ?? index,
        isManager: member['isManager'] as bool? ?? false,
      );
    }).toList();
  }

  List<Map<String, dynamic>> _memberList(Map<String, dynamic> json) {
    final members = json['members'];
    if (members is! List<dynamic>) {
      return const [];
    }
    return members
        .whereType<Map>()
        .map((member) => Map<String, dynamic>.from(member))
        .toList();
  }

  DateTime? _parseDate(Object? value) {
    if (value is! String || value.trim().isEmpty) {
      return null;
    }
    return DateTime.tryParse(value);
  }

  int? _readInt(Object? value) {
    if (value is int) {
      return value;
    }
    if (value is num) {
      return value.toInt();
    }
    if (value is String) {
      return int.tryParse(value);
    }
    return null;
  }

  String _readString(Map<String, dynamic> json, List<String> keys) {
    for (final key in keys) {
      final value = _nullableString(json[key]);
      if (value != null) {
        return value;
      }
    }
    return '';
  }

  String? _nullableString(Object? value) {
    if (value is! String) {
      return null;
    }
    final trimmed = value.trim();
    return trimmed.isEmpty ? null : trimmed;
  }

  String? _formatPhone(String value) {
    var digits = value.replaceAll(RegExp(r'[^0-9]'), '');
    if (digits.startsWith('82') && digits.length >= 11) {
      digits = '0${digits.substring(2)}';
    }
    if (digits.length == 11 && digits.startsWith('010')) {
      return '${digits.substring(0, 3)}-${digits.substring(3, 7)}-${digits.substring(7)}';
    }
    return value.trim().isEmpty ? null : value.trim();
  }
}

final orgContextApiClientProvider = Provider<OrgContextApiClient>((ref) {
  final environment = ref.watch(appEnvironmentProvider);
  return OrgContextApiClient(
    httpClient: ref.watch(httpClientProvider),
    baseUri: Uri.parse(environment.orgContextApiBaseUrl),
    tenantSlug: environment.orgContextTenantSlug,
    sessionStore: ref.watch(authSessionStoreProvider),
  );
});

final orgContextSnapshotProvider = FutureProvider.autoDispose<OrgChartSnapshot>(
  (ref) {
    return ref.watch(orgContextApiClientProvider).fetchOrgContext();
  },
);

class _OrgContextCredentialSnapshot {
  const _OrgContextCredentialSnapshot({
    required this.baseUri,
    required this.tenantSlug,
    required this.appSessionToken,
  });

  final Uri baseUri;
  final String tenantSlug;
  final String appSessionToken;

  String get cacheKey {
    return [
      baseUri.toString(),
      tenantSlug,
      appSessionToken,
    ].join('|');
  }
}

class _OrgContextCacheEntry {
  const _OrgContextCacheEntry({required this.future, required this.expiresAt});

  final Future<OrgChartSnapshot> future;
  final DateTime expiresAt;
}
