import { attendance_leave_type, Prisma } from "@prisma/client";
import prisma from "../config/database";
import { getBusinessDaysInMonth, isHoliday } from "../utils/czech-holidays";
import { localDateStr } from "../utils/date";
import { getSystemSettings } from "./system-settings";

/** Get active users whose role has attendance.record permission */
async function getAttendanceUsers() {
  return prisma.users.findMany({
    where: {
      is_active: true,
      roles: {
        role_permissions: {
          some: { permissions: { name: "attendance.record" } },
        },
      },
    },
    select: { id: true, first_name: true, last_name: true },
    orderBy: { last_name: "asc" },
  });
}

type AttendanceWithRelations = Prisma.attendanceGetPayload<{
  include: {
    users: { select: { id: true; first_name: true; last_name: true } };
    attendance_project_logs: true;
  };
}>;

const VALID_LEAVE_TYPES = ["work", "vacation", "sick", "unpaid"] as const;

/** Default annual vacation allowance (hours) when a user has no leave_balances row. */
const DEFAULT_VACATION_TOTAL = 160;

const MONTH_NAMES = [
  "Leden",
  "Únor",
  "Březen",
  "Duben",
  "Květen",
  "Červen",
  "Červenec",
  "Srpen",
  "Září",
  "Říjen",
  "Listopad",
  "Prosinec",
];

// ── Helpers ──────────────────────────────────────────────────────────

function calcWorkedHours(
  arrival: Date,
  departure: Date,
  breakStart: Date | null,
  breakEnd: Date | null,
): number {
  let mins = (departure.getTime() - arrival.getTime()) / 60000;
  if (breakStart && breakEnd) {
    mins -= (breakEnd.getTime() - breakStart.getTime()) / 60000;
  }
  return Math.max(0, mins) / 60;
}

const roundUp = (d: Date, minutes: number) => {
  if (!minutes || minutes <= 0) return d;
  const ms = minutes * 60 * 1000;
  return new Date(Math.ceil(d.getTime() / ms) * ms);
};

const roundDown = (d: Date, minutes: number) => {
  if (!minutes || minutes <= 0) return d;
  const ms = minutes * 60 * 1000;
  return new Date(Math.floor(d.getTime() / ms) * ms);
};

/**
 * Lock a user row inside an existing transaction. Used to serialize punch
 * actions and project switches for a single user: two parallel taps of the
 * "Příchod" button would otherwise both see "no open shift" and both create
 * one, leaving the user with two parallel open shifts. With the lock, the
 * second call waits, then re-reads and bails.
 */
async function lockUserRow(
  tx: Prisma.TransactionClient,
  userId: number,
): Promise<void> {
  // Must be $queryRaw (not $executeRaw): $executeRaw is for write statements
  // and may not reliably acquire/hold the row lock for a bare SELECT ... FOR
  // UPDATE. $queryRaw runs it as a query so the lock is actually taken.
  await tx.$queryRaw`SELECT id FROM ${Prisma.raw("users")} WHERE id = ${userId} FOR UPDATE`;
}

// ── Interfaces ───────────────────────────────────────────────────────

export interface ListAttendanceParams {
  page: number;
  limit: number;
  skip: number;
  order: "asc" | "desc";
  userId?: number;
  isAdmin: boolean;
  authUserId: number;
  month?: number;
  year?: number;
}

export interface PunchData {
  punch_action: string;
  latitude?: number | string | null;
  longitude?: number | string | null;
  accuracy?: number | string | null;
  address?: string | null;
}

export interface CreateAttendanceData {
  user_id?: number;
  shift_date: string;
  arrival_time?: string | null;
  arrival_lat?: number | null;
  arrival_lng?: number | null;
  arrival_accuracy?: number | null;
  arrival_address?: string | null;
  departure_time?: string | null;
  departure_lat?: number | null;
  departure_lng?: number | null;
  departure_accuracy?: number | null;
  departure_address?: string | null;
  break_start?: string | null;
  break_end?: string | null;
  notes?: string | null;
  project_id?: number | null;
  leave_type: string;
  leave_hours?: number | null;
  project_logs?: Array<{
    project_id: number;
    hours?: number;
    minutes?: number;
  }>;
}

export interface UpdateAttendanceData {
  arrival_time?: string | null;
  departure_time?: string | null;
  break_start?: string | null;
  break_end?: string | null;
  notes?: string | null;
  project_id?: number | null;
  leave_type?: string;
  leave_hours?: number | null;
  project_logs?: Array<{
    project_id: number;
    hours?: number;
    minutes?: number;
  }>;
}

export interface LeaveData {
  user_id?: number;
  date_from: string;
  date_to?: string;
  leave_type: string;
  leave_hours?: number;
  notes?: string;
}

export interface BulkAttendanceData {
  month: string;
  user_ids: number[];
  arrival_time: string;
  departure_time: string;
  break_start_time: string;
  break_end_time: string;
}

export interface BalancesData {
  user_id: number;
  year?: number;
  action_type: string;
  vacation_total?: number | null;
  vacation_used?: number | null;
  sick_used?: number | null;
}

// ── Service Functions ────────────────────────────────────────────────

