import { computed, ref, type Ref, type ComputedRef } from 'vue';
import chroma from 'chroma-js';
import type { Dayjs } from 'dayjs';
import type { TimeEntry, Project, Client, Task } from '@/packages/api/src';
import { getBreakPlacementHint } from '../utils/breakPlacement';
import { getDayJsInstance, getLocalizedDayJs } from '../utils/time';
import type { CalendarSettings } from './calendarSettings';
import type { CalendarEvent, DayEvent } from './calendarTypes';

interface PositionedEvent {
    event: CalendarEvent;
    startMin: number;
    endMin: number;
    isClippedStart: boolean;
    isClippedEnd: boolean;
}

interface ColumnAssignment extends PositionedEvent {
    col: number;
}

/** Clip an event's time range to a single day and the visible hour range. */
function clipEventToDay(
    ev: CalendarEvent,
    dayStart: Dayjs,
    dayEnd: Dayjs,
    visibleStartMin: number,
    visibleEndMin: number,
    timeToMinutesFromMidnight: (time: Dayjs) => number
): PositionedEvent {
    const isClippedStart = ev.dayStart.isBefore(dayStart);
    const isClippedEnd = ev.dayEnd.isAfter(dayEnd);

    let evStartMin = isClippedStart ? 0 : timeToMinutesFromMidnight(ev.dayStart);
    let evEndMin = isClippedEnd ? 24 * 60 : timeToMinutesFromMidnight(ev.dayEnd);

    evStartMin = Math.max(evStartMin, visibleStartMin);
    evEndMin = Math.min(evEndMin, visibleEndMin);

    if (evEndMin <= evStartMin) {
        evEndMin = evStartMin + 1;
    }

    return { event: ev, startMin: evStartMin, endMin: evEndMin, isClippedStart, isClippedEnd };
}

/** Greedily assign each event to the first column where it fits without overlap. */
function assignColumns(positioned: PositionedEvent[]): ColumnAssignment[] {
    const columns: PositionedEvent[][] = [];
    const result: ColumnAssignment[] = [];

    for (const item of positioned) {
        let placed = false;
        for (let c = 0; c < columns.length; c++) {
            const lastInCol = columns[c]![columns[c]!.length - 1]!;
            if (lastInCol.endMin <= item.startMin) {
                columns[c]!.push(item);
                result.push({ ...item, col: c });
                placed = true;
                break;
            }
        }
        if (!placed) {
            columns.push([item]);
            result.push({ ...item, col: columns.length - 1 });
        }
    }

    return result;
}

/** Convert column-assigned groups into pixel-positioned DayEvent objects. */
function groupsToDayEvents(
    groups: { items: ColumnAssignment[]; totalCols: number }[],
    visibleStartMin: number,
    minutesToPixels: (minutes: number) => number
): DayEvent[] {
    const result: DayEvent[] = [];
    for (const group of groups) {
        for (const item of group.items) {
            const top = minutesToPixels(item.startMin - visibleStartMin);
            const height = minutesToPixels(item.endMin - item.startMin);
            result.push({
                event: item.event,
                top,
                height: Math.max(height, 1),
                left: `${(item.col / group.totalCols) * 100}%`,
                width: `${(1 / group.totalCols) * 100}%`,
                isClippedStart: item.isClippedStart,
                isClippedEnd: item.isClippedEnd,
            });
        }
    }
    return result;
}

/** Compute positioned events for a single day. */
function layoutDayEvents(
    dayEvents: CalendarEvent[],
    dayStart: Dayjs,
    dayEnd: Dayjs,
    visibleStartMin: number,
    visibleEndMin: number,
    timeToMinutesFromMidnight: (time: Dayjs) => number,
    minutesToPixels: (minutes: number) => number
): DayEvent[] {
    const positioned = dayEvents.map((ev) =>
        clipEventToDay(
            ev,
            dayStart,
            dayEnd,
            visibleStartMin,
            visibleEndMin,
            timeToMinutesFromMidnight
        )
    );

    // Sort: earliest start first, then longest duration first (for stable column assignment)
    positioned.sort((a, b) => {
        if (a.startMin !== b.startMin) return a.startMin - b.startMin;
        return b.endMin - b.startMin - (a.endMin - a.startMin);
    });

    const eventColumns = assignColumns(positioned);
    const groups = groupOverlappingEvents(eventColumns);
    return groupsToDayEvents(groups, visibleStartMin, minutesToPixels);
}

/** Group events that transitively overlap so each group shares column count. */
function groupOverlappingEvents(
    eventColumns: ColumnAssignment[]
): { items: ColumnAssignment[]; totalCols: number }[] {
    const groups: { items: ColumnAssignment[]; totalCols: number }[] = [];
    const assigned = new Set<number>();

    for (let i = 0; i < eventColumns.length; i++) {
        if (assigned.has(i)) continue;

        const group = [eventColumns[i]!];
        assigned.add(i);

        let expanded = true;
        while (expanded) {
            expanded = false;
            for (let j = 0; j < eventColumns.length; j++) {
                if (assigned.has(j)) continue;
                const candidate = eventColumns[j]!;
                for (const member of group) {
                    if (candidate.startMin < member.endMin && candidate.endMin > member.startMin) {
                        group.push(candidate);
                        assigned.add(j);
                        expanded = true;
                        break;
                    }
                }
            }
        }

        let maxCol = 0;
        for (const item of group) {
            if (item.col > maxCol) maxCol = item.col;
        }
        groups.push({ items: group, totalCols: maxCol + 1 });
    }

    return groups;
}

