import 'dart:async';
import 'dart:developer';
import 'dart:io';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:drift/drift.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

import '../../../../main.dart';
import '../../../core/constants/app_constants.dart';
import '../../../core/utils/connectivity_helper.dart';
import '../local/app_database.dart';
import '../repositories/user_repository.dart';
import 'api_client.dart';

final syncServiceProvider = Provider<SyncService>((ref) {
  final db = ref.watch(appDatabaseProvider);
  final apiClient = ref.watch(apiClientProvider);
  return SyncService(db: db, apiClient: apiClient);
});

class SyncService {
  final AppDatabase db;
  final ApiClient apiClient;
  final _storage = const FlutterSecureStorage();
  bool _isSyncing = false;

  SyncService({required this.db, required this.apiClient});

  Future<void> sync() async {
    if (_isSyncing) return;

    if (!await ConnectivityHelper.hasConnection()) {
      log('Sync skipped: No internet connection');
      return;
    }

    _isSyncing = true;

    try {
      await _retry(_performSync, maxAttempts: 3);
      log('✅ Sync completed successfully');
    } catch (e) {
      log('❌ Sync failed finally: $e');
    } finally {
      _isSyncing = false;
    }
  }

  Future<void> _performSync() async {
    log('🔄 Starting Sync...');

    final dirtyCycles = await (db.select(db.cycles)
          ..where((c) => c.isDirty.equals(true)))
        .get();

    for (var cycle in dirtyCycles) {
      if (cycle.serverId == null) {
        log('📤 Pushing new cycle ${cycle.cycleNumber}...');
        final tmsMap = cycle.trainingMaxes
            .map((k, v) => MapEntry(k, (v as num).toDouble()));

        final response = await apiClient.createCycle(tmsMap);
        final newServerId = response['id'];

        await db.transaction(() async {
          await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
              .write(
            CyclesCompanion(
              serverId: Value(newServerId),
              isDirty: const Value(false),
            ),
          );

          final oldLocalIdRef = cycle.id.toString();
          await (db.update(db.workouts)
                ..where((w) => w.cycleId.equals(oldLocalIdRef)))
              .write(
            WorkoutsCompanion(
              cycleId: Value(newServerId),
              isDirty: const Value(true),
            ),
          );
        });
      } else {
        await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
            .write(const CyclesCompanion(isDirty: Value(false)));
      }
    }

    final dirtyUser = await (db.select(db.users)
          ..where((u) => u.isDirty.equals(true)))
        .getSingleOrNull();
    final dirtyWorkouts = await (db.select(db.workouts)
          ..where((w) => w.isDirty.equals(true)))
        .get();

    final validWorkouts =
        dirtyWorkouts.where((w) => w.cycleId.length > 5).toList();

    final pushData = <String, dynamic>{
      'workouts': validWorkouts.map((w) {
        return {
          'id': w.serverId,
          'local_id': w.id,
          'cycle_id': w.cycleId,
          'week': w.week,
          'day': w.day,
          'completed_at': w.completedAt?.toIso8601String(),
          'xp_earned': w.xpEarned,
          'notes': w.notes,
          'exercises': w.exercises,
        };
      }).toList(),
      'user_stats': dirtyUser != null
          ? {
              'xp': dirtyUser.xp,
              'level': dirtyUser.level,
              'current_bodyweight': dirtyUser.currentBodyweight,
              'exercise_variants': dirtyUser.exerciseVariants,
            }
          : null,
    };

    final lastSync = await _storage.read(key: AppConstants.keyLastSync);

    // if ((pushData['workouts'] as List).isNotEmpty || pushData['user_stats'] != null) { ... }

    log('Checking for updates...');
    final response = await apiClient.sync(
      lastSyncTimestamp: lastSync ?? '',
      pushData: pushData,
    );

    await db.transaction(() async {
      if (dirtyUser != null) {
        await (db.update(db.users)..where((u) => u.id.equals(dirtyUser.id)))
            .write(const UsersCompanion(isDirty: Value(false)));
      }
      for (var w in validWorkouts) {
        await (db.update(db.workouts)..where((dw) => dw.id.equals(w.id)))
            .write(const WorkoutsCompanion(isDirty: Value(false)));
      }

      if (response['pull_data'] != null) {
        if (response['pull_data']['cycles'] != null) {
          final pulledCycles = response['pull_data']['cycles'] as List;
          for (var cJson in pulledCycles) {
            final serverId = cJson['id'] as String;
            final existing = await (db.select(db.cycles)
                  ..where((c) => c.serverId.equals(serverId)))
                .getSingleOrNull();

            final tms = cJson['training_maxes'] as Map<String, dynamic>;
            final companion = CyclesCompanion(
              serverId: Value(serverId),
              userId: Value(cJson['user_id']),
              cycleNumber: Value(cJson['cycle_number']),
              startDate: Value(DateTime.parse(cJson['start_date'])),
              endDate: Value(DateTime.tryParse(cJson['end_date'] ?? '')),
              isActive: Value(cJson['is_active'] ?? false),
              trainingMaxes: Value(tms),
              isDirty: const Value(false),
              updatedAt: Value(DateTime.now()),
              createdAt: existing == null
                  ? Value(DateTime.now())
                  : const Value.absent(),
            );

            if (existing != null) {
              await (db.update(db.cycles)
                    ..where((c) => c.id.equals(existing.id)))
                  .write(companion);
            } else {
              await db.into(db.cycles).insert(companion);
            }
          }
        }

        if (response['pull_data']['workouts'] != null) {
          final pulledWorkouts = response['pull_data']['workouts'] as List;
          log('📥 Pulled ${pulledWorkouts.length} workouts.');

          for (var wJson in pulledWorkouts) {
            final serverId = wJson['id'] as String;
            final cycleId = wJson['cycle_id'] as String;
            final week = wJson['week'] as int;
            final day = wJson['day'] as int;

            var existing = await (db.select(db.workouts)
                  ..where((w) => w.serverId.equals(serverId)))
                .getSingleOrNull();

            if (existing == null) {
              final candidates = await (db.select(db.workouts)
                    ..where((w) =>
                        w.cycleId.equals(cycleId) &
                        w.week.equals(week) &
                        w.day.equals(day)))
                  .get();
              if (candidates.isNotEmpty) {
                existing = candidates.first;
                log('🔄 Merging local workout ${existing.id} with server ID $serverId');
              }
            }

            final companion = WorkoutsCompanion(
              serverId: Value(serverId),
              cycleId: Value(cycleId),
              userId: Value(wJson['user_id']),
              week: Value(week),
              day: Value(day),
              completedAt:
                  Value(DateTime.tryParse(wJson['completed_at'] ?? '')),
              xpEarned: Value(wJson['xp_earned'] ?? 0),
              exercises: Value(wJson['exercises'] ?? []),
              notes: Value(wJson['notes'] ?? ''),
              isDirty: const Value(false),
              updatedAt: Value(DateTime.now()),
              createdAt: existing == null
                  ? Value(DateTime.now())
                  : const Value.absent(),
            );

            if (existing != null) {
              await (db.update(db.workouts)
                    ..where((w) => w.id.equals(existing!.id)))
                  .write(companion);
            } else {
              await db.into(db.workouts).insert(companion);
            }
          }
        }
      }
    });

    if (response['server_timestamp'] != null) {
      await _storage.write(
          key: AppConstants.keyLastSync, value: response['server_timestamp']);
    }
  }

  Future<T> _retry<T>(
    Future<T> Function() function, {
    int maxAttempts = 3,
    Duration initialDelay = const Duration(seconds: 2),
  }) async {
    int attempts = 0;
    while (true) {
      try {
        attempts++;
        return await function();
      } catch (e) {
        final shouldRetry = (e is SocketException) ||
            (e.toString().contains('500')) ||
            (e.toString().contains('502')) ||
            (e.toString().contains('timeout'));

        if (attempts >= maxAttempts || !shouldRetry) {
          rethrow;
        }

        final delay = initialDelay * (1 << (attempts - 1)); // 2s, 4s, 8s
        log('⚠️ Sync attempt $attempts failed. Retrying in ${delay.inSeconds}s... Error: $e');
        await Future.delayed(delay);
      }
    }
  }
}