export async function getStatus(userId: number) {
  const now = new Date();
  const y = now.getFullYear(),
    m = now.getMonth(),
    d = now.getDate();
  // shift_date is @db.Date: Prisma compares it by its UTC date part, so the
  // range boundaries must be UTC midnights of the LOCAL calendar day. A local
  // midnight (= 22:00/23:00 UTC of the previous day) shifts the whole window
  // one day back. Half-open [gte, lt) ranges.
  const todayStart = new Date(Date.UTC(y, m, d));
  const todayEnd = new Date(Date.UTC(y, m, d + 1));

  // Monthly fund range (used by query #4)
  const monthStart = new Date(Date.UTC(y, m, 1));
  const monthEnd = new Date(Date.UTC(y, m + 1, 1));

  // Queries 1-4 are independent of one another → run them in parallel.
  const [ongoingShift, todayShiftsRaw, balance, monthRecords] =
    await Promise.all([
      // 1) Ongoing shift (no departure)
      prisma.attendance.findFirst({
        where: {
          user_id: userId,
          departure_time: null,
          arrival_time: { not: null },
        },
        orderBy: { created_at: "desc" },
      }),
      // 2) Today's completed shifts
      prisma.attendance.findMany({
        where: {
          user_id: userId,
          shift_date: { gte: todayStart, lt: todayEnd },
          departure_time: { not: null },
        },
        include: {
          attendance_project_logs: { orderBy: { started_at: "asc" } },
        },
        orderBy: { arrival_time: "asc" },
      }),
      // 3) Leave balance
      prisma.leave_balances.findFirst({
        where: { user_id: userId, year: y },
      }),
      // 4) Monthly fund records
      prisma.attendance.findMany({
        where: {
          user_id: userId,
          shift_date: { gte: monthStart, lt: monthEnd },
        },
      }),
    ]);

  const leaveBalance = {
    vacation_total: balance
      ? Number(balance.vacation_total)
      : DEFAULT_VACATION_TOTAL,
    vacation_used: balance ? Number(balance.vacation_used) : 0,
    vacation_remaining: balance
      ? Number(balance.vacation_total) - Number(balance.vacation_used)
      : DEFAULT_VACATION_TOTAL,
    sick_used: balance ? Number(balance.sick_used) : 0,
  };

  const workingDays = getBusinessDaysInMonth(y, m);
  const fund = workingDays * 8;

  let workedHours = 0;
  let vacationHours = 0;
  let sickHours = 0;
  let unpaidHours = 0;

  for (const rec of monthRecords) {
    const lt = (rec.leave_type as string) || "work";
    if (lt !== "work") {
      const hrs = Number(rec.leave_hours) || 8;
      if (lt === "vacation") vacationHours += hrs;
      else if (lt === "sick") sickHours += hrs;
      else if (lt === "unpaid") unpaidHours += hrs;
      // "holiday" is no longer a user-selectable leave_type — it is
      // auto-derived from Czech public holidays. Old rows are skipped.
      continue;
    }
    if (rec.arrival_time && rec.departure_time) {
      workedHours += calcWorkedHours(
        rec.arrival_time,
        rec.departure_time,
        rec.break_start,
        rec.break_end,
      );
    }
  }

  const worked = Math.round(workedHours * 100) / 100;
  const adjustedFund = Math.max(0, fund);
  const leaveHours = vacationHours + sickHours;
  const covered = worked + leaveHours;
  const remaining = Math.max(0, adjustedFund - covered);
  const overtime = Math.max(0, covered - adjustedFund);

  const monthlyFund = {
    month_name: `${MONTH_NAMES[m]} ${y}`,
    fund: adjustedFund,
    business_days: workingDays,
    worked,
    covered,
    remaining,
    overtime,
    leave_hours: leaveHours,
    vacation_hours: vacationHours,
    sick_hours: sickHours,
    unpaid_hours: unpaidHours,
  };

  // 5) Project logs for ongoing shift
  const completedProjectIds = new Set<number>();
  for (const shift of todayShiftsRaw) {
    for (const log of shift.attendance_project_logs) {
      completedProjectIds.add(log.project_id);
    }
  }

  const completedProjectNames = new Map<number, string>();
  if (completedProjectIds.size > 0) {
    const projects = await prisma.projects.findMany({
      where: { id: { in: [...completedProjectIds] } },
      select: { id: true, name: true, project_number: true },
    });
    for (const p of projects) {
      completedProjectNames.set(
        p.id,
        p.project_number ? `${p.project_number} – ${p.name}` : p.name || "",
      );
    }
  }

  const todayShifts = todayShiftsRaw.map((shift) => ({
    ...shift,
    project_logs: shift.attendance_project_logs.map((l) => ({
      ...l,
      project_name:
        completedProjectNames.get(l.project_id) || `Projekt #${l.project_id}`,
    })),
  }));

  let projectLogs: Array<{
    id: number;
    attendance_id: number;
    project_id: number;
    started_at: Date | null;
    ended_at: Date | null;
    project_name?: string;
  }> = [];
  let activeProjectId: number | null = null;

  if (ongoingShift) {
    const logs = await prisma.attendance_project_logs.findMany({
      where: { attendance_id: ongoingShift.id },
      orderBy: { started_at: "asc" },
    });

    const projectIds = [...new Set(logs.map((l) => l.project_id))];
    const projectNames = new Map<number, string>();
    if (projectIds.length > 0) {
      const projects = await prisma.projects.findMany({
        where: { id: { in: projectIds } },
        select: { id: true, name: true, project_number: true },
      });
      for (const p of projects) {
        projectNames.set(
          p.id,
          p.project_number ? `${p.project_number} – ${p.name}` : p.name || "",
        );
      }
    }

    projectLogs = logs.map((l) => ({
      ...l,
      project_name:
        projectNames.get(l.project_id) || `Projekt #${l.project_id}`,
    }));

    const activeLog = logs.find((l) => l.ended_at === null);
    if (activeLog) {
      activeProjectId = activeLog.project_id;
    } else {
      activeProjectId = ongoingShift.project_id ?? null;
    }
  }

  return {
    ongoing_shift: ongoingShift,
    today_shifts: todayShifts,
    leave_balance: leaveBalance,
    monthly_fund: monthlyFund,
    date: localDateStr(now),
    project_logs: projectLogs,
    active_project_id: activeProjectId,
  };
}

export async function saveNotes(userId: number, notes: string | null) {
  const ongoing = await prisma.attendance.findFirst({
    where: {
      user_id: userId,
      departure_time: null,
      arrival_time: { not: null },
    },
    orderBy: { created_at: "desc" },
  });
  if (!ongoing) return { error: "Nemáte aktivní směnu." };

  await prisma.attendance.update({
    where: { id: ongoing.id },
    data: { notes: notes ? String(notes) : null },
  });

  return { success: true };
}

export async function updateAddress(
  userId: number,
  address: string | null,
  punchAction: string,
) {
  // When updating departure address, the punch already set departure_time,
  // so we can't filter on departure_time: null. Find the latest record instead.
  const where: Record<string, unknown> = {
    user_id: userId,
    arrival_time: { not: null },
  };
  if (punchAction === "arrival") {
    where.departure_time = null;
  }

  const latest = await prisma.attendance.findFirst({
    where,
    orderBy: { created_at: "desc" },
  });
  if (!latest) return { error: "Nenalezen záznam" };

  const data: Record<string, unknown> = {};
  if (punchAction === "departure") {
    data.departure_address = address;
  } else {
    data.arrival_address = address;
  }

  await prisma.attendance.update({ where: { id: latest.id }, data });
  return { success: true };
}

