import {
  getSettings, updateSettings as updateSettingsRow,
  listHolidays, findHolidayByDate, findHolidayById, insertHoliday, updateHoliday as updateHolidayRow, removeHoliday,
} from '../repositories/officeCalendar.repo.js';
import { createWithId } from '../utils/id.js';
import { NotFoundError, ValidationError } from '../utils/errors.js';
import { emitter } from '../events/emitter.js';
import type { OfficeSettings, HolidayEntry } from '../types/index.js';

// In-memory cache, same pattern as stage.service.ts's stage-list cache —
// officeMinutesElapsed() below runs per idle order line on hot dashboard/
// orders paths, so it needs to be synchronous rather than a DB round trip
// per row. Refreshed on every settings/holiday mutation and at startup.
let settingsCache: OfficeSettings = { startTime: '09:00:00', endTime: '21:00:00', weeklyOffDay: 0 };
let holidaysCache: Map<string, HolidayEntry> = new Map();

export async function refreshCache(): Promise<void> {
  const [settings, holidays] = await Promise.all([getSettings(), listHolidays()]);
  settingsCache = settings;
  holidaysCache = new Map(holidays.map((h) => [h.date, h]));
}

export function getCachedSettings(): OfficeSettings {
  return settingsCache;
}

export function getCachedHolidays(): HolidayEntry[] {
  return [...holidaysCache.values()].sort((a, b) => a.date.localeCompare(b.date));
}

function pad(n: number): string {
  return String(n).padStart(2, '0');
}

function formatDate(d: Date): string {
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}

function timeStrToParts(t: string): [number, number, number] {
  const [h, m, s] = t.split(':').map(Number);
  return [h ?? 0, m ?? 0, s ?? 0];
}

function atTime(day: Date, timeStr: string): Date {
  const [h, m, s] = timeStrToParts(timeStr);
  const d = new Date(day.getFullYear(), day.getMonth(), day.getDate(), h, m, s);
  return d;
}

// Which part of this calendar day counts as "office open" — null if the
// whole day is closed (weekly off or a full holiday). A holiday_calendar
// entry always overrides the weekly-off day, in both directions:
// 'holiday'/'half_day' close/shorten an otherwise-open day, while
// 'active_day' forces a normally closed weekly-off day (e.g. one specific
// Sunday that staff actually worked) to run full office hours — every
// *other* occurrence of that weekday stays closed as usual, only the
// marked date changes.
function getDayWindow(day: Date, dateStr: string): { start: string; end: string } | null {
  const holiday = holidaysCache.get(dateStr);
  if (holiday) {
    if (holiday.type === 'holiday') return null;
    if (holiday.type === 'active_day') return { start: settingsCache.startTime, end: settingsCache.endTime };
    return { start: holiday.halfDayStart!, end: holiday.halfDayEnd! };
  }
  if (day.getDay() === settingsCache.weeklyOffDay) return null;
  return { start: settingsCache.startTime, end: settingsCache.endTime };
}

// Core engine: converts a wall-clock [enteredAt, now) span into office
// minutes — i.e. only the portion that falls inside office hours on
// working days, excluding the weekly off day and any holiday_calendar
// dates (half-days contribute only their reduced window). This is what
// fixes the original bug: an order that enters a stage at 8:55 PM and is
// still there at 9:05 AM the next day is 10 minutes idle, not ~13 hours,
// because the entire 9 PM–9 AM closed window contributes 0.
export function officeMinutesElapsed(enteredAt: Date, now: Date): number {
  if (now <= enteredAt) return 0;

  let total = 0;
  const cursor = new Date(enteredAt.getFullYear(), enteredAt.getMonth(), enteredAt.getDate());
  const lastDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());

  while (cursor <= lastDay) {
    const dateStr = formatDate(cursor);
    const window = getDayWindow(cursor, dateStr);
    if (window) {
      const dayStart = atTime(cursor, window.start);
      const dayEnd = atTime(cursor, window.end);
      const overlapStart = dayStart > enteredAt ? dayStart : enteredAt;
      const overlapEnd = dayEnd < now ? dayEnd : now;
      if (overlapEnd > overlapStart) total += (overlapEnd.getTime() - overlapStart.getTime()) / 60_000;
    }
    cursor.setDate(cursor.getDate() + 1);
  }
  return total;
}

export async function getSettingsAndHolidays(): Promise<{ settings: OfficeSettings; holidays: HolidayEntry[] }> {
  return { settings: getCachedSettings(), holidays: getCachedHolidays() };
}

export async function updateOfficeSettings(
  startTime: string, endTime: string, weeklyOffDay: number, userId: string,
): Promise<OfficeSettings> {
  if (startTime >= endTime) throw new ValidationError('Start time must be before end time');
  if (weeklyOffDay < 0 || weeklyOffDay > 6) throw new ValidationError('weeklyOffDay must be between 0 and 6');
  await updateSettingsRow({ startTime, endTime, weeklyOffDay }, userId);
  emitter.emit('activity.create', { entity_type: 'office_settings', entity_id: 'default', action: 'updated', user_id: userId });
  await refreshCache();
  return getCachedSettings();
}

