import { db } from "../db";
import type { ShiftSlot, ShiftSlotStatus } from "../types";
import { formatDateOnly } from "../util/date";

/**
 * Default weekly shift template. Slots for a whole calendar month are
 * generated lazily (idempotently, via a UNIQUE index on date+start+end) the
 * first time anyone asks for that period, rather than on a cron schedule —
 * there's no scheduler in this app and this is simpler and just as reliable.
 */
function templatesForDow(dow: number): { start: string; end: string; capacity: number }[] {
  if (dow === 5 || dow === 6) {
    // Friday / Saturday
    return [
      { start: "18:00", end: "02:00", capacity: 1 },
      { start: "20:00", end: "00:00", capacity: 1 },
    ];
  }
  // Sunday - Thursday
  return [{ start: "18:00", end: "23:00", capacity: 1 }];
}

export function ensureSlotsForPeriod(period: string): void {
  const [year, monthIndex] = parsePeriod(period);
  const days = new Date(year, monthIndex + 1, 0).getDate();

  const insert = db.prepare(
    `INSERT OR IGNORE INTO shift_slots (date, start_time, end_time, capacity, status, generated)
     VALUES (?, ?, ?, ?, 'open', 1)`
  );

  const generate = db.transaction(() => {
    for (let day = 1; day <= days; day++) {
      const date = new Date(year, monthIndex, day);
      const dateText = formatDateOnly(date);
      for (const template of templatesForDow(date.getDay())) {
        insert.run(dateText, template.start, template.end, template.capacity);
      }
    }
  });

  generate();
}

export interface AdminShiftSlotView {
  id: number;
  date: string;
  start_time: string;
  end_time: string;
  capacity: number;
  status: ShiftSlotStatus;
  note: string;
  generated: boolean;
  occupied: number;
  employees: { employee_id: number; name: string }[];
}

export function listAdminSlotsForPeriod(period: string): AdminShiftSlotView[] {
  ensureSlotsForPeriod(period);

  const slots = db
    .prepare<[string], ShiftSlot>("SELECT * FROM shift_slots WHERE date LIKE ? ORDER BY date, start_time")
    .all(`${period}-%`);

  const signupsForSlot = db.prepare<[number], { employee_id: number; email: string; name: string | null }>(
    `SELECT ss.employee_id, e.email, e.name
     FROM shift_signups ss
     JOIN employees e ON e.id = ss.employee_id
     WHERE ss.slot_id = ? AND ss.status = 'approved'`
  );

  return slots.map((slot) => {
    const employees = signupsForSlot
      .all(slot.id)
      .map((row) => ({ employee_id: row.employee_id, name: row.name ?? row.email }));

    return {
      id: slot.id,
      date: slot.date,
      start_time: slot.start_time,
      end_time: slot.end_time,
      capacity: slot.capacity,
      status: slot.status,
      note: slot.note,
      generated: slot.generated === 1,
      occupied: employees.length,
      employees,
    };
  });
}

export interface SlotInput {
  date: string;
  start_time: string;
  end_time: string;
  capacity: number;
  note?: string;
}

export function createSlot(data: SlotInput): number {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(data.date)) {
    throw new Error("Neplatné datum.");
  }
  const startTime = normalizeTime(data.start_time);
  const endTime = normalizeTime(data.end_time);
  if (!Number.isInteger(data.capacity) || data.capacity < 1) {
    throw new Error("Kapacita musí být alespoň 1.");
  }

  try {
    const result = db
      .prepare(
        `INSERT INTO shift_slots (date, start_time, end_time, capacity, status, note, generated)
         VALUES (?, ?, ?, ?, 'open', ?, 0)`
      )
      .run(data.date, startTime, endTime, data.capacity, data.note ?? "");
    return Number(result.lastInsertRowid);
  } catch (err) {
    if (String(err).includes("UNIQUE")) {
      throw new Error("Stejná směna už existuje.");
    }
    throw err;
  }
}