export async function switchProject(userId: number, projectId: number | null) {
  return prisma.$transaction(async (tx) => {
    // Lock the user row so two parallel "switch project" clicks (or one
    // interleaved with a departure punch) cannot both create overlapping
    // project logs for the same open shift.
    await lockUserRow(tx, userId);

    const ongoing = await tx.attendance.findFirst({
      where: {
        user_id: userId,
        departure_time: null,
        arrival_time: { not: null },
      },
      orderBy: { created_at: "desc" },
    });
    if (!ongoing) return { error: "Nemáte aktivní směnu." };

    const now = new Date();

    // End active project logs, ensuring ended_at is never before started_at
    // (can happen when arrival_time was rounded up and now is still earlier)
    const activeLogs = await tx.attendance_project_logs.findMany({
      where: { attendance_id: ongoing.id, ended_at: null },
    });
    for (const log of activeLogs) {
      const endedAt =
        log.started_at && log.started_at > now ? log.started_at : now;
      await tx.attendance_project_logs.update({
        where: { id: log.id },
        data: { ended_at: endedAt },
      });
    }

    if (projectId) {
      const existingLogs = await tx.attendance_project_logs.count({
        where: { attendance_id: ongoing.id },
      });
      const isFirstProject = existingLogs === 0;
      let startedAt = isFirstProject ? ongoing.arrival_time! : now;
      if (startedAt > now) startedAt = now;
      await tx.attendance_project_logs.create({
        data: {
          attendance_id: ongoing.id,
          project_id: projectId,
          started_at: startedAt,
          ended_at: null,
        },
      });
    }

    await tx.attendance.update({
      where: { id: ongoing.id },
      data: { project_id: projectId },
    });

    return { success: true };
  });
}

export async function getBalances(year: number) {
  const users = await getAttendanceUsers();

  const balances: Record<
    string,
    {
      name: string;
      vacation_total: number;
      vacation_used: number;
      vacation_remaining: number;
      sick_used: number;
    }
  > = {};
  // Single query for every user's balance for the year (was an N+1 loop).
  const balanceRows = await prisma.leave_balances.findMany({
    where: { year, user_id: { in: users.map((u) => u.id) } },
  });
  // Keep the first row seen per user (matches the old per-user findFirst).
  const balanceByUser = new Map<number, (typeof balanceRows)[number]>();
  for (const row of balanceRows) {
    if (!balanceByUser.has(row.user_id)) balanceByUser.set(row.user_id, row);
  }

  for (const u of users) {
    const lb = balanceByUser.get(u.id);
    const vTotal = lb ? Number(lb.vacation_total) : DEFAULT_VACATION_TOTAL;
    const vUsed = lb ? Number(lb.vacation_used) : 0;
    const sUsed = lb ? Number(lb.sick_used) : 0;
    balances[String(u.id)] = {
      name: `${u.first_name} ${u.last_name}`.trim(),
      vacation_total: vTotal,
      vacation_used: vUsed,
      vacation_remaining: vTotal - vUsed,
      sick_used: sUsed,
    };
  }

  return {
    users: users.map((u) => ({
      id: u.id,
      name: `${u.first_name} ${u.last_name}`.trim(),
    })),
    balances,
  };
}

export async function getWorkfund(year: number) {
  const users = await getAttendanceUsers();

  const now = new Date();
  const currentYear = now.getFullYear();
  const currentMonth = now.getMonth(); // 0-based
  const maxMonth =
    year < currentYear ? 11 : year === currentYear ? currentMonth : -1;

  if (maxMonth < 0) {
    return {
      months: {},
      users: users.map((u) => ({
        id: u.id,
        name: `${u.first_name} ${u.last_name}`.trim(),
      })),
      balances: {},
    };
  }

  const months: Record<
    string,
    {
      month_name: string;
      fund: number;
      fund_to_date: number;
      business_days: number;
      users: Record<
        string,
        {
          name: string;
          worked: number;
          covered: number;
          overtime: number;
          missing: number;
        }
      >;
    }
  > = {};

  for (let m = 0; m <= maxMonth; m++) {
    const isCurrentMonth = year === currentYear && m === currentMonth;
    const bizDays = getBusinessDaysInMonth(year, m);
    const bizDaysToDate = isCurrentMonth
      ? getBusinessDaysInMonth(year, m, now.getDate())
      : bizDays;
    const fund = bizDays * 8;
    const fundToDate = bizDaysToDate * 8;
    // shift_date is @db.Date (compared by UTC date part) → UTC-midnight
    // month boundaries, half-open range. Local midnights would double-count
    // each previous month's last day.
    const monthStart = new Date(Date.UTC(year, m, 1));
    const monthEnd = new Date(Date.UTC(year, m + 1, 1));
    const monthRecords = await prisma.attendance.findMany({
      where: { shift_date: { gte: monthStart, lt: monthEnd } },
      select: {
        user_id: true,
        shift_date: true,
        leave_type: true,
        arrival_time: true,
        departure_time: true,
        break_start: true,
        break_end: true,
        leave_hours: true,
      },
    });

    const monthUsers: Record<
      string,
      {
        name: string;
        worked: number;
        covered: number;
        overtime: number;
        missing: number;
      }
    > = {};

    for (const u of users) {
      const recs = monthRecords.filter((r) => r.user_id === u.id);
      let worked = 0;
      let vacationHours = 0;
      let sickHours = 0;
      for (const rec of recs) {
        const lt = (rec.leave_type as string) || "work";
        if (lt === "work") {
          if (rec.arrival_time && rec.departure_time) {
            worked += calcWorkedHours(
              rec.arrival_time,
              rec.departure_time,
              rec.break_start,
              rec.break_end,
            );
          }
        } else if (lt === "vacation") {
          vacationHours += Number(rec.leave_hours) || 8;
        } else if (lt === "sick") {
          sickHours += Number(rec.leave_hours) || 8;
        }
        // "holiday" is auto-derived from Czech public holidays.
      }

      const userFund = fundToDate;
      const workedRound = Math.round(worked * 10) / 10;
      const leaveHours = vacationHours + sickHours;
      const covered = Math.round((worked + leaveHours) * 10) / 10;
      const missing = Math.max(0, Math.round((userFund - covered) * 10) / 10);
      const overtime = Math.max(0, Math.round((covered - userFund) * 10) / 10);

      monthUsers[String(u.id)] = {
        name: `${u.first_name} ${u.last_name}`.trim(),
        worked: workedRound,
        covered,
        overtime,
        missing,
      };
    }

    months[String(m + 1)] = {
      month_name: MONTH_NAMES[m],
      fund,
      fund_to_date: fundToDate,
      business_days: bizDays,
      users: monthUsers,
    };
  }

  return {
    months,
    users: users.map((u) => ({
      id: u.id,
      name: `${u.first_name} ${u.last_name}`.trim(),
    })),
    balances: {},
    holidays: [],
  };
}

