// SPDX-FileCopyrightText: © 2025 Nøkken.io <nokken.io@proton.me>
// SPDX-License-Identifier: AGPL-3.0
//
//  date_time_formatter.dart
//  A utility class for formatting dates, times, and related display elements
//

import 'package:flutter/material.dart';
import 'package:nokken/src/features/medication_tracker/models/medication.dart';
import 'package:nokken/src/core/constants/date_constants.dart';
import 'package:nokken/src/core/ui/theme/app_icons.dart';

class DateTimeFormatter {
  //----------------------------------------------------------------------------
  // DATE FORMATTING FUNCTIONS
  //----------------------------------------------------------------------------

  static String getDaysAgo(DateTime? date) {
    if (date == null) return 'N/A';

    final now = DateTime.now();
    final difference = now.difference(date);

    if (difference.inDays == 0) {
      return 'Today';
    } else if (difference.inDays == 1) {
      return 'Yesterday';
    } else {
      return '${difference.inDays} days ago';
    }
  }

  /// Formats a date as "Month Day, Year" with special handling for today,
  /// tomorrow and yesterday.
  ///
  /// Examples:
  /// - Today - Jan 1, 2025
  /// - Tomorrow - Jan 2, 2025
  /// - Jan 15, 2025
  static String formatDateMMMDDYYYY(DateTime date) {
    final now = DateTime.now();

    /// Helper function to create the base date string
    String formatDateStringMMMDDYYYY() {
      return '${DateConstants.months[date.month - 1]} ${date.day}, ${date.year}';
    }

    if (date.year == now.year &&
        date.month == now.month &&
        date.day == now.day) {
      return 'Today - ${formatDateStringMMMDDYYYY()}';
    } else if (date.year == now.year &&
        date.month == now.month &&
        date.day == now.day + 1) {
      return 'Tomorrow - ${formatDateStringMMMDDYYYY()}';
    } else if (date.year == now.year &&
        date.month == now.month &&
        date.day == now.day - 1) {
      return 'Yesterday - ${formatDateStringMMMDDYYYY()}';
    }

    return formatDateStringMMMDDYYYY();
  }

  /// Formats a date in MM/DD/YYYY format
  ///
  /// Example: 2/26/2025
  static String formatDateDDMMYY(DateTime date) {
    return '${date.month}/${date.day}/${date.year}';
  }

  /// Formats a set of days of the week into a human-readable string
  ///
  /// If all 7 days are included, returns "Everyday"
  /// Otherwise, returns a comma-separated list of day names
  static String formatDaysOfWeek(Set<String> days) {
    if (days.length == 7 && DateConstants.orderedDays.every(days.contains)) {
      return 'Everyday';
    }

    final sortedDays = days.toList()
      ..sort((a, b) => DateConstants.orderedDays
          .indexOf(a)
          .compareTo(DateConstants.orderedDays.indexOf(b)));

    return sortedDays.map((day) => DateConstants.dayNames[day]).join(', ');
  }

  //----------------------------------------------------------------------------
  // MEDICATION-SPECIFIC FORMATTING
  //----------------------------------------------------------------------------