export function updateSlot(slotId: number, data: SlotInput): void {
  const slot = getSlot(slotId);
  if (!slot) {
    throw new Error("Směna nebyla nalezena.");
  }
  if (!Number.isInteger(data.capacity) || data.capacity < 0) {
    throw new Error("Kapacita musí být celé číslo 0 nebo vyšší.");
  }

  const occupied = countApproved(slotId);
  if (data.capacity < occupied) {
    throw new Error("Kapacitu nelze snížit pod počet již přihlášených zaměstnanců.");
  }

  db.prepare(
    `UPDATE shift_slots
     SET date = ?, start_time = ?, end_time = ?, capacity = ?, status = ?, note = ?,
         updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
     WHERE id = ?`
  ).run(
    data.date || slot.date,
    normalizeTime(data.start_time),
    normalizeTime(data.end_time),
    data.capacity,
    computeStatus(data.capacity, occupied),
    data.note ?? "",
    slotId
  );
}

export function assignEmployeeToSlot(slotId: number, employeeId: number): void {
  signupInternal(slotId, employeeId);
}

export function removeEmployeeFromSlot(slotId: number, employeeId: number, actorEmail: string): void {
  cancelInternal(slotId, employeeId, actorEmail);
}

export interface AvailableSlotView {
  slot_id: number;
  date: string;
  start_time: string;
  end_time: string;
  capacity: number;
  occupied: number;
  free_places: number;
  note: string;
}

export function getAvailableSlotsForEmployee(employeeId: number, period: string): AvailableSlotView[] {
  ensureSlotsForPeriod(period);

  return db
    .prepare<[string], ShiftSlot>(
      "SELECT * FROM shift_slots WHERE date LIKE ? AND status = 'open' ORDER BY date, start_time"
    )
    .all(`${period}-%`)
    .filter((slot) => !hasApprovedSignup(slot.id, employeeId))
    .map((slot) => {
      const occupied = countApproved(slot.id);
      return {
        slot_id: slot.id,
        date: slot.date,
        start_time: slot.start_time,
        end_time: slot.end_time,
        capacity: slot.capacity,
        occupied,
        free_places: Math.max(0, slot.capacity - occupied),
        note: slot.note,
      };
    });
}

export interface MySlotView {
  signup_id: number;
  slot_id: number;
  date: string;
  start_time: string;
  end_time: string;
  note: string;
}

export function getMySlotsForEmployee(employeeId: number, period: string): MySlotView[] {
  ensureSlotsForPeriod(period);

  return db
    .prepare<[number, string], MySlotView>(
      `SELECT ss.id as signup_id, sl.id as slot_id, sl.date, sl.start_time, sl.end_time, sl.note
       FROM shift_signups ss
       JOIN shift_slots sl ON sl.id = ss.slot_id
       WHERE ss.employee_id = ? AND ss.status = 'approved' AND sl.date LIKE ?
       ORDER BY sl.date, sl.start_time`
    )
    .all(employeeId, `${period}-%`);
}

export function signupForSlot(employeeId: number, slotId: number): void {
  signupInternal(slotId, employeeId);
}

export function cancelMySignup(employeeId: number, slotId: number, actorEmail: string): void {
  cancelInternal(slotId, employeeId, actorEmail);
}

/* ---------- internals ---------- */

function signupInternal(slotId: number, employeeId: number): void {
  const slot = getSlot(slotId);
  if (!slot) {
    throw new Error("Směna nebyla nalezena.");
  }
  if (slot.status === "closed") {
    throw new Error("Směna je uzavřená.");
  }
  if (hasApprovedSignup(slotId, employeeId)) {
    throw new Error("Na této směně už jsi přihlášen.");
  }

  assertNoCollision(employeeId, slot);

  const occupied = countApproved(slotId);
  if (occupied >= slot.capacity) {
    throw new Error("Směna je už obsazená.");
  }

  db.prepare("INSERT INTO shift_signups (slot_id, employee_id, status) VALUES (?, ?, 'approved')").run(
    slotId,
    employeeId
  );

  refreshSlotStatus(slotId);
}