export async function getProjectReport(year: number) {
  const yearStart = new Date(year, 0, 1);
  const yearEnd = new Date(year, 11, 31, 23, 59, 59);

  const records = await prisma.attendance.findMany({
    where: {
      shift_date: { gte: yearStart, lte: yearEnd },
      leave_type: "work",
      arrival_time: { not: null },
      departure_time: { not: null },
    },
    include: {
      users: { select: { id: true, first_name: true, last_name: true } },
      attendance_project_logs: {
        orderBy: { started_at: "asc" },
      },
    },
  });

  // Collect all project ids from both attendance.project_id and project logs
  const projectIds = new Set<number>();
  for (const rec of records) {
    if (rec.project_id) projectIds.add(rec.project_id);
    for (const log of rec.attendance_project_logs) {
      projectIds.add(log.project_id);
    }
  }

  const projectsMap = new Map<
    number,
    { name: string; project_number: string }
  >();
  if (projectIds.size > 0) {
    const projects = await prisma.projects.findMany({
      where: { id: { in: [...projectIds] } },
      select: { id: true, name: true, project_number: true },
    });
    for (const p of projects) {
      projectsMap.set(p.id, {
        name: p.name || "",
        project_number: p.project_number || "",
      });
    }
  }

  const months: Record<
    string,
    {
      month_name: string;
      projects: Array<{
        project_id: number | null;
        project_number?: string;
        project_name?: string;
        hours: number;
        users: Array<{ user_id: number; user_name: string; hours: number }>;
      }>;
    }
  > = {};

  for (let m = 0; m < 12; m++) {
    const monthStart = new Date(year, m, 1);
    const monthEnd = new Date(year, m + 1, 0, 23, 59, 59);
    const monthRecs = records.filter(
      (r) => r.shift_date >= monthStart && r.shift_date <= monthEnd,
    );

    if (monthRecs.length === 0) continue;

    const projectMap = new Map<
      number | null,
      {
        project_number?: string;
        project_name?: string;
        userMap: Map<number, { name: string; hours: number }>;
      }
    >();

    for (const rec of monthRecs) {
      const uid = rec.user_id;
      const uName = rec.users
        ? `${rec.users.first_name} ${rec.users.last_name}`.trim()
        : `User #${uid}`;

      if (rec.attendance_project_logs.length === 0) {
        // No detailed project logs — fall back to attendance.project_id
        const pid = rec.project_id;
        const hours = calcWorkedHours(
          rec.arrival_time!,
          rec.departure_time!,
          rec.break_start,
          rec.break_end,
        );

        if (!projectMap.has(pid)) {
          const projInfo = pid ? projectsMap.get(pid) : undefined;
          projectMap.set(pid, {
            project_number: projInfo?.project_number || undefined,
            project_name: projInfo?.name || undefined,
            userMap: new Map(),
          });
        }

        const pg = projectMap.get(pid)!;
        if (!pg.userMap.has(uid)) {
          pg.userMap.set(uid, { name: uName, hours: 0 });
        }
        pg.userMap.get(uid)!.hours += hours;
        continue;
      }

      // Use detailed project logs (started_at/ended_at or hours/minutes)
      for (const log of rec.attendance_project_logs) {
        let hours = 0;
        if (log.hours != null || log.minutes != null) {
          hours = (Number(log.hours) || 0) + (Number(log.minutes) || 0) / 60;
        } else if (log.started_at && log.ended_at) {
          hours =
            (new Date(log.ended_at).getTime() -
              new Date(log.started_at).getTime()) /
            (1000 * 60 * 60);
        } else {
          continue;
        }

        const pid = log.project_id;
        if (!projectMap.has(pid)) {
          const projInfo = projectsMap.get(pid);
          projectMap.set(pid, {
            project_number: projInfo?.project_number || undefined,
            project_name: projInfo?.name || undefined,
            userMap: new Map(),
          });
        }

        const pg = projectMap.get(pid)!;
        if (!pg.userMap.has(uid)) {
          pg.userMap.set(uid, { name: uName, hours: 0 });
        }
        pg.userMap.get(uid)!.hours += hours;
      }
    }

    const projects = Array.from(projectMap.entries()).map(([pid, pg]) => ({
      project_id: pid,
      project_number: pg.project_number,
      project_name: pg.project_name,
      hours:
        Math.round(
          Array.from(pg.userMap.values()).reduce((s, u) => s + u.hours, 0) * 10,
        ) / 10,
      users: Array.from(pg.userMap.entries()).map(([uid, ud]) => ({
        user_id: uid,
        user_name: ud.name,
        hours: Math.round(ud.hours * 10) / 10,
      })),
    }));

    months[String(m + 1)] = {
      month_name: MONTH_NAMES[m],
      projects,
    };
  }

  return { months };
}

