import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import 'package:intl/intl.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import '../../domain/repositories/shifts_repository_interface.dart';

class ShiftsRepositoryImpl
    implements ShiftsRepositoryInterface {
  final dc.DataConnectService _service;

  ShiftsRepositoryImpl() : _service = dc.DataConnectService.instance;

  // Cache: ShiftID -> ApplicationID (For Accept/Decline)
  final Map<String, String> _shiftToAppIdMap = {};
  // Cache: ApplicationID -> RoleID (For Accept/Decline w/ Update mutation)
  final Map<String, String> _appToRoleIdMap = {};

  @override
  Future<List<Shift>> getMyShifts({
    required DateTime start,
    required DateTime end,
  }) async {
    return _fetchApplications(start: start, end: end);
  }

  @override
  Future<List<Shift>> getPendingAssignments() async {
    return <Shift>[];
  }

  @override
  Future<List<Shift>> getCancelledShifts() async {
    return <Shift>[];
  }

  @override
  Future<List<Shift>> getHistoryShifts() async {
    final staffId = await _service.getStaffId();
    final fdc.QueryResult<dc.ListCompletedApplicationsByStaffIdData, dc.ListCompletedApplicationsByStaffIdVariables> response = await _service.executeProtected(() => _service.connector
        .listCompletedApplicationsByStaffId(staffId: staffId)
        .execute());
    final List<Shift> shifts = [];

    for (final app in response.data.applications) {
      _shiftToAppIdMap[app.shift.id] = app.id;
      _appToRoleIdMap[app.id] = app.shiftRole.id;

      final String roleName = app.shiftRole.role.name;
      final String orderName =
          (app.shift.order.eventName ?? '').trim().isNotEmpty
              ? app.shift.order.eventName!
              : app.shift.order.business.businessName;
      final String title = '$roleName - $orderName';
      final DateTime? shiftDate = _service.toDateTime(app.shift.date);
      final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
      final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
      final DateTime? createdDt = _service.toDateTime(app.createdAt);

      shifts.add(
        Shift(
          id: app.shift.id,
          roleId: app.shiftRole.roleId,
          title: title,
          clientName: app.shift.order.business.businessName,
          logoUrl: app.shift.order.business.companyLogoUrl,
          hourlyRate: app.shiftRole.role.costPerHour,
          location: app.shift.location ?? '',
          locationAddress: app.shift.order.teamHub.hubName,
          date: shiftDate?.toIso8601String() ?? '',
          startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
          endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
          createdDate: createdDt?.toIso8601String() ?? '',
          status: _mapStatus(dc.ApplicationStatus.CHECKED_OUT),
          description: app.shift.description,
          durationDays: app.shift.durationDays,
          requiredSlots: app.shiftRole.count,
          filledSlots: app.shiftRole.assigned ?? 0,
          hasApplied: true,
          latitude: app.shift.latitude,
          longitude: app.shift.longitude,
          breakInfo: BreakAdapter.fromData(
            isPaid: app.shiftRole.isBreakPaid ?? false,
            breakTime: app.shiftRole.breakType?.stringValue,
          ),
        ),
      );
    }
    return shifts;
  }

  Future<List<Shift>> _fetchApplications({
    DateTime? start,
    DateTime? end,
  }) async {
    final staffId = await _service.getStaffId();
    var query = _service.connector.getApplicationsByStaffId(staffId: staffId);
    if (start != null && end != null) {
      query = query.dayStart(_service.toTimestamp(start)).dayEnd(_service.toTimestamp(end));
    }
    final fdc.QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> response = await _service.executeProtected(() => query.execute());

    final apps = response.data.applications;
    final List<Shift> shifts = [];

    for (final app in apps) {
      _shiftToAppIdMap[app.shift.id] = app.id;
      _appToRoleIdMap[app.id] = app.shiftRole.id;

      final String roleName = app.shiftRole.role.name;
      final String orderName =
          (app.shift.order.eventName ?? '').trim().isNotEmpty
              ? app.shift.order.eventName!
              : app.shift.order.business.businessName;
      final String title = '$roleName - $orderName';
      final DateTime? shiftDate = _service.toDateTime(app.shift.date);
      final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
      final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
      final DateTime? createdDt = _service.toDateTime(app.createdAt);

      // Override status to reflect the application state (e.g., CHECKED_OUT, CONFIRMED)
      final bool hasCheckIn = app.checkInTime != null;
      final bool hasCheckOut = app.checkOutTime != null;
      dc.ApplicationStatus? appStatus;
      if (app.status is dc.Known<dc.ApplicationStatus>) {
        appStatus = (app.status as dc.Known<dc.ApplicationStatus>).value;
      }
      final String mappedStatus = hasCheckOut
          ? 'completed'
          : hasCheckIn
              ? 'checked_in'
              : _mapStatus(appStatus ?? dc.ApplicationStatus.CONFIRMED);
      shifts.add(
        Shift(
          id: app.shift.id,
          roleId: app.shiftRole.roleId,
          title: title,
          clientName: app.shift.order.business.businessName,
          logoUrl: app.shift.order.business.companyLogoUrl,
          hourlyRate: app.shiftRole.role.costPerHour,
          location: app.shift.location ?? '',
          locationAddress: app.shift.order.teamHub.hubName,
          date: shiftDate?.toIso8601String() ?? '',
          startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
          endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
          createdDate: createdDt?.toIso8601String() ?? '',
          status: mappedStatus,
          description: app.shift.description,
          durationDays: app.shift.durationDays,
          requiredSlots: app.shiftRole.count,
          filledSlots: app.shiftRole.assigned ?? 0,
          hasApplied: true,
          latitude: app.shift.latitude,
          longitude: app.shift.longitude,
          breakInfo: BreakAdapter.fromData(
            isPaid: app.shiftRole.isBreakPaid ?? false,
            breakTime: app.shiftRole.breakType?.stringValue,
          ),
        ),
      );
    }
    return shifts;
  }

  String _mapStatus(dc.ApplicationStatus status) {
    switch (status) {
      case dc.ApplicationStatus.CONFIRMED:
        return 'confirmed';
      case dc.ApplicationStatus.PENDING:
        return 'pending';
      case dc.ApplicationStatus.CHECKED_OUT:
        return 'completed';
      case dc.ApplicationStatus.REJECTED:
        return 'cancelled';
      default:
        return 'open';
    }
  }

  @override
  Future<List<Shift>> getAvailableShifts(String query, String type) async {
    final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
    if (vendorId == null || vendorId.isEmpty) {
      return <Shift>[];
    }

    final fdc.QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> result = await _service.executeProtected(() => _service.connector
        .listShiftRolesByVendorId(vendorId: vendorId)
        .execute());
    


    final allShiftRoles = result.data.shiftRoles;

    // Fetch my applications to filter out already booked shifts
    final List<Shift> myShifts = await _fetchApplications();
    final Set<String> myShiftIds = myShifts.map((s) => s.id).toSet();

    final List<Shift> mappedShifts = [];
    for (final sr in allShiftRoles) {
      // Skip if I have already applied/booked this shift
      if (myShiftIds.contains(sr.shiftId)) continue;


      final DateTime? shiftDate = _service.toDateTime(sr.shift.date);
      final startDt = _service.toDateTime(sr.startTime);
      final endDt = _service.toDateTime(sr.endTime);
      final createdDt = _service.toDateTime(sr.createdAt);

      mappedShifts.add(
        Shift(
          id: sr.shiftId,
          roleId: sr.roleId,
          title: sr.role.name,
          clientName: sr.shift.order.business.businessName,
          logoUrl: null,
          hourlyRate: sr.role.costPerHour,
          location: sr.shift.location ?? '',
          locationAddress: sr.shift.locationAddress ?? '',
          date: shiftDate?.toIso8601String() ?? '',
          startTime: startDt != null
              ? DateFormat('HH:mm').format(startDt)
              : '',
          endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
          createdDate: createdDt?.toIso8601String() ?? '',
          status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
          description: sr.shift.description,
          durationDays: sr.shift.durationDays,
          requiredSlots: sr.count,
          filledSlots: sr.assigned ?? 0,
          latitude: sr.shift.latitude,
          longitude: sr.shift.longitude,
          breakInfo: BreakAdapter.fromData(
            isPaid: sr.isBreakPaid ?? false,
            breakTime: sr.breakType?.stringValue,
          ),
        ),
      );
    }

    if (query.isNotEmpty) {
      return mappedShifts
          .where(
            (s) =>
                s.title.toLowerCase().contains(query.toLowerCase()) ||
                s.clientName.toLowerCase().contains(query.toLowerCase()),
          )
          .toList();
    }

    return mappedShifts;
  }

  @override
  Future<Shift?> getShiftDetails(String shiftId, {String? roleId}) async {
    return _getShiftDetails(shiftId, roleId: roleId);
  }

  Future<Shift?> _getShiftDetails(String shiftId, {String? roleId}) async {
    if (roleId != null && roleId.isNotEmpty) {
      final roleResult = await _service.executeProtected(() => _service.connector
          .getShiftRoleById(shiftId: shiftId, roleId: roleId)
          .execute());
      final sr = roleResult.data.shiftRole;
      if (sr == null) return null;

      final DateTime? startDt = _service.toDateTime(sr.startTime);
      final DateTime? endDt = _service.toDateTime(sr.endTime);
      final DateTime? createdDt = _service.toDateTime(sr.createdAt);

      final String staffId = await _service.getStaffId();
      bool hasApplied = false;
      String status = 'open';
      final apps = await _service.executeProtected(() =>
          _service.connector.getApplicationsByStaffId(staffId: staffId).execute());
      final app = apps.data.applications
          .where(
            (a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId,
          )
          .firstOrNull;
      if (app != null) {
        hasApplied = true;
        if (app.status is dc.Known<dc.ApplicationStatus>) {
          final dc.ApplicationStatus s =
              (app.status as dc.Known<dc.ApplicationStatus>).value;
          status = _mapStatus(s);
        }
      }
    
      return Shift(
        id: sr.shiftId,
        roleId: sr.roleId,
        title: sr.shift.order.business.businessName,
        clientName: sr.shift.order.business.businessName,
        logoUrl: sr.shift.order.business.companyLogoUrl,
        hourlyRate: sr.role.costPerHour,
        location: sr.shift.location ?? sr.shift.order.teamHub.hubName,
        locationAddress: sr.shift.locationAddress ?? '',
        date: startDt?.toIso8601String() ?? '',
        startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
        endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
        createdDate: createdDt?.toIso8601String() ?? '',
        status: status,
        description: sr.shift.description,
        durationDays: null,
        requiredSlots: sr.count,
        filledSlots: sr.assigned ?? 0,
        hasApplied: hasApplied,
        totalValue: sr.totalValue,
        latitude: sr.shift.latitude,
        longitude: sr.shift.longitude,
        breakInfo: BreakAdapter.fromData(
          isPaid: sr.isBreakPaid ?? false,
          breakTime: sr.breakType?.stringValue,
        ),
      );
    }

    final fdc.QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result =
        await _service.executeProtected(() => _service.connector.getShiftById(id: shiftId).execute());
    final s = result.data.shift;
    if (s == null) return null;

    int? required;
    int? filled;
    Break? breakInfo;
    try {
      final rolesRes = await _service.executeProtected(() =>
          _service.connector.listShiftRolesByShiftId(shiftId: shiftId).execute());
      if (rolesRes.data.shiftRoles.isNotEmpty) {
        required = 0;
        filled = 0;
        for (var r in rolesRes.data.shiftRoles) {
          required = (required ?? 0) + r.count;
          filled = (filled ?? 0) + (r.assigned ?? 0);
        }
        // Use the first role's break info as a representative
        final firstRole = rolesRes.data.shiftRoles.first;
        breakInfo = BreakAdapter.fromData(
          isPaid: firstRole.isBreakPaid ?? false,
          breakTime: firstRole.breakType?.stringValue,
        );
      }
    } catch (_) {}

    final startDt = _service.toDateTime(s.startTime);
    final endDt = _service.toDateTime(s.endTime);
    final createdDt = _service.toDateTime(s.createdAt);

    return Shift(
      id: s.id,
      title: s.title,
      clientName: s.order.business.businessName,
      logoUrl: null,
      hourlyRate: s.cost ?? 0.0,
      location: s.location ?? '',
      locationAddress: s.locationAddress ?? '',
      date: startDt?.toIso8601String() ?? '',
      startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
      endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
      createdDate: createdDt?.toIso8601String() ?? '',
      status: s.status?.stringValue ?? 'OPEN',
      description: s.description,
      durationDays: s.durationDays,
      requiredSlots: required,
      filledSlots: filled,
      latitude: s.latitude,
      longitude: s.longitude,
      breakInfo: breakInfo,
    );
  }

  @override
  Future<void> applyForShift(
    String shiftId, {
    bool isInstantBook = false,
    String? roleId,
  }) async {
    final staffId = await _service.getStaffId();

    String targetRoleId = roleId ?? '';
    if (targetRoleId.isEmpty) {
      throw Exception('Missing role id.');
    }

    final roleResult = await _service.executeProtected(() => _service.connector
        .getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
        .execute());
    final role = roleResult.data.shiftRole;
    if (role == null) {
      throw Exception('Shift role not found');
    }
    final shiftResult =
        await _service.executeProtected(() => _service.connector.getShiftById(id: shiftId).execute());
    final shift = shiftResult.data.shift;
    if (shift == null) {
      throw Exception('Shift not found');
    }
    final DateTime? shiftDate = _service.toDateTime(shift.date);
    if (shiftDate != null) {
      final DateTime dayStartUtc = DateTime.utc(
        shiftDate.year,
        shiftDate.month,
        shiftDate.day,
      );
      final DateTime dayEndUtc = DateTime.utc(
        shiftDate.year,
        shiftDate.month,
        shiftDate.day,
        23,
        59,
        59,
        999,
        999,
      );

      final dayApplications = await _service.executeProtected(() => _service.connector
          .vaidateDayStaffApplication(staffId: staffId)
          .dayStart(_service.toTimestamp(dayStartUtc))
          .dayEnd(_service.toTimestamp(dayEndUtc))
          .execute());
      if (dayApplications.data.applications.isNotEmpty) {
        throw Exception('The user already has a shift that day.');
      }
    }
    final existingApplicationResult = await _service.executeProtected(() => _service.connector
        .getApplicationByStaffShiftAndRole(
          staffId: staffId,
          shiftId: shiftId,
          roleId: targetRoleId,
        )
        .execute());
    if (existingApplicationResult.data.applications.isNotEmpty) {
      throw Exception('Application already exists.');
    }
    final int assigned = role.assigned ?? 0;
    if (assigned >= role.count) {
      throw Exception('This shift is full.');
    }

    final int filled = shift.filled ?? 0;

    String? appId;
    bool updatedRole = false;
    bool updatedShift = false;
    try {
      final appResult = await _service.executeProtected(() => _service.connector
          .createApplication(
            shiftId: shiftId,
            staffId: staffId,
            roleId: targetRoleId,
            status: dc.ApplicationStatus.CONFIRMED,
            origin: dc.ApplicationOrigin.STAFF,
          )
          // TODO: this should be PENDING so a vendor can accept it.
          .execute());
      appId = appResult.data.application_insert.id;

      await _service.executeProtected(() => _service.connector
          .updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
          .assigned(assigned + 1)
          .execute());
      updatedRole = true;

      await _service.executeProtected(
          () => _service.connector.updateShift(id: shiftId).filled(filled + 1).execute());
      updatedShift = true;
    } catch (e) {
      if (updatedShift) {
        try {
          await _service.connector.updateShift(id: shiftId).filled(filled).execute();
        } catch (_) {}
      }
      if (updatedRole) {
        try {
          await _service.connector
              .updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
              .assigned(assigned)
              .execute();
        } catch (_) {}
      }
      if (appId != null) {
        try {
          await _service.connector.deleteApplication(id: appId).execute();
        } catch (_) {}
      }
      rethrow;
    }
  }

  @override
  Future<void> acceptShift(String shiftId) async {
    await _updateApplicationStatus(shiftId, dc.ApplicationStatus.CONFIRMED);
  }

  @override
  Future<void> declineShift(String shiftId) async {
    await _updateApplicationStatus(shiftId, dc.ApplicationStatus.REJECTED);
  }

  Future<void> _updateApplicationStatus(
    String shiftId,
    dc.ApplicationStatus newStatus,
  ) async {
    String? appId = _shiftToAppIdMap[shiftId];
    String? roleId;

    if (appId == null) {
      // Try to find it in pending
      await getPendingAssignments();
    }
    // Re-check map
    appId = _shiftToAppIdMap[shiftId];
    if (appId != null) {
      roleId = _appToRoleIdMap[appId];
    } else {
      // Fallback fetch
      final staffId = await _service.getStaffId();
      final apps = await _service.executeProtected(() =>
          _service.connector.getApplicationsByStaffId(staffId: staffId).execute());
      final app = apps.data.applications
          .where((a) => a.shiftId == shiftId)
          .firstOrNull;
      if (app != null) {
        appId = app.id;
        roleId = app.shiftRole.id;
      }
    }

    if (appId == null || roleId == null) {
      // If we are rejecting and can't find an application, create one as rejected (declining an available shift)
      if (newStatus == dc.ApplicationStatus.REJECTED) {
        final rolesResult = await _service.executeProtected(() =>
            _service.connector.listShiftRolesByShiftId(shiftId: shiftId).execute());
        if (rolesResult.data.shiftRoles.isNotEmpty) {
          final role = rolesResult.data.shiftRoles.first;
          final staffId = await _service.getStaffId();
          await _service.executeProtected(() => _service.connector
              .createApplication(
                shiftId: shiftId,
                staffId: staffId,
                roleId: role.id,
                status: dc.ApplicationStatus.REJECTED,
                origin: dc.ApplicationOrigin.STAFF,
              )
              .execute());
          return;
        }
      }
      throw Exception("Application not found for shift $shiftId");
    }

    await _service.executeProtected(() => _service.connector
        .updateApplicationStatus(id: appId!)
        .status(newStatus)
        .execute());
  }
}