export function useCalendarEvents(params: {
    timeEntries: () => TimeEntry[];
    projects: () => Project[];
    clients: () => Client[];
    tasks: () => Task[];
    calendarSettings: Ref<CalendarSettings>;
    viewDays: ComputedRef<Dayjs[]>;
    currentTime: Ref<Dayjs>;
    cssBackground: Ref<string>;
    minutesToPixels: (minutes: number) => number;
    timeToMinutesFromMidnight: (time: Dayjs) => number;
}) {
    const optimisticOverrides = ref<Map<string, TimeEntry>>(new Map());

    const calendarEvents = computed<CalendarEvent[]>(() => {
        const themeBackground = params.cssBackground.value?.trim();
        const allEntries = params.timeEntries();
        return allEntries.map((rawEntry) => {
            const timeEntry = optimisticOverrides.value.get(rawEntry.id) || rawEntry;
            const isRunning = timeEntry.end === null;
            const project = params.projects().find((p) => p.id === timeEntry.project_id);
            const client = params.clients().find((c) => c.id === project?.client_id);
            const task = params.tasks().find((t) => t.id === timeEntry.task_id);

            const effectiveEnd = isRunning
                ? params.currentTime.value
                : getDayJsInstance()(timeEntry.end!);
            const durationMinutes = effectiveEnd.diff(
                getDayJsInstance()(timeEntry.start),
                'minutes'
            );

            const isBreak = timeEntry.type === 'break';
            const isMisplacedBreak = isBreak
                ? (getBreakPlacementHint(timeEntry, allEntries)?.misplaced ?? false)
                : false;
            let title: string;
            if (isBreak) {
                title = timeEntry.description ? `Break · ${timeEntry.description}` : 'Break';
            } else {
                title = timeEntry.description || 'No description';
            }
            const baseColor = isBreak ? '#F59E0B' : project?.color || '#6B7280';
            const backgroundColor = chroma
                .mix(baseColor, themeBackground, isBreak ? 0.75 : 0.65, 'lab')
                .hex();
            const borderColor = chroma.mix(baseColor, themeBackground, 0.5, 'lab').hex();

            const startTime = getLocalizedDayJs(timeEntry.start);
            const endTime = isRunning
                ? getLocalizedDayJs(params.currentTime.value.toISOString())
                : durationMinutes === 0
                  ? startTime.add(1, 'second')
                  : getLocalizedDayJs(timeEntry.end!);

            return {
                id: timeEntry.id,
                timeEntry,
                project,
                client,
                task,
                isRunning,
                isBreak,
                isMisplacedBreak,
                durationMinutes,
                title,
                backgroundColor,
                borderColor,
                dayStart: startTime,
                dayEnd: endTime,
            };
        });
    });

    const eventsByDay = computed(() => {
        const s = params.calendarSettings.value;
        const visibleStartMin = s.startHour * 60;
        const visibleEndMin = s.endHour * 60;
        const result: Record<string, DayEvent[]> = {};

        for (const day of params.viewDays.value) {
            const dayStart = day.startOf('day');
            const dayEnd = day.endOf('day');

            const dayEvents = calendarEvents.value.filter(
                (ev) => ev.dayStart.isBefore(dayEnd) && ev.dayEnd.isAfter(dayStart)
            );

            result[day.format('YYYY-MM-DD')] = layoutDayEvents(
                dayEvents,
                dayStart,
                dayEnd,
                visibleStartMin,
                visibleEndMin,
                params.timeToMinutesFromMidnight,
                params.minutesToPixels
            );
        }

        return result;
    });

    function computeDailyTotals(filter: (entry: TimeEntry) => boolean): Record<string, number> {
        const totals: Record<string, number> = {};
        params
            .timeEntries()
            .filter(filter)
            .forEach((entry) => {
                const date = getLocalizedDayJs(entry.start).format('YYYY-MM-DD');
                let durationSeconds: number;

                if (entry.end !== null) {
                    durationSeconds = getDayJsInstance()(entry.end).diff(
                        getDayJsInstance()(entry.start),
                        'seconds'
                    );
                } else {
                    durationSeconds = Math.max(
                        0,
                        params.currentTime.value.diff(getDayJsInstance()(entry.start), 'seconds')
                    );
                }

                totals[date] = (totals[date] || 0) + durationSeconds;
            });
        return totals;
    }

    // Breaks are not working time: the day total only sums work entries,
    // the break portion is exposed separately
    const dailyTotals = computed(() => computeDailyTotals((entry) => entry.type !== 'break'));

    const dailyBreakTotals = computed(() => computeDailyTotals((entry) => entry.type === 'break'));

    function isToday(day: Dayjs): boolean {
        return day.isSame(getLocalizedDayJs(), 'day');
    }

    const nowIndicatorTop = computed(() => {
        const s = params.calendarSettings.value;
        const now = getLocalizedDayJs(params.currentTime.value.toISOString());
        const minutesFromMidnight = now.hour() * 60 + now.minute();
        const startMin = s.startHour * 60;
        if (minutesFromMidnight < startMin || minutesFromMidnight >= s.endHour * 60) return -1;
        return params.minutesToPixels(minutesFromMidnight - startMin);
    });

    return {
        optimisticOverrides,
        calendarEvents,
        eventsByDay,
        dailyTotals,
        dailyBreakTotals,
        isToday,
        nowIndicatorTop,
    };
}
