import Foundation

extension Date {
    // MARK: - Formatting

    var shortDateString: String {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .none
        return formatter.string(from: self)
    }

    var mediumDateString: String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .none
        return formatter.string(from: self)
    }

    var longDateString: String {
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        formatter.timeStyle = .none
        return formatter.string(from: self)
    }

    var monthYearString: String {
        let formatter = DateFormatter()
        formatter.dateFormat = "MMM yyyy"
        return formatter.string(from: self)
    }

    var yearString: String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy"
        return formatter.string(from: self)
    }

    var iso8601String: String {
        ISO8601DateFormatter().string(from: self)
    }

    // MARK: - Components

    var startOfDay: Date {
        Calendar.current.startOfDay(for: self)
    }

    var startOfMonth: Date {
        let components = Calendar.current.dateComponents([.year, .month], from: self)
        return Calendar.current.date(from: components) ?? self
    }

    var startOfYear: Date {
        let components = Calendar.current.dateComponents([.year], from: self)
        return Calendar.current.date(from: components) ?? self
    }

    var endOfMonth: Date {
        let startOfNextMonth = Calendar.current.date(
            byAdding: .month,
            value: 1,
            to: startOfMonth
        ) ?? self
        return Calendar.current.date(byAdding: .day, value: -1, to: startOfNextMonth) ?? self
    }

    // MARK: - Comparisons

    func isSameDay(as other: Date) -> Bool {
        Calendar.current.isDate(self, inSameDayAs: other)
    }

    func isSameMonth(as other: Date) -> Bool {
        let selfComponents = Calendar.current.dateComponents([.year, .month], from: self)
        let otherComponents = Calendar.current.dateComponents([.year, .month], from: other)
        return selfComponents.year == otherComponents.year &&
               selfComponents.month == otherComponents.month
    }

    func isSameYear(as other: Date) -> Bool {
        Calendar.current.component(.year, from: self) ==
            Calendar.current.component(.year, from: other)
    }

    var isToday: Bool {
        Calendar.current.isDateInToday(self)
    }

    var isYesterday: Bool {
        Calendar.current.isDateInYesterday(self)
    }

    var isThisMonth: Bool {
        isSameMonth(as: Date())
    }

    var isThisYear: Bool {
        isSameYear(as: Date())
    }

    // MARK: - Calculations

    func adding(days: Int) -> Date {
        Calendar.current.date(byAdding: .day, value: days, to: self) ?? self
    }

    func adding(months: Int) -> Date {
        Calendar.current.date(byAdding: .month, value: months, to: self) ?? self
    }

    func adding(years: Int) -> Date {
        Calendar.current.date(byAdding: .year, value: years, to: self) ?? self
    }

    func monthsBetween(_ other: Date) -> Int {
        let components = Calendar.current.dateComponents([.month], from: self, to: other)
        return components.month ?? 0
    }

    func daysBetween(_ other: Date) -> Int {
        let components = Calendar.current.dateComponents([.day], from: self, to: other)
        return components.day ?? 0
    }

    func yearsBetween(_ other: Date) -> Double {
        let days = Double(daysBetween(other))
        return days / 365.25
    }

    // MARK: - Relative Description

    var relativeDescription: String {
        let formatter = RelativeDateTimeFormatter()
        formatter.unitsStyle = .abbreviated
        return formatter.localizedString(for: self, relativeTo: Date())
    }

    var relativeDayDescription: String {
        let calendar = Calendar.current
        let days = calendar.dateComponents([.day], from: calendar.startOfDay(for: self), to: calendar.startOfDay(for: Date())).day ?? 0
        if days == 0 { return String(localized: "date_today") }
        if days == 1 { return "1d ago" }
        if days < 31 { return "\(days)d ago" }
        let months = calendar.dateComponents([.month], from: self, to: Date()).month ?? 0
        if months < 12 { return "\(max(1, months))mo ago" }
        let years = calendar.dateComponents([.year], from: self, to: Date()).year ?? 0
        return "\(max(1, years))y ago"
    }

    var friendlyDescription: String {
        if isToday {
            return String(localized: "date_today")
        } else if isYesterday {
            return String(localized: "date_yesterday")
        } else if isThisMonth {
            let formatter = DateFormatter()
            formatter.dateFormat = "EEEE, d"
            return formatter.string(from: self)
        } else if isThisYear {
            let formatter = DateFormatter()
            formatter.dateFormat = "MMM d"
            return formatter.string(from: self)
        } else {
            return mediumDateString
        }
    }
}

// MARK: - Date Range

struct DateRange {
    let start: Date
    let end: Date

    static var thisMonth: DateRange {
        let now = Date()
        return DateRange(start: now.startOfMonth, end: now)
    }

    static var lastMonth: DateRange {
        let now = Date()
        let lastMonth = now.adding(months: -1)
        return DateRange(start: lastMonth.startOfMonth, end: lastMonth.endOfMonth)
    }

    static var thisYear: DateRange {
        let now = Date()
        return DateRange(start: now.startOfYear, end: now)
    }

    static var lastYear: DateRange {
        let now = Date()
        let lastYear = now.adding(years: -1)
        return DateRange(start: lastYear.startOfYear, end: now.startOfYear.adding(days: -1))
    }

    static func month(containing date: Date) -> DateRange {
        DateRange(start: date.startOfMonth, end: date.endOfMonth)
    }

    static func last(months: Int) -> DateRange {
        let now = Date()
        let start = now.adding(months: -months)
        return DateRange(start: start, end: now)
    }

    func contains(_ date: Date) -> Bool {
        date >= start && date <= end
    }
}