export async function getPrintData(
  monthStr: string,
  filterUserId: number | null,
) {
  const [yearStr, monthNumStr] = monthStr.split("-");
  const yr = Number(yearStr);
  const mo = Number(monthNumStr);

  // shift_date is @db.Date (compared by UTC date part) → UTC-midnight month
  // boundaries, half-open range (local midnights pulled in the previous
  // month's last day).
  const monthStart = new Date(Date.UTC(yr, mo - 1, 1));
  const monthEnd = new Date(Date.UTC(yr, mo, 1));

  const users = await getAttendanceUsers();

  const where: Record<string, unknown> = {
    shift_date: { gte: monthStart, lt: monthEnd },
  };
  if (filterUserId) where.user_id = filterUserId;

  const records = await prisma.attendance.findMany({
    where,
    include: {
      users: { select: { id: true, first_name: true, last_name: true } },
      attendance_project_logs: {
        orderBy: { started_at: "asc" },
      },
    },
    orderBy: [{ users: { last_name: "asc" } }, { shift_date: "asc" }],
  });

  const fundHours = getBusinessDaysInMonth(yr, mo - 1) * 8;

  const typedRecords = records as AttendanceWithRelations[];

  const projectIds = [
    ...new Set(
      typedRecords
        .flatMap(
          (r) => r.attendance_project_logs?.map((l) => l.project_id) || [],
        )
        .filter(Boolean),
    ),
  ];
  const projectMap: Record<number, string> = {};
  if (projectIds.length > 0) {
    const projects = await prisma.projects.findMany({
      where: { id: { in: projectIds } },
      select: { id: true, name: true, project_number: true },
    });
    for (const p of projects) {
      projectMap[p.id] = p.project_number
        ? `${p.project_number} – ${p.name}`
        : p.name || `#${p.id}`;
    }
  }

  const userTotals: Record<string, Record<string, unknown>> = {};
  for (const rec of typedRecords) {
    const uid = String(rec.user_id);
    if (!userTotals[uid]) {
      const u = rec.users;
      userTotals[uid] = {
        name: u ? `${u.first_name} ${u.last_name}`.trim() : `User #${uid}`,
        minutes: 0,
        records: [],
        vacation_hours: 0,
        sick_hours: 0,
        unpaid_hours: 0,
        fund: fundHours,
        worked_hours: 0,
        covered: 0,
        missing: 0,
        overtime: 0,
      };
    }

    const projectLogs =
      rec.attendance_project_logs?.map((log) => ({
        project_id: log.project_id,
        project_name: projectMap[log.project_id] || null,
        hours: log.hours,
        minutes: log.minutes,
        started_at: log.started_at,
        ended_at: log.ended_at,
      })) || [];

    (userTotals[uid].records as unknown[]).push({
      ...rec,
      project_logs: projectLogs,
      project_name: projectLogs.length > 0 ? projectLogs[0].project_name : null,
    });

    const lt = (rec.leave_type as string) || "work";
    if (lt !== "work") {
      const hrs = Number(rec.leave_hours) || 8;
      if (lt === "vacation") (userTotals[uid].vacation_hours as number) += hrs;
      else if (lt === "sick") (userTotals[uid].sick_hours as number) += hrs;
      else if (lt === "unpaid") (userTotals[uid].unpaid_hours as number) += hrs;
      // "holiday" is auto-derived from Czech public holidays.
    } else if (rec.arrival_time && rec.departure_time) {
      const mins =
        calcWorkedHours(
          rec.arrival_time,
          rec.departure_time,
          rec.break_start,
          rec.break_end,
        ) * 60;
      (userTotals[uid].minutes as number) += Math.round(mins);
    }
  }

  for (const uid of Object.keys(userTotals)) {
    const ut = userTotals[uid];
    const workedH = Math.round(((ut.minutes as number) / 60) * 10) / 10;
    ut.worked_hours = workedH;
    const covered =
      workedH + (ut.vacation_hours as number) + (ut.sick_hours as number);
    ut.covered = Math.round(covered * 10) / 10;
    ut.missing = Math.max(
      0,
      Math.round(((ut.fund as number) - covered) * 10) / 10,
    );
    ut.overtime = Math.max(
      0,
      Math.round((covered - (ut.fund as number)) * 10) / 10,
    );
  }

  const leaveBalances: Record<string, Record<string, number>> = {};
  const balanceRecords = await prisma.leave_balances.findMany({
    where: { year: yr },
  });
  for (const bal of balanceRecords) {
    const uid = String(bal.user_id);
    leaveBalances[uid] = {
      vacation_total: Number(bal.vacation_total) || DEFAULT_VACATION_TOTAL,
      vacation_remaining:
        (Number(bal.vacation_total) || DEFAULT_VACATION_TOTAL) -
        (Number(bal.vacation_used) || 0),
    };
  }

  let selectedUserName = "";
  if (filterUserId) {
    const u = users.find((u) => u.id === filterUserId);
    if (u) selectedUserName = `${u.first_name} ${u.last_name}`.trim();
  }

  return {
    user_totals: userTotals,
    leave_balances: leaveBalances,
    users: users.map((u) => ({
      id: u.id,
      name: `${u.first_name} ${u.last_name}`.trim(),
    })),
    month: monthStr,
    month_name: `${MONTH_NAMES[mo - 1]} ${yr}`,
    selected_user: filterUserId,
    selected_user_name: selectedUserName,
    year: yr,
    fund: {
      business_days: getBusinessDaysInMonth(yr, mo - 1),
      hours: fundHours,
    },
  };
}

export async function getActiveProjects() {
  const activeProjects = await prisma.projects.findMany({
    where: { status: "aktivni" },
    select: { id: true, name: true, project_number: true },
    orderBy: { name: "asc" },
  });
  return activeProjects.map((p) => ({
    id: p.id,
    name: p.name,
    project_number: p.project_number ?? "",
  }));
}

export async function getProjectLogs(attendanceId: number) {
  return prisma.attendance_project_logs.findMany({
    where: { attendance_id: attendanceId },
    orderBy: { started_at: "asc" },
  });
}

export async function getLocationRecord(id: number) {
  return prisma.attendance.findUnique({
    where: { id },
    include: {
      users: { select: { id: true, first_name: true, last_name: true } },
    },
  });
}

export async function listAttendance(params: ListAttendanceParams) {
  const { page, limit, skip, order, isAdmin, authUserId } = params;

  const where: Record<string, unknown> = {};
  if (!isAdmin) {
    where.user_id = authUserId;
  } else if (params.userId) {
    where.user_id = params.userId;
  }
  if (params.month && params.year) {
    // shift_date is @db.Date: Prisma compares by UTC date part, so the month
    // boundaries must be UTC midnights. Local midnights shifted the window a
    // day back (included prev-month's last day, dropped this month's last day).
    where.shift_date = {
      gte: new Date(Date.UTC(params.year, params.month - 1, 1)),
      lt: new Date(Date.UTC(params.year, params.month, 1)),
    };
  }

  const [records, total] = await Promise.all([
    prisma.attendance.findMany({
      where,
      skip,
      take: limit,
      orderBy: { shift_date: order },
      include: {
        users: {
          select: {
            id: true,
            first_name: true,
            last_name: true,
            username: true,
          },
        },
        attendance_project_logs: { orderBy: { started_at: "asc" } },
      },
    }),
    prisma.attendance.count({ where }),
  ]);

  const allProjectIds = new Set<number>();
  for (const rec of records) {
    if (rec.project_id) allProjectIds.add(rec.project_id);
    for (const log of rec.attendance_project_logs) {
      allProjectIds.add(log.project_id);
    }
  }
  const projectNameMap = new Map<number, string>();
  if (allProjectIds.size > 0) {
    const projects = await prisma.projects.findMany({
      where: { id: { in: [...allProjectIds] } },
      select: { id: true, name: true, project_number: true },
    });
    for (const p of projects) {
      projectNameMap.set(
        p.id,
        p.project_number ? `${p.project_number} – ${p.name}` : p.name || "",
      );
    }
  }

  const enriched = records.map((rec) => {
    const logs = rec.attendance_project_logs.map((l) => ({
      ...l,
      project_name:
        projectNameMap.get(l.project_id) || `Projekt #${l.project_id}`,
    }));
    const u = rec.users;
    return {
      ...rec,
      user_name: u ? `${u.first_name} ${u.last_name}`.trim() : "",
      project_name: rec.project_id
        ? projectNameMap.get(rec.project_id) || null
        : null,
      project_logs: logs,
    };
  });

  return { records: enriched, total, page, limit };
}

