import { DateTime, Duration, Interval } from 'luxon';

import { TimeSlot } from '../models/hourCategorization';
import { PairedEntry } from '../models/PairedEntry';

const MIN_PAIRED_ENTRY_DIFFERENCE_MINUTES = 1;

/**
 * Converts paired clock-in/out entries into Luxon Intervals.
 * Pairs without both start and end, or shorter than 1 minute, are skipped.
 */
export function pairedEntriesToIntervals(pairedEntries: PairedEntry[], timezone: string): Interval[] {
    const intervals: Interval[] = [];

    for (const pair of pairedEntries) {
        if (!pair.start || !pair.end) continue;

        const start = DateTime.fromJSDate(pair.start.time).setZone(timezone);
        const end = DateTime.fromJSDate(pair.end.time).setZone(timezone);

        if (end.diff(start, 'minutes').minutes < MIN_PAIRED_ENTRY_DIFFERENCE_MINUTES) {
            continue;
        }

        intervals.push(Interval.fromDateTimes(start, end));
    }

    return Interval.merge(intervals);
}

/**
 * Converts a HH:MM time slot (possibly cross-midnight) into Intervals
 * anchored to the reference date's day in the given timezone.
 *
 * A cross-midnight slot like ["21:00", "06:00"] produces two intervals:
 *   - referenceDay 00:00 → referenceDay 06:00
 *   - referenceDay 21:00 → referenceDay+1 06:00
 */
export function timeSlotToIntervals(timeSlot: TimeSlot, referenceDate: DateTime, timezone: string): Interval[] {
    const dayStart = referenceDate.setZone(timezone).startOf('day');

    const [startHour, startMinute] = timeSlot[0].split(':').map(Number);
    const [endHour, endMinute] = timeSlot[1].split(':').map(Number);

    const slotStartMin = startHour * 60 + startMinute;
    const slotEndMin = endHour * 60 + endMinute;

    if (slotEndMin <= slotStartMin) {
        const todayMidnight = dayStart;
        const todaySlotEnd = dayStart.plus({ minutes: slotEndMin });
        const todaySlotStart = dayStart.plus({ minutes: slotStartMin });
        const tomorrowSlotEnd = dayStart.plus({ days: 1, minutes: slotEndMin });

        return [Interval.fromDateTimes(todayMidnight, todaySlotEnd), Interval.fromDateTimes(todaySlotStart, tomorrowSlotEnd)];
    }

    return [Interval.fromDateTimes(dayStart.plus({ minutes: slotStartMin }), dayStart.plus({ minutes: slotEndMin }))];
}

/**
 * Intersects two sets of intervals, returning only the overlapping portions.
 * Equivalent to AND logic: time must be in BOTH sets.
 */
export function intersectIntervalSets(setA: Interval[], setB: Interval[]): Interval[] {
    const result: Interval[] = [];

    for (const a of setA) {
        for (const b of setB) {
            const inter = a.intersection(b);
            if (inter && inter.length('minutes') > 0) {
                result.push(inter);
            }
        }
    }

    return Interval.merge(result);
}

/**
 * Computes the set difference: portions of setA NOT covered by setB.
 * Used for NOT_EQUALS time range conditions.
 */
export function differenceIntervalSets(setA: Interval[], setB: Interval[]): Interval[] {
    let result = [...setA];

    for (const b of setB) {
        const next: Interval[] = [];
        for (const r of result) {
            next.push(...r.difference(b));
        }
        result = next;
    }

    return Interval.merge(result);
}

/**
 * Removes the first `duration` of chronological time from the intervals.
 * Returns the remaining tail intervals.
 * Used for GREATER_THAN threshold: only excess beyond threshold counts.
 */
export function trimFromStart(intervals: Interval[], duration: Duration): Interval[] {
    const sorted = Interval.merge(intervals);
    let remainingMs = duration.as('milliseconds');
    const result: Interval[] = [];

    for (const interval of sorted) {
        if (remainingMs <= 0) {
            result.push(interval);
            continue;
        }

        const lengthMs = interval.length('milliseconds');
        if (remainingMs >= lengthMs) {
            remainingMs -= lengthMs;
            continue;
        }

        const trimmed = Interval.fromDateTimes(interval.start!.plus(remainingMs), interval.end!);
        remainingMs = 0;
        result.push(trimmed);
    }

    return result;
}

/**
 * Keeps only the first `duration` of chronological time from the intervals.
 * Trims everything after.
 * Used for LESS_THAN threshold: only time up to threshold counts.
 */
export function keepFromStart(intervals: Interval[], duration: Duration): Interval[] {
    const sorted = Interval.merge(intervals);
    let remainingMs = duration.as('milliseconds');
    const result: Interval[] = [];

    for (const interval of sorted) {
        if (remainingMs <= 0) break;

        const lengthMs = interval.length('milliseconds');
        if (remainingMs >= lengthMs) {
            result.push(interval);
            remainingMs -= lengthMs;
            continue;
        }

        const kept = Interval.fromDateTimes(interval.start!, interval.start!.plus(remainingMs));
        remainingMs = 0;
        result.push(kept);
    }

    return result;
}

/**
 * Sums the total duration in hours across all intervals.
 */
export function totalDurationInHours(intervals: Interval[]): number {
    return intervals.reduce((sum, interval) => sum + interval.length('hours'), 0);
}

/**
 * Returns the longer of two Durations (by absolute milliseconds).
 * Accepts nulls: a null value is treated as "no value yet" and the other wins.
 */
export function maxDuration(a: Duration | null, b: Duration): Duration {
    if (a === null) return b;
    return b.as('milliseconds') > a.as('milliseconds') ? b : a;
}

/**
 * Returns the shorter of two Durations (by absolute milliseconds).
 * Accepts nulls: a null value is treated as "no value yet" and the other wins.
 */
export function minDuration(a: Duration | null, b: Duration): Duration {
    if (a === null) return b;
    return b.as('milliseconds') < a.as('milliseconds') ? b : a;
}
