import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';

import '../../domain/repositories/clock_in_repository_interface.dart';

/// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect.
class ClockInRepositoryImpl implements ClockInRepositoryInterface {
  ClockInRepositoryImpl() : _service = dc.DataConnectService.instance;

  final dc.DataConnectService _service;
  final Map<String, String> _shiftToApplicationId = <String, String>{};
  String? _activeApplicationId;

  ({fdc.Timestamp start, fdc.Timestamp end}) _utcDayRange(DateTime localDay) {
    final DateTime dayStartUtc = DateTime.utc(
      localDay.year,
      localDay.month,
      localDay.day,
    );
    final DateTime dayEndUtc = DateTime.utc(
      localDay.year,
      localDay.month,
      localDay.day,
      23,
      59,
      59,
      999,
      999,
    );
    return (
      start: _service.toTimestamp(dayStartUtc),
      end: _service.toTimestamp(dayEndUtc),
    );
  }

  /// Helper to find today's applications ordered with the closest at the end.
  Future<List<dc.GetApplicationsByStaffIdApplications>> _getTodaysApplications(
    String staffId,
  ) async {
    final DateTime now = DateTime.now();
    final ({fdc.Timestamp start, fdc.Timestamp end}) range = _utcDayRange(now);
    final fdc.QueryResult<dc.GetApplicationsByStaffIdData,
        dc.GetApplicationsByStaffIdVariables> result = await _service.run(
      () => _service.connector
          .getApplicationsByStaffId(staffId: staffId)
          .dayStart(range.start)
          .dayEnd(range.end)
          .execute(),
    );

    final List<dc.GetApplicationsByStaffIdApplications> apps =
        result.data.applications;
    if (apps.isEmpty) return const <dc.GetApplicationsByStaffIdApplications>[];

    _shiftToApplicationId
      ..clear()
      ..addEntries(apps.map((dc.GetApplicationsByStaffIdApplications app) =>
          MapEntry<String, String>(app.shiftId, app.id)));

    apps.sort((dc.GetApplicationsByStaffIdApplications a,
        dc.GetApplicationsByStaffIdApplications b) {
      final DateTime? aTime =
          _service.toDateTime(a.shift.startTime) ?? _service.toDateTime(a.shift.date);
      final DateTime? bTime =
          _service.toDateTime(b.shift.startTime) ?? _service.toDateTime(b.shift.date);
      if (aTime == null && bTime == null) return 0;
      if (aTime == null) return -1;
      if (bTime == null) return 1;
      final Duration aDiff = aTime.difference(now).abs();
      final Duration bDiff = bTime.difference(now).abs();
      return bDiff.compareTo(aDiff); // closest at the end
    });

    return apps;
  }

  @override
  Future<List<Shift>> getTodaysShifts() async {
    return _service.run(() async {
      final String staffId = await _service.getStaffId();
      final List<dc.GetApplicationsByStaffIdApplications> apps =
          await _getTodaysApplications(staffId);
      if (apps.isEmpty) return const <Shift>[];

      final List<Shift> shifts = <Shift>[];
      for (final dc.GetApplicationsByStaffIdApplications app in apps) {
        final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift;
        final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
        final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
        final DateTime? createdDt = _service.toDateTime(app.createdAt);

        final String roleName = app.shiftRole.role.name;
        final String orderName =
            (shift.order.eventName ?? '').trim().isNotEmpty
                ? shift.order.eventName!
                : shift.order.business.businessName;
        final String title = '$roleName - $orderName';
        shifts.add(
          Shift(
            id: shift.id,
            title: title,
            clientName: shift.order.business.businessName,
            logoUrl: shift.order.business.companyLogoUrl ?? '',
            hourlyRate: app.shiftRole.role.costPerHour,
            location: shift.location ?? '',
            locationAddress: shift.order.teamHub.hubName,
            date: startDt?.toIso8601String() ?? '',
            startTime: startDt?.toIso8601String() ?? '',
            endTime: endDt?.toIso8601String() ?? '',
            createdDate: createdDt?.toIso8601String() ?? '',
            status: shift.status?.stringValue,
            description: shift.description,
            latitude: shift.latitude,
            longitude: shift.longitude,
          ),
        );
      }

      return shifts;
    });
  }