export async function handleBalances(data: BalancesData) {
  const yr = data.year || new Date().getFullYear();

  if (data.action_type === "edit") {
    await prisma.leave_balances.upsert({
      where: { user_id_year: { user_id: data.user_id, year: yr } },
      update: {
        vacation_total:
          data.vacation_total != null ? Number(data.vacation_total) : undefined,
        vacation_used:
          data.vacation_used != null ? Number(data.vacation_used) : undefined,
        sick_used: data.sick_used != null ? Number(data.sick_used) : undefined,
        updated_at: new Date(),
      },
      create: {
        user_id: data.user_id,
        year: yr,
        vacation_total: Number(data.vacation_total) || DEFAULT_VACATION_TOTAL,
        vacation_used: Number(data.vacation_used) || 0,
        sick_used: Number(data.sick_used) || 0,
      },
    });
    return { success: true, message: "Bilance byla uložena", year: yr };
  }

  if (data.action_type === "reset") {
    await prisma.leave_balances.upsert({
      where: { user_id_year: { user_id: data.user_id, year: yr } },
      update: { vacation_used: 0, sick_used: 0, updated_at: new Date() },
      create: {
        user_id: data.user_id,
        year: yr,
        vacation_total: DEFAULT_VACATION_TOTAL,
        vacation_used: 0,
        sick_used: 0,
      },
    });
    return { success: true, message: "Bilance byla resetována", year: yr };
  }

  return { error: "Neplatný typ akce" };
}

export async function bulkCreateAttendance(data: BulkAttendanceData) {
  const [yrStr, moStr] = data.month.split("-");
  const yr = Number(yrStr);
  const mo = Number(moStr);
  const daysInMonth = new Date(yr, mo, 0).getDate();

  const dateFrom = new Date(yr, mo - 1, 1);
  const dateTo = new Date(yr, mo, 0, 23, 59, 59);
  const existing = await prisma.attendance.findMany({
    where: {
      user_id: { in: data.user_ids.map(Number) },
      shift_date: { gte: dateFrom, lte: dateTo },
    },
    select: { user_id: true, shift_date: true },
  });
  const existingSet = new Set(
    existing.map((r) => `${r.user_id}:${localDateStr(r.shift_date)}`),
  );

  let inserted = 0;
  let skipped = 0;

  await prisma.$transaction(async (tx) => {
    for (const userId of data.user_ids.map(Number)) {
      for (let day = 1; day <= daysInMonth; day++) {
        const date = new Date(yr, mo - 1, day);
        const dateStr = localDateStr(date);
        const dow = date.getDay();

        if (dow === 0 || dow === 6) continue;

        // Holidays are auto-paid via the fond in mzda math — skip them in
        // the bulk-create work-shift fill. The user shouldn't end up with
        // an explicit "holiday" record; the Svátek summary line and
        // attendance_hours are derived from Czech public holidays instead.
        if (isHoliday(dateStr)) {
          skipped++;
          continue;
        }

        if (existingSet.has(`${userId}:${dateStr}`)) {
          skipped++;
          continue;
        }

        const shiftDate = new Date(yr, mo - 1, day, 12, 0, 0);

        await tx.attendance.create({
          data: {
            user_id: userId,
            shift_date: shiftDate,
            arrival_time: new Date(`${dateStr}T${data.arrival_time}:00`),
            departure_time: new Date(`${dateStr}T${data.departure_time}:00`),
            break_start: new Date(`${dateStr}T${data.break_start_time}:00`),
            break_end: new Date(`${dateStr}T${data.break_end_time}:00`),
            leave_type: "work",
          },
        });
        inserted++;
      }
    }
  });

  let msg = `Vytvořeno ${inserted} záznamů`;
  if (skipped > 0) msg += ` (${skipped} přeskočeno — již existují)`;

  return { inserted, skipped, message: msg };
}

export async function createLeave(data: LeaveData, authUserId: number) {
  const userId = data.user_id ?? authUserId;
  const dateFrom = data.date_from;
  const dateTo = data.date_to || dateFrom;
  const leaveTypeStr = data.leave_type;
  if (
    !VALID_LEAVE_TYPES.includes(
      leaveTypeStr as (typeof VALID_LEAVE_TYPES)[number],
    )
  ) {
    return { error: "Neplatný typ nepřítomnosti" };
  }
  const leaveType = leaveTypeStr as attendance_leave_type;

  if (!dateFrom) return { error: "Datum je povinné" };

  const start = new Date(dateFrom);
  const end = new Date(dateTo);
  const perDayHours = data.leave_hours ? Number(data.leave_hours) : 8;
  let created = 0;

  try {
    await prisma.$transaction(async (tx) => {
      // A leave range can span a year boundary (e.g. Dec → Jan). Each day's
      // hours must be booked against ITS OWN year's balance, so accumulate
      // per calendar year rather than attributing the whole range to the
      // start year.
      const hoursByYear = new Map<number, number>();
      const current = new Date(start);
      while (current <= end) {
        const dow = current.getDay();
        if (dow !== 0 && dow !== 6 && !isHoliday(localDateStr(current))) {
          const shiftDate = new Date(
            current.getFullYear(),
            current.getMonth(),
            current.getDate(),
            12,
            0,
            0,
          );
          const duplicate = await tx.attendance.findFirst({
            where: { user_id: userId, shift_date: shiftDate },
          });
          if (duplicate) {
            throw new Error("Pro zvolené datumy již existují záznamy docházky");
          }
          await tx.attendance.create({
            data: {
              user_id: userId,
              shift_date: shiftDate,
              leave_type: leaveType,
              leave_hours: perDayHours,
              notes: data.notes ? String(data.notes) : null,
            },
          });
          created++;
          const dayYear = current.getFullYear();
          hoursByYear.set(
            dayYear,
            (hoursByYear.get(dayYear) ?? 0) + perDayHours,
          );
        }
        current.setDate(current.getDate() + 1);
      }

      if (leaveType === "vacation" || leaveType === "sick") {
        const updateField =
          leaveType === "vacation" ? "vacation_used" : "sick_used";
        for (const [year, yearHours] of hoursByYear) {
          if (yearHours <= 0) continue;
          const existingBalance = await tx.leave_balances.findFirst({
            where: { user_id: userId, year },
          });
          if (existingBalance) {
            await tx.leave_balances.update({
              where: { id: existingBalance.id },
              data: {
                [updateField]: Number(existingBalance[updateField]) + yearHours,
                updated_at: new Date(),
              },
            });
          } else {
            await tx.leave_balances.create({
              data: {
                user_id: userId,
                year,
                vacation_total: DEFAULT_VACATION_TOTAL,
                vacation_used: leaveType === "vacation" ? yearHours : 0,
                sick_used: leaveType === "sick" ? yearHours : 0,
              },
            });
          }
        }
      }
    });
  } catch (err) {
    if (
      err instanceof Error &&
      err.message === "Pro zvolené datumy již existují záznamy docházky"
    ) {
      return { error: err.message };
    }
    throw err;
  }

  return { created, message: `Vytvořeno ${created} záznamů nepřítomnosti` };
}

