import { db } from "../db";
import type { AttendanceEvent, EventType } from "../types";

export type LiveStatus = "clocked_out" | "working" | "on_break";

const NEXT_ALLOWED: Record<LiveStatus, EventType[]> = {
  clocked_out: ["clock_in"],
  working: ["break_start", "clock_out"],
  on_break: ["break_end"],
};

function statusAfter(lastType: EventType | null): LiveStatus {
  switch (lastType) {
    case null:
    case "clock_out":
      return "clocked_out";
    case "clock_in":
    case "break_end":
      return "working";
    case "break_start":
      return "on_break";
  }
}

const MAX_SHIFT_MS = 12 * 60 * 60 * 1000;

export function getLastEvent(employeeId: number): AttendanceEvent | undefined {
  return db
    .prepare<[number], AttendanceEvent>(
      "SELECT * FROM attendance_events WHERE employee_id = ? ORDER BY ts DESC, id DESC LIMIT 1"
    )
    .get(employeeId);
}

export function getLiveStatus(employeeId: number): LiveStatus {
  autoCloseStaleShift(employeeId);
  const last = getLastEvent(employeeId);
  return statusAfter(last?.type ?? null);
}

/**
 * A shift nobody clocked out of (forgotten, phone died, whatever) is capped at 12h — closed
 * at clock_in + 12h rather than growing forever. Runs lazily on every read instead of a
 * background job, so it self-heals without needing a scheduler.
 */
function autoCloseStaleShift(employeeId: number): void {
  const last = getLastEvent(employeeId);
  const status = statusAfter(last?.type ?? null);
  if (status === "clocked_out") return;

  const clockIn = db
    .prepare<[number], AttendanceEvent>(
      "SELECT * FROM attendance_events WHERE employee_id = ? AND type = 'clock_in' ORDER BY ts DESC, id DESC LIMIT 1"
    )
    .get(employeeId)!;

  if (Date.now() - new Date(clockIn.ts).getTime() <= MAX_SHIFT_MS) return;

  const cutoff = new Date(new Date(clockIn.ts).getTime() + MAX_SHIFT_MS).toISOString();
  if (status === "on_break") {
    db.prepare("INSERT INTO attendance_events (employee_id, type, ts) VALUES (?, 'break_end', ?)").run(
      employeeId,
      cutoff
    );
  }
  db.prepare("INSERT INTO attendance_events (employee_id, type, ts) VALUES (?, 'clock_out', ?)").run(
    employeeId,
    cutoff
  );
}

export class InvalidTransitionError extends Error {
  constructor(public readonly current: LiveStatus, public readonly attempted: EventType) {
    super(`Cannot record "${attempted}" while status is "${current}"`);
  }
}

export function recordEvent(employeeId: number, type: EventType): AttendanceEvent {
  const current = getLiveStatus(employeeId);
  if (!NEXT_ALLOWED[current].includes(type)) {
    throw new InvalidTransitionError(current, type);
  }
  const result = db
    .prepare("INSERT INTO attendance_events (employee_id, type) VALUES (?, ?)")
    .run(employeeId, type);
  return db
    .prepare<[number], AttendanceEvent>("SELECT * FROM attendance_events WHERE id = ?")
    .get(Number(result.lastInsertRowid))!;
}

export function eventsInRange(employeeId: number, fromIso: string, toIso: string): AttendanceEvent[] {
  autoCloseStaleShift(employeeId);
  return db
    .prepare<[number, string, string], AttendanceEvent>(
      `SELECT * FROM attendance_events
       WHERE employee_id = ? AND ts >= ? AND ts < ?
       ORDER BY ts ASC, id ASC`
    )
    .all(employeeId, fromIso, toIso);
}

export interface Session {
  clockIn: string;
  clockOut: string | null;
  breaks: { start: string; end: string | null }[];
  workedMs: number;
  breakMs: number;
  open: boolean;
}

/**
 * Walks a chronological event list and pairs them into shifts. Any session still
 * open at the end of the range (no clock_out yet) is included with open: true so
 * "currently working" shows up in today's view, but callers doing historical
 * totals should treat open sessions' worked time as provisional (measured up to `now`).
 */
export function buildSessions(events: AttendanceEvent[], now: Date): Session[] {
  const sessions: Session[] = [];
  let current: Session | null = null;

  for (const event of events) {
    const ts = event.ts;
    if (event.type === "clock_in") {
      current = { clockIn: ts, clockOut: null, breaks: [], workedMs: 0, breakMs: 0, open: true };
      sessions.push(current);
    } else if (!current) {
      // Orphaned event (e.g. data edited manually) — ignore rather than crash.
      continue;
    } else if (event.type === "break_start") {
      current.breaks.push({ start: ts, end: null });
    } else if (event.type === "break_end") {
      const openBreak = current.breaks.find((b) => b.end === null);
      if (openBreak) openBreak.end = ts;
    } else if (event.type === "clock_out") {
      current.clockOut = ts;
      current.open = false;
      current = null;
    }
  }

  for (const session of sessions) {
    const end = session.clockOut ? new Date(session.clockOut) : now;
    const start = new Date(session.clockIn);
    session.breakMs = session.breaks.reduce((sum, b) => {
      const bEnd = b.end ? new Date(b.end) : now;
      return sum + Math.max(0, bEnd.getTime() - new Date(b.start).getTime());
    }, 0);
    session.workedMs = Math.max(0, end.getTime() - start.getTime() - session.breakMs);
  }

  return sessions;
}

export interface StatsSummary {
  sessions: Session[];
  totalWorkedMs: number;
  totalBreakMs: number;
  shiftCount: number;
}

export function summarize(events: AttendanceEvent[], now: Date): StatsSummary {
  const sessions = buildSessions(events, now);
  return {
    sessions,
    totalWorkedMs: sessions.reduce((sum, s) => sum + s.workedMs, 0),
    totalBreakMs: sessions.reduce((sum, s) => sum + s.breakMs, 0),
    shiftCount: sessions.length,
  };
}