  @override
  Future<AttendanceStatus> getAttendanceStatus() async {
    return _service.run(() async {
      final String staffId = await _service.getStaffId();
      final List<dc.GetApplicationsByStaffIdApplications> apps =
          await _getTodaysApplications(staffId);
      if (apps.isEmpty) {
        return const AttendanceStatus(isCheckedIn: false);
      }

      dc.GetApplicationsByStaffIdApplications? activeApp;
      for (final dc.GetApplicationsByStaffIdApplications app in apps) {
        if (app.checkInTime != null && app.checkOutTime == null) {
          if (activeApp == null) {
            activeApp = app;
          } else {
            final DateTime? current = _service.toDateTime(activeApp.checkInTime);
            final DateTime? next = _service.toDateTime(app.checkInTime);
            if (current == null || (next != null && next.isAfter(current))) {
              activeApp = app;
            }
          }
        }
      }

      if (activeApp == null) {
        _activeApplicationId = null;
        return const AttendanceStatus(isCheckedIn: false);
      }

      _activeApplicationId = activeApp.id;

      return AttendanceStatus(
        isCheckedIn: true,
        checkInTime: _service.toDateTime(activeApp.checkInTime),
        checkOutTime: _service.toDateTime(activeApp.checkOutTime),
        activeShiftId: activeApp.shiftId,
        activeApplicationId: activeApp.id,
      );
    });
  }

  @override
  Future<AttendanceStatus> clockIn({required String shiftId, String? notes}) async {
    return _service.run(() async {
      final String staffId = await _service.getStaffId();

      final String? cachedAppId = _shiftToApplicationId[shiftId];
      dc.GetApplicationsByStaffIdApplications? app;
      if (cachedAppId != null) {
        try {
          final List<dc.GetApplicationsByStaffIdApplications> apps =
              await _getTodaysApplications(staffId);
          app = apps.firstWhere(
              (dc.GetApplicationsByStaffIdApplications a) => a.id == cachedAppId);
        } catch (_) {}
      }
      app ??= (await _getTodaysApplications(staffId)).firstWhere(
          (dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId);

      final fdc.Timestamp checkInTs = _service.toTimestamp(DateTime.now());

      await _service.run(() => _service.connector
          .updateApplicationStatus(
            id: app!.id,
          )
          .checkInTime(checkInTs)
          .execute());
      _activeApplicationId = app.id;

      return getAttendanceStatus();
    });
  }

  @override
  Future<AttendanceStatus> clockOut({
    String? notes,
    int? breakTimeMinutes,
    String? applicationId,
  }) async {
    return _service.run(() async {
      await _service.getStaffId(); // Validate session

      final String? targetAppId = applicationId ?? _activeApplicationId;
      if (targetAppId == null || targetAppId.isEmpty) {
        throw Exception('No active application id for checkout');
      }
      final fdc.QueryResult<dc.GetApplicationByIdData,
              dc.GetApplicationByIdVariables> appResult =
          await _service.run(() => _service.connector
              .getApplicationById(id: targetAppId)
              .execute());
      final dc.GetApplicationByIdApplication? app = appResult.data.application;

      if (app == null) {
        throw Exception('Application not found for checkout');
      }
      if (app.checkInTime == null || app.checkOutTime != null) {
        throw Exception('No active shift found to clock out');
      }

      await _service.run(() => _service.connector
          .updateApplicationStatus(
            id: targetAppId,
          )
          .checkOutTime(_service.toTimestamp(DateTime.now()))
          .execute());

      return getAttendanceStatus();
    });
  }
}