  /// Returns a string describing the frequency of a medication
  /// Accounts for medication type and frequency and weekly vs. biweekly
  static String formatMedicationFrequencyDosage(Medication medication) {
    // Handle as-needed medications first
    if (medication.asNeeded) {
      switch (medication.medicationType) {
        case MedicationType.oral:
          return 'Take ${medication.dosage} as needed';
        case MedicationType.injection:
          return 'Inject ${medication.dosage} as needed';
        case MedicationType.topical:
          return 'Apply ${medication.dosage} as needed';
        case MedicationType.patch:
          return 'Change ${medication.name} patch, ${medication.dosage}, as needed';
      }
    }

    // Format frequency text (once/twice/three times)
    String frequencyText = medication.frequency == 1
        ? 'once'
        : medication.frequency == 2
            ? 'twice'
            : '${medication.frequency} times';

    // Handle different medication types
    switch (medication.medicationType) {
      case MedicationType.oral:
        if (medication.daysOfWeek.isEmpty ||
            medication.daysOfWeek.length == 7) {
          return 'Take ${medication.dosage} $frequencyText daily, everyday';
        } else {
          return 'Take ${medication.dosage} $frequencyText daily, on ${medication.daysOfWeek.join(', ')}';
        }

      case MedicationType.injection:
        // Add this condition for biweekly injections
        if (medication.injectionDetails?.frequency ==
            InjectionFrequency.biweekly) {
          return 'Inject ${medication.dosage} every 2 weeks on ${medication.daysOfWeek.join(', ')}';
        }
        return 'Inject ${medication.dosage} every week on ${medication.daysOfWeek.join(', ')}';

      case MedicationType.topical:
        if (medication.daysOfWeek.isEmpty ||
            medication.daysOfWeek.length == 7) {
          return 'Apply ${medication.dosage} $frequencyText daily, everyday';
        } else {
          return 'Apply ${medication.dosage} $frequencyText daily, on ${medication.daysOfWeek.join(', ')}';
        }

      case MedicationType.patch:
        if (medication.daysOfWeek.isEmpty ||
            medication.daysOfWeek.length == 7) {
          return 'Change ${medication.name} patch, ${medication.dosage}, $frequencyText daily, everyday';
        } else {
          return 'Change ${medication.name} patch, ${medication.dosage}, $frequencyText on ${medication.daysOfWeek.join(', ')}';
        }
    }
  }

  /// Returns a string describing the frequency of a medication
  static String formatMedicationFrequency(Medication medication) {
    // Handle as-needed medications
    if (medication.asNeeded) {
      return 'As Needed';
    }

    if (medication.medicationType == MedicationType.injection) {
      if (medication.injectionDetails?.frequency ==
          InjectionFrequency.biweekly) {
        return 'every 2 weeks';
      }
      return 'every week';
    } else if (medication.medicationType == MedicationType.patch) {
      String frequencyText = medication.frequency == 1
          ? 'once'
          : medication.frequency == 2
              ? 'twice'
              : '${medication.frequency} times';

      return 'change $frequencyText a day';
    }

    String frequencyText = medication.frequency == 1
        ? 'once'
        : medication.frequency == 2
            ? 'twice'
            : '${medication.frequency} times';

    return '$frequencyText a day';
  }

  //----------------------------------------------------------------------------
  // TIME PARSING AND FORMATTING
  //----------------------------------------------------------------------------

  /// Parses a time string in various formats (like "8:30 AM") to a TimeOfDay object
  ///
  /// Handles both 12-hour and 24-hour formats
  ///
  /// @param timeStr The time string to parse
  /// @return A TimeOfDay representation of the input string
  static TimeOfDay parseTimeString(String timeStr) {
    final isPM = timeStr.toLowerCase().contains('pm');
    final cleanTime =
        timeStr.toLowerCase().replaceAll(RegExp(r'[ap]m'), '').trim();

    final parts = cleanTime.split(':');
    if (parts.length != 2) return const TimeOfDay(hour: 0, minute: 0);

    var hour = int.tryParse(parts[0]) ?? 0;
    final minute = int.tryParse(parts[1]) ?? 0;

    if (isPM && hour != 12) hour += 12;
    if (!isPM && hour == 12) hour = 0;

    return TimeOfDay(
      hour: hour.clamp(0, 23),
      minute: minute.clamp(0, 59),
    );
  }

  /// Formats a TimeOfDay object to a 12-hour AM/PM time string
  ///
  /// @param time The TimeOfDay object to format
  /// @return A formatted string like "8:30 AM"
  static String formatTimeToAMPM(TimeOfDay time) {
    final int hour = time.hourOfPeriod == 0 ? 12 : time.hourOfPeriod;
    final String minute = time.minute.toString().padLeft(2, '0');
    final String period = time.period == DayPeriod.am ? 'AM' : 'PM';
    return '$hour:$minute $period';
  }