export async function punchAction(userId: number, data: PunchData) {
  const settings = await getSystemSettings();
  const action = data.punch_action;
  const now = new Date();
  const y = now.getFullYear(),
    m = now.getMonth(),
    d = now.getDate();
  const today = new Date(y, m, d, 12, 0, 0);

  const gpsLat =
    data.latitude != null && data.latitude !== ""
      ? Number(data.latitude)
      : null;
  const gpsLng =
    data.longitude != null && data.longitude !== ""
      ? Number(data.longitude)
      : null;
  const gpsAcc =
    data.accuracy != null && data.accuracy !== ""
      ? Number(data.accuracy)
      : null;
  const gpsAddr = data.address ?? null;

  if (action === "arrival") {
    return prisma.$transaction(async (tx) => {
      // Lock the user row first so a double-tap of "Příchod" (parallel POSTs
      // to /api/attendance/punch) cannot both see "no open shift" and both
      // create one. The second call waits here, then re-reads and bails.
      await lockUserRow(tx, userId);

      const ongoing = await tx.attendance.findFirst({
        where: {
          user_id: userId,
          departure_time: null,
          arrival_time: { not: null },
        },
        orderBy: { created_at: "desc" },
      });
      if (ongoing) {
        return {
          error: "Máte již aktivní směnu. Nejdříve zaznamenejte odchod.",
        };
      }

      const arrivalTime = roundUp(now, settings.clock_rounding_minutes);
      const record = await tx.attendance.create({
        data: {
          user_id: userId,
          shift_date: today,
          arrival_time: arrivalTime,
          arrival_lat: gpsLat,
          arrival_lng: gpsLng,
          arrival_accuracy: gpsAcc,
          arrival_address: gpsAddr,
          leave_type: "work",
        },
      });

      return {
        id: record.id,
        status: 201,
        message: "Příchod zaznamenán",
        auditAction: "create" as const,
        auditDescription: "Zaznamenán příchod",
      };
    });
  } else if (action === "departure") {
    return prisma.$transaction(async (tx) => {
      await lockUserRow(tx, userId);

      const ongoing = await tx.attendance.findFirst({
        where: {
          user_id: userId,
          departure_time: null,
          arrival_time: { not: null },
        },
        orderBy: { created_at: "desc" },
      });
      if (!ongoing) {
        return { error: "Nemáte aktivní směnu." };
      }

      const departureTime = roundDown(now, settings.clock_rounding_minutes);

      const updateData: Record<string, unknown> = {
        departure_time: departureTime,
        departure_lat: gpsLat,
        departure_lng: gpsLng,
        departure_accuracy: gpsAcc,
        departure_address: gpsAddr,
      };

      if (!ongoing.break_start && ongoing.arrival_time) {
        const shiftMs =
          departureTime.getTime() - ongoing.arrival_time.getTime();
        const shiftHours = shiftMs / (1000 * 60 * 60);
        if (shiftHours > settings.break_threshold_hours) {
          const midpoint = roundDown(
            new Date(ongoing.arrival_time.getTime() + shiftMs / 2),
            settings.clock_rounding_minutes,
          );
          const breakMins =
            shiftHours > settings.break_threshold_hours * 2
              ? settings.break_duration_long
              : settings.break_duration_short;
          updateData.break_start = midpoint;
          updateData.break_end = new Date(
            midpoint.getTime() + breakMins * 60 * 1000,
          );
        }
      }

      await tx.attendance.update({
        where: { id: ongoing.id },
        data: updateData,
      });

      // End active project logs, ensuring ended_at is never before started_at
      const activeLogs = await tx.attendance_project_logs.findMany({
        where: { attendance_id: ongoing.id, ended_at: null },
      });
      for (const log of activeLogs) {
        const endedAt =
          log.started_at && log.started_at > departureTime
            ? log.started_at
            : departureTime;
        await tx.attendance_project_logs.update({
          where: { id: log.id },
          data: { ended_at: endedAt },
        });
      }

      return {
        id: ongoing.id,
        status: 200,
        message: "Odchod zaznamenán",
        auditAction: "update" as const,
        auditDescription: "Zaznamenán odchod",
      };
    });
  } else if (action === "break_start") {
    return prisma.$transaction(async (tx) => {
      await lockUserRow(tx, userId);

      const ongoing = await tx.attendance.findFirst({
        where: {
          user_id: userId,
          departure_time: null,
          arrival_time: { not: null },
          break_start: null,
        },
        orderBy: { created_at: "desc" },
      });
      if (!ongoing) {
        return { error: "Nemáte aktivní směnu bez přestávky." };
      }

      let msRound = settings.clock_rounding_minutes * 60 * 1000;
      if (
        !settings.clock_rounding_minutes ||
        settings.clock_rounding_minutes <= 0
      ) {
        msRound = 0;
      }
      const breakStart =
        msRound > 0
          ? new Date(Math.round(now.getTime() / msRound) * msRound)
          : now;
      const breakEnd = new Date(
        breakStart.getTime() + settings.break_duration_long * 60 * 1000,
      );

      await tx.attendance.update({
        where: { id: ongoing.id },
        data: { break_start: breakStart, break_end: breakEnd },
      });

      return {
        id: ongoing.id,
        status: 200,
        message: "Přestávka zaznamenána",
        auditAction: "update" as const,
        auditDescription: "Zaznamenána přestávka",
      };
    });
  }

  return { error: "Neplatná akce" };
}

