import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/availability_repository.dart';

/// Implementation of [AvailabilityRepository] using Firebase Data Connect.
///
/// Note: The backend schema supports recurring availablity (Weekly/DayOfWeek),
/// not specific date availability. Therefore, updating availability for a specific
/// date will update the availability for that Day of Week globally (Recurring).
class AvailabilityRepositoryImpl implements AvailabilityRepository {
  final dc.ExampleConnector _dataConnect;
  final firebase.FirebaseAuth _firebaseAuth;

  AvailabilityRepositoryImpl({
    required dc.ExampleConnector dataConnect,
    required firebase.FirebaseAuth firebaseAuth,
  })  : _dataConnect = dataConnect,
        _firebaseAuth = firebaseAuth;

  Future<String> _getStaffId() async {
    final firebase.User? user = _firebaseAuth.currentUser;
    if (user == null) throw Exception('User not authenticated');

    final QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> result =
        await _dataConnect.getStaffByUserId(userId: user.uid).execute();
    if (result.data.staffs.isEmpty) {
      throw Exception('Staff profile not found');
    }
    return result.data.staffs.first.id;
  }

  @override
  Future<List<DayAvailability>> getAvailability(DateTime start, DateTime end) async {
    final String staffId = await _getStaffId();
    
    // 1. Fetch Weekly recurring availability
    final QueryResult<dc.ListStaffAvailabilitiesByStaffIdData, dc.ListStaffAvailabilitiesByStaffIdVariables> result =
        await _dataConnect.listStaffAvailabilitiesByStaffId(staffId: staffId).limit(100).execute();

    final List<dc.ListStaffAvailabilitiesByStaffIdStaffAvailabilities> items = result.data.staffAvailabilities;

    // 2. Map to lookup: DayOfWeek -> Map<SlotName, IsAvailable>
    final Map<dc.DayOfWeek, Map<dc.AvailabilitySlot, bool>> weeklyMap = {};
    
    for (final item in items) {
      dc.DayOfWeek day;
      try {
        day = dc.DayOfWeek.values.byName(item.day.stringValue);
      } catch (_) {
        continue;
      }

      dc.AvailabilitySlot slot;
      try {
        slot = dc.AvailabilitySlot.values.byName(item.slot.stringValue);
      } catch (_) {
        continue;
      }

      bool isAvailable = false;
      try {
        final dc.AvailabilityStatus status = dc.AvailabilityStatus.values.byName(item.status.stringValue);
        isAvailable = _statusToBool(status);
      } catch (_) {
        isAvailable = false;
      }

      if (!weeklyMap.containsKey(day)) {
        weeklyMap[day] = {};
      }
      weeklyMap[day]![slot] = isAvailable;
    }

    // 3. Generate DayAvailability for requested range
    final List<DayAvailability> days = [];
    final int dayCount = end.difference(start).inDays;

    for (int i = 0; i <= dayCount; i++) {
        final DateTime date = start.add(Duration(days: i));
        final dc.DayOfWeek dow = _toBackendDay(date.weekday);
        
      final Map<dc.AvailabilitySlot, bool> daySlots = weeklyMap[dow] ?? {};
      
      // We define 3 standard slots for every day
      final List<AvailabilitySlot> slots = [
        _createSlot(date, dow, daySlots, dc.AvailabilitySlot.MORNING),
        _createSlot(date, dow, daySlots, dc.AvailabilitySlot.AFTERNOON),
        _createSlot(date, dow, daySlots, dc.AvailabilitySlot.EVENING),
      ];

      final bool isDayAvailable = slots.any((s) => s.isAvailable);

      days.add(DayAvailability(
        date: date,
        isAvailable: isDayAvailable,
        slots: slots,
      ));
    }
    return days;
  }

  AvailabilitySlot _createSlot(
    DateTime date, 
    dc.DayOfWeek dow, 
    Map<dc.AvailabilitySlot, bool> existingSlots, 
    dc.AvailabilitySlot slotEnum, 
  ) {
    final bool isAvailable = existingSlots[slotEnum] ?? false;
    return AvailabilityAdapter.fromPrimitive(slotEnum.name, isAvailable: isAvailable);
  }

  @override
  Future<DayAvailability> updateDayAvailability(DayAvailability availability) async {
    final String staffId = await _getStaffId();
    final dc.DayOfWeek dow = _toBackendDay(availability.date.weekday);

    // Update each slot in the backend. 
    // This updates the recurring rule for this DayOfWeek.
    for (final AvailabilitySlot slot in availability.slots) {
      final dc.AvailabilitySlot slotEnum = _toBackendSlot(slot.id);
      final dc.AvailabilityStatus status = _boolToStatus(slot.isAvailable);

      await _upsertSlot(staffId, dow, slotEnum, status);
    }

    return availability;
  }