function cancelInternal(slotId: number, employeeId: number, actorEmail: string): void {
  const signup = db
    .prepare<[number, number], { id: number }>(
      "SELECT id FROM shift_signups WHERE slot_id = ? AND employee_id = ? AND status = 'approved'"
    )
    .get(slotId, employeeId);

  if (!signup) {
    throw new Error("Přihlášení na směnu nebylo nalezeno.");
  }

  db.prepare(
    `UPDATE shift_signups
     SET status = 'cancelled', cancelled_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), cancelled_by = ?
     WHERE id = ?`
  ).run(actorEmail, signup.id);

  refreshSlotStatus(slotId);
}

function assertNoCollision(employeeId: number, targetSlot: ShiftSlot): void {
  const mySlots = db
    .prepare<[number], ShiftSlot>(
      `SELECT sl.* FROM shift_slots sl
       JOIN shift_signups ss ON ss.slot_id = sl.id
       WHERE ss.employee_id = ? AND ss.status = 'approved'`
    )
    .all(employeeId);

  const targetStart = slotStart(targetSlot);
  const targetEnd = slotEnd(targetSlot);

  for (const slot of mySlots) {
    if (targetStart < slotEnd(slot) && targetEnd > slotStart(slot)) {
      throw new Error("Tato směna se překrývá s jinou směnou, na kterou už jsi přihlášen.");
    }
  }
}

function refreshSlotStatus(slotId: number): void {
  const slot = getSlot(slotId);
  if (!slot) return;
  const occupied = countApproved(slotId);
  db.prepare(
    `UPDATE shift_slots SET status = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`
  ).run(computeStatus(slot.capacity, occupied), slotId);
}

function computeStatus(capacity: number, occupied: number): ShiftSlotStatus {
  if (capacity <= 0) return "closed";
  return occupied >= capacity ? "full" : "open";
}

function countApproved(slotId: number): number {
  const row = db
    .prepare<[number], { n: number }>("SELECT COUNT(*) as n FROM shift_signups WHERE slot_id = ? AND status = 'approved'")
    .get(slotId)!;
  return row.n;
}

function hasApprovedSignup(slotId: number, employeeId: number): boolean {
  return !!db
    .prepare("SELECT 1 FROM shift_signups WHERE slot_id = ? AND employee_id = ? AND status = 'approved'")
    .get(slotId, employeeId);
}

function getSlot(id: number): ShiftSlot | undefined {
  return db.prepare<[number], ShiftSlot>("SELECT * FROM shift_slots WHERE id = ?").get(id);
}

function slotStart(slot: Pick<ShiftSlot, "date" | "start_time">): Date {
  const [year, month, day] = slot.date.split("-").map(Number);
  const [hour, minute] = slot.start_time.split(":").map(Number);
  return new Date(year, month - 1, day, hour, minute);
}

/** End time rolls over to the next day whenever it's not after the start time (e.g. 18:00-02:00). */
function slotEnd(slot: Pick<ShiftSlot, "date" | "start_time" | "end_time">): Date {
  const start = slotStart(slot);
  const [hour, minute] = slot.end_time.split(":").map(Number);
  const end = new Date(start.getFullYear(), start.getMonth(), start.getDate(), hour, minute);
  if (end <= start) end.setDate(end.getDate() + 1);
  return end;
}

function parsePeriod(period: string): [number, number] {
  if (!/^\d{4}-\d{2}$/.test(period)) {
    throw new Error("Neplatné období.");
  }
  return [Number(period.slice(0, 4)), Number(period.slice(5, 7)) - 1];
}

function normalizeTime(value: string): string {
  const match = /^(\d{1,2}):(\d{2})$/.exec(String(value ?? "").trim());
  if (!match) {
    throw new Error("Neplatný čas.");
  }
  const hour = Number(match[1]);
  const minute = Number(match[2]);
  if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
    throw new Error("Neplatný čas.");
  }
  return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
}