export async function createAttendance(
  data: CreateAttendanceData,
  authUserId: number,
) {
  const userId = data.user_id ?? authUserId;
  const shiftDate = new Date(data.shift_date);
  // shift_date arrives as "YYYY-MM-DD" → parsed as UTC midnight, and the
  // @db.Date column is compared by UTC date part. The duplicate/overlap
  // window must therefore be the UTC day [shiftDate, shiftDate+1) — building
  // it from LOCAL getters made both bounds 22:00/23:00 UTC of the previous
  // day, so the validation queried ONLY yesterday's records (same-day
  // overlaps undetected, false duplicates when yesterday had records).
  const startOfDay = new Date(
    Date.UTC(
      shiftDate.getUTCFullYear(),
      shiftDate.getUTCMonth(),
      shiftDate.getUTCDate(),
    ),
  );
  const endOfDay = new Date(
    Date.UTC(
      shiftDate.getUTCFullYear(),
      shiftDate.getUTCMonth(),
      shiftDate.getUTCDate() + 1,
    ),
  );

  // Multiple work shifts per day are allowed — only block when the new
  // shift's time range overlaps an existing one. Two leave-type records
  // on the same day still make no sense and are blocked.
  const dayRecords = await prisma.attendance.findMany({
    where: {
      user_id: userId,
      shift_date: { gte: startOfDay, lt: endOfDay },
    },
    select: {
      id: true,
      leave_type: true,
      arrival_time: true,
      departure_time: true,
    },
  });

  const isLeave = (data.leave_type as string) !== "work";
  if (isLeave) {
    const otherLeave = dayRecords.find(
      (r) => (r.leave_type as string) !== "work",
    );
    if (otherLeave) {
      return {
        error:
          "Pro zvolené datum již existuje záznam o nepřítomnosti (dovolená / nemoc / neplacené volno).",
        status: 400,
      };
    }
  } else if (data.arrival_time && data.departure_time) {
    const newStart = new Date(data.arrival_time).getTime();
    const newEnd = new Date(data.departure_time).getTime();
    for (const r of dayRecords) {
      if ((r.leave_type as string) !== "work") continue;
      if (!r.arrival_time || !r.departure_time) continue;
      const eStart = new Date(r.arrival_time).getTime();
      const eEnd = new Date(r.departure_time).getTime();
      // Half-open overlap: [newStart, newEnd) ∩ [eStart, eEnd) ≠ ∅
      if (newStart < eEnd && eStart < newEnd) {
        return {
          error: "Pro zvolený čas se směna překrývá s jinou směnou tentýž den.",
          status: 400,
        };
      }
    }
  } else {
    // Work shift with missing arrival/departure — block any other record
    // on the same day so the user doesn't end up with two ambiguous rows.
    if (dayRecords.length > 0) {
      return {
        error:
          "Pro zvolené datum již existuje záznam docházky. Doplňte příchod a odchod, nebo smažte/upravte stávající záznam.",
        status: 400,
      };
    }
  }

  const record = await prisma.attendance.create({
    data: {
      user_id: userId,
      shift_date: shiftDate,
      arrival_time: data.arrival_time ? new Date(data.arrival_time) : null,
      arrival_lat: data.arrival_lat ?? null,
      arrival_lng: data.arrival_lng ?? null,
      arrival_accuracy: data.arrival_accuracy ?? null,
      arrival_address: data.arrival_address ?? null,
      departure_time: data.departure_time
        ? new Date(data.departure_time)
        : null,
      departure_lat: data.departure_lat ?? null,
      departure_lng: data.departure_lng ?? null,
      departure_accuracy: data.departure_accuracy ?? null,
      departure_address: data.departure_address ?? null,
      break_start: data.break_start ? new Date(data.break_start) : null,
      break_end: data.break_end ? new Date(data.break_end) : null,
      notes: data.notes ?? null,
      project_id: data.project_id ?? null,
      leave_type: data.leave_type as attendance_leave_type,
      leave_hours: data.leave_hours ?? null,
    },
  });

  if (Array.isArray(data.project_logs)) {
    const logs = data.project_logs.filter(
      (l) => l.project_id && (Number(l.hours) > 0 || Number(l.minutes) > 0),
    );
    if (logs.length > 0) {
      await prisma.attendance_project_logs.createMany({
        data: logs.map((l) => ({
          attendance_id: record.id,
          project_id: Number(l.project_id),
          hours: Number(l.hours) || 0,
          minutes: Number(l.minutes) || 0,
        })),
      });
    }
  }

  return { id: record.id };
}

export async function updateAttendance(
  id: number,
  data: UpdateAttendanceData,
  authUserId: number,
  isAdmin: boolean,
) {
  const existing = await prisma.attendance.findUnique({ where: { id } });
  if (!existing) return { error: "Záznam nenalezen", status: 404 };

  if (existing.user_id !== authUserId && !isAdmin) {
    return { error: "Nemáte oprávnění upravit tento záznam", status: 403 };
  }

  await prisma.attendance.update({
    where: { id },
    data: {
      arrival_time:
        data.arrival_time !== undefined
          ? data.arrival_time
            ? new Date(String(data.arrival_time))
            : null
          : undefined,
      departure_time:
        data.departure_time !== undefined
          ? data.departure_time
            ? new Date(String(data.departure_time))
            : null
          : undefined,
      break_start:
        data.break_start !== undefined
          ? data.break_start
            ? new Date(String(data.break_start))
            : null
          : undefined,
      break_end:
        data.break_end !== undefined
          ? data.break_end
            ? new Date(String(data.break_end))
            : null
          : undefined,
      notes:
        data.notes !== undefined
          ? data.notes
            ? String(data.notes)
            : null
          : undefined,
      project_id:
        data.project_id !== undefined
          ? data.project_id
            ? Number(data.project_id)
            : null
          : undefined,
      leave_type:
        data.leave_type !== undefined
          ? (String(data.leave_type) as attendance_leave_type)
          : undefined,
      leave_hours:
        data.leave_hours !== undefined
          ? data.leave_hours
            ? Number(data.leave_hours)
            : null
          : undefined,
    },
  });

  if (Array.isArray(data.project_logs)) {
    await prisma.attendance_project_logs.deleteMany({
      where: { attendance_id: id },
    });
    const logs = data.project_logs.filter(
      (l) => l.project_id && (Number(l.hours) > 0 || Number(l.minutes) > 0),
    );
    if (logs.length > 0) {
      await prisma.attendance_project_logs.createMany({
        data: logs.map((l) => ({
          attendance_id: id,
          project_id: Number(l.project_id),
          hours: Number(l.hours) || 0,
          minutes: Number(l.minutes) || 0,
        })),
      });
    }
  }

  return { id };
}

export async function deleteAttendance(
  id: number,
  requesterId?: number,
  isAdmin?: boolean,
) {
  const existing = await prisma.attendance.findUnique({ where: { id } });
  if (!existing) return { error: "Záznam nenalezen" };

  // Ownership is enforced ONLY when a requester is supplied, keeping the
  // signature backward compatible (existing callers pass nothing and skip
  // the check, behaving exactly as before).
  if (
    requesterId !== undefined &&
    !isAdmin &&
    existing.user_id !== requesterId
  ) {
    return { error: "Nemáte oprávnění smazat tento záznam", status: 403 };
  }

  await prisma.attendance.delete({ where: { id } });
  return { success: true };
}