  @override
  Future<List<DayAvailability>> applyQuickSet(DateTime start, DateTime end, String type) async {
    final String staffId = await _getStaffId();
    
    // QuickSet updates the Recurring schedule for all days involved.
    // However, if the user selects a range that covers e.g. Mon-Fri, we update Mon-Fri.
    
    final int dayCount = end.difference(start).inDays;
    final Set<dc.DayOfWeek> processedDays = {};
    final List<DayAvailability> resultDays = [];

    for (int i = 0; i <= dayCount; i++) {
        final DateTime date = start.add(Duration(days: i));
        final dc.DayOfWeek dow = _toBackendDay(date.weekday);
        
        // Logic to determine if enabled based on type
        bool enableDay = false;
        if (type == 'all') enableDay = true;
        else if (type == 'clear') enableDay = false;
        else if (type == 'weekdays') {
           enableDay = (dow != dc.DayOfWeek.SATURDAY && dow != dc.DayOfWeek.SUNDAY);
        } else if (type == 'weekends') {
           enableDay = (dow == dc.DayOfWeek.SATURDAY || dow == dc.DayOfWeek.SUNDAY);
        }

        // Only update backend once per DayOfWeek (since it's recurring)
        // to avoid redundant calls if range > 1 week.
        if (!processedDays.contains(dow)) {
           processedDays.add(dow);
           
           final dc.AvailabilityStatus status = _boolToStatus(enableDay);
           
           await Future.wait([
             _upsertSlot(staffId, dow, dc.AvailabilitySlot.MORNING, status),
             _upsertSlot(staffId, dow, dc.AvailabilitySlot.AFTERNOON, status),
             _upsertSlot(staffId, dow, dc.AvailabilitySlot.EVENING, status),
           ]);
        }
        
        // Prepare return object
        final slots = [
             AvailabilityAdapter.fromPrimitive('MORNING', isAvailable: enableDay),
             AvailabilityAdapter.fromPrimitive('AFTERNOON', isAvailable: enableDay),
             AvailabilityAdapter.fromPrimitive('EVENING', isAvailable: enableDay),
        ];

        resultDays.add(DayAvailability(
          date: date,
          isAvailable: enableDay,
          slots: slots,
        ));
    }

    return resultDays;
  }

  Future<void> _upsertSlot(String staffId, dc.DayOfWeek day, dc.AvailabilitySlot slot, dc.AvailabilityStatus status) async {
    // Check if exists
    final result = await _dataConnect.getStaffAvailabilityByKey(
      staffId: staffId,
      day: day,
      slot: slot,
    ).execute();
    
    if (result.data.staffAvailability != null) {
       // Update
       await _dataConnect.updateStaffAvailability(
        staffId: staffId,
        day: day,
        slot: slot,
      ).status(status).execute();
    } else {
       // Create
       await _dataConnect.createStaffAvailability(
        staffId: staffId,
        day: day,
        slot: slot,
      ).status(status).execute();
    }
  }

  // --- Private Helpers ---

  dc.DayOfWeek _toBackendDay(int weekday) {
    switch (weekday) {
      case DateTime.monday: return dc.DayOfWeek.MONDAY;
      case DateTime.tuesday: return dc.DayOfWeek.TUESDAY;
      case DateTime.wednesday: return dc.DayOfWeek.WEDNESDAY;
      case DateTime.thursday: return dc.DayOfWeek.THURSDAY;
      case DateTime.friday: return dc.DayOfWeek.FRIDAY;
      case DateTime.saturday: return dc.DayOfWeek.SATURDAY;
      case DateTime.sunday: return dc.DayOfWeek.SUNDAY;
      default: return dc.DayOfWeek.MONDAY;
    }
  }

  dc.AvailabilitySlot _toBackendSlot(String id) {
    switch (id.toLowerCase()) {
      case 'morning': return dc.AvailabilitySlot.MORNING;
      case 'afternoon': return dc.AvailabilitySlot.AFTERNOON;
      case 'evening': return dc.AvailabilitySlot.EVENING;
      default: return dc.AvailabilitySlot.MORNING;
    }
  }

  bool _statusToBool(dc.AvailabilityStatus status) {
    return status == dc.AvailabilityStatus.CONFIRMED_AVAILABLE;
  }

  dc.AvailabilityStatus _boolToStatus(bool isAvailable) {
    return isAvailable ? dc.AvailabilityStatus.CONFIRMED_AVAILABLE : dc.AvailabilityStatus.BLOCKED;
  }
}