  /// Compares two time strings for sorting
  ///
  /// @param a First time string (e.g., "8:30 AM")
  /// @param b Second time string (e.g., "2:45 PM")
  /// @return A negative value if a is earlier than b,
  ///         positive if a is later than b,
  ///         zero if they are the same time
  static int compareTimeSlots(String a, String b) {
    final timeA = parseTimeString(a);
    final timeB = parseTimeString(b);
    return timeA.hour * 60 + timeA.minute - (timeB.hour * 60 + timeB.minute);
  }

  //----------------------------------------------------------------------------
  // TIME ICON SELECTION
  //----------------------------------------------------------------------------

  /// Returns an appropriate icon based on the time of day
  ///
  /// Time ranges:
  /// - 5:00-8:59 AM: twilight icon (dawn)
  /// - 9:00 AM-4:59 PM: sun icon (daytime)
  /// - 5:00-8:59 PM: twilight icon (dusk)
  /// - 9:00 PM-4:59 AM: moon icon (noon)
  ///
  /// @param timeOfDay A TimeOfDay object
  /// @return An IconData object for the corresponding time period
  static IconData getTimeIconFromTimeOfDay(TimeOfDay timeOfDay) {
    final hour = timeOfDay.hour;

    if (hour >= 5 && hour < 9) {
      return AppIcons.getFilled('twilight');
    } else if (hour >= 9 && hour < 17) {
      return AppIcons.getFilled('sun');
    } else if (hour >= 17 && hour < 21) {
      return AppIcons.getFilled('twilight');
    } else {
      return AppIcons.getFilled('night');
    }
  }

  /// Gets an icon appropriate for the given time string
  ///
  /// @param timeSlot A string representation of time (e.g., "8:30 AM")
  /// @return An IconData object for the corresponding time period
  static IconData getTimeIcon(String timeSlot) {
    final timeOfDay = parseTimeString(timeSlot);
    return getTimeIconFromTimeOfDay(timeOfDay);
  }

  /// Normalizes a DateTime by removing the time component
  ///
  /// Returns a new DateTime with only year, month, and day preserved
  static DateTime normalizeDate(DateTime date) {
    return DateTime(date.year, date.month, date.day);
  }

  /// Checks if two dates are the same day (ignoring time)
  ///
  /// Returns true if both dates have the same year, month, and day
  static bool isSameDay(DateTime date1, DateTime date2) {
    return date1.year == date2.year &&
        date1.month == date2.month &&
        date1.day == date2.day;
  }

  /// Efficiently generates a date range between two dates
  ///
  /// Returns an Iterable of DateTime objects from start to end inclusive
  static Iterable<DateTime> dateRange(DateTime start, DateTime end) sync* {
    var current = normalizeDate(start);
    final endDate = normalizeDate(end);

    while (!current.isAfter(endDate)) {
      yield current;
      current = current.add(const Duration(days: 1));
    }
  }

  /// Creates a cached date range for the visible calendar period
  ///
  /// Returns dates for the current month plus padding weeks, optimizing
  /// for calendar view performance
  static Iterable<DateTime> visibleCalendarRange(DateTime focusedMonth) {
    // Get first day of the month
    final firstDay = DateTime(focusedMonth.year, focusedMonth.month, 1);

    // Get first day of calendar grid (may be in previous month)
    // Calendar usually shows from Sunday before the 1st to complete the grid
    final int firstWeekday = firstDay.weekday % 7;
    final startDate = firstDay.subtract(Duration(days: firstWeekday));

    // Get last day of month
    final lastDay = DateTime(focusedMonth.year, focusedMonth.month + 1, 0);

    // Get last day of calendar grid (may be in next month)
    // Calendar usually shows 6 weeks, so add days to make a complete grid
    final int lastWeekday = lastDay.weekday % 7;
    final endDate = lastDay.add(Duration(days: 6 - lastWeekday));

    return dateRange(startDate, endDate);
  }
}