type CalendarEntryType = 'holiday' | 'half_day' | 'active_day';

// A full holiday can only be declared for a strictly future date — it must
// be announced at least a day ahead, never marked on/after the fact for a
// day that's already partway (or fully) elapsed, since that would
// retroactively erase idle math already accrued today. An Active Day
// override follows the same future-only rule — it's a planned staffing
// decision, not an emergency, so it's announced ahead of time too.
//
// A half-day is the deliberate exception: it's the "today, emergency
// closure" path (e.g. office shuts early for an unplanned reason) — so
// today's date is allowed for type 'half_day', just not any date already in
// the past.
function assertValidDate(date: string, type: CalendarEntryType): void {
  const today = formatDate(new Date());
  if (type === 'holiday' && date <= today) {
    throw new ValidationError('A holiday must be declared for a future date (not today or in the past) — for an emergency same-day closure, mark it as a half-day instead');
  }
  if (type === 'active_day' && date <= today) {
    throw new ValidationError('An Active Day must be marked for a future date (not today or in the past)');
  }
  if (type === 'half_day' && date < today) {
    throw new ValidationError('Half-day date cannot be in the past');
  }
}

function typeLabel(type: CalendarEntryType): string {
  return type === 'holiday' ? 'a holiday' : type === 'half_day' ? 'a half-day' : 'an active day';
}

export async function addHoliday(
  date: string, type: CalendarEntryType, halfDayStart: string | undefined, halfDayEnd: string | undefined,
  remark: string | undefined, userId: string,
): Promise<HolidayEntry> {
  assertValidDate(date, type);
  if (type === 'half_day') {
    if (!halfDayStart || !halfDayEnd) throw new ValidationError('halfDayStart and halfDayEnd are required for a half-day');
    if (halfDayStart >= halfDayEnd) throw new ValidationError('halfDayStart must be before halfDayEnd');
  }
  if (type === 'active_day' && new Date(`${date}T00:00:00`).getDay() !== settingsCache.weeklyOffDay) {
    throw new ValidationError('Active Day only makes sense on the weekly off day — that date is already a normal working day');
  }
  const existing = await findHolidayByDate(date);
  if (existing) throw new ValidationError(`${date} is already marked as ${typeLabel(existing.type)}`);

  const { id } = await createWithId('holiday_calendar', 'HOL', (id) => insertHoliday(
    { date, type, halfDayStart: type === 'half_day' ? halfDayStart : undefined, halfDayEnd: type === 'half_day' ? halfDayEnd : undefined, remark },
    id, userId,
  ));
  emitter.emit('activity.create', { entity_type: 'holiday_calendar', entity_id: id, action: 'created', user_id: userId });
  await refreshCache();
  const created = await findHolidayByDate(date);
  if (!created) throw new NotFoundError('Holiday not found after creation');
  return created;
}

export async function updateHoliday(
  id: string, date: string, type: CalendarEntryType, halfDayStart: string | undefined, halfDayEnd: string | undefined,
  remark: string | undefined, userId: string,
): Promise<HolidayEntry> {
  const existing = await findHolidayById(id);
  if (!existing) throw new NotFoundError('Holiday not found');
  // Can't touch an entry whose date has already had its idle math applied —
  // same "declared/undone in advance" rule as add/delete.
  assertValidDate(existing.date, existing.type);
  assertValidDate(date, type);
  if (type === 'half_day') {
    if (!halfDayStart || !halfDayEnd) throw new ValidationError('halfDayStart and halfDayEnd are required for a half-day');
    if (halfDayStart >= halfDayEnd) throw new ValidationError('halfDayStart must be before halfDayEnd');
  }
  if (type === 'active_day' && new Date(`${date}T00:00:00`).getDay() !== settingsCache.weeklyOffDay) {
    throw new ValidationError('Active Day only makes sense on the weekly off day — that date is already a normal working day');
  }
  if (date !== existing.date) {
    const collision = await findHolidayByDate(date);
    if (collision) throw new ValidationError(`${date} is already marked as ${typeLabel(collision.type)}`);
  }

  await updateHolidayRow(
    id, { date, type, halfDayStart: type === 'half_day' ? halfDayStart : undefined, halfDayEnd: type === 'half_day' ? halfDayEnd : undefined, remark },
    userId,
  );
  emitter.emit('activity.create', { entity_type: 'holiday_calendar', entity_id: id, action: 'updated', user_id: userId });
  await refreshCache();
  const updated = await findHolidayById(id);
  if (!updated) throw new NotFoundError('Holiday not found after update');
  return updated;
}

export async function deleteHoliday(id: string, userId: string): Promise<void> {
  const existing = getCachedHolidays().find((h) => h.id === id);
  if (!existing) throw new NotFoundError('Holiday not found');
  // Same rule applies symmetrically to removal — a full holiday or Active
  // Day can't be un-marked once its date is today or past, but a half-day
  // (today's emergency-closure path) can still be reverted same-day.
  assertValidDate(existing.date, existing.type);
  await removeHoliday(id);
  emitter.emit('activity.create', { entity_type: 'holiday_calendar', entity_id: id, action: 'deleted', user_id: userId });
  await refreshCache();
}
