import { Types } from "mongoose";
import DesktopSession, {
  LOG_TYPES,
  LogType,
  IOsInfo,
  BREAK_TYPES,
  ALL_BREAK_TYPES,
  AnyBreakType,
} from "../models/desktopSession";
import User from "../models/users";
import { toKolkataTime, fromKolkataTime } from "./../utils/timezone";
import logger from "../utils/logger";

const err = (msg: string, code = 400): Error & { statusCode: number } =>
  Object.assign(new Error(msg), { statusCode: code });

const paginationMeta = (total: number, page: number, limit: number) => ({
  total,
  page,
  limit,
  totalPages: Math.ceil(total / limit),
  hasNextPage: page < Math.ceil(total / limit),
  hasPrevPage: page > 1,
});

// ─── Day-summary grouping (admin views) ──────────────────────────────────────

interface PopulatedUser {
  _id: Types.ObjectId;
  name: string;
  email: string;
  department: string | null;
}

const dateKey = (d: Date) => toKolkataTime(d, "yyyy-MM-dd");
const timeKey = (d: Date) => toKolkataTime(d, "HH:mm:ss");
const createdAtKey = (d: Date) => toKolkataTime(d, "yyyy-MM-dd HH:mm:ss");

interface LogEntry {
  log_type: LogType;
  timestamp: Date;
  createdAt: Date;
  ip_address?: string;
  os_info?: IOsInfo;
  device_name?: string;
  break_type?: AnyBreakType;
  end_time?: Date;
  duration_seconds?: number;
  is_auto_checkout?: boolean;
}

const buildDaySummary = (
  user: PopulatedUser,
  date: string,
  logs: LogEntry[],
) => {
  const sorted = [...logs].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());

  let totalBreakSeconds = 0;
  let totalIdleSeconds = 0;
  let totalInterruptedSeconds = 0;
  let workingSeconds = 0;
  let openBreakStart: Date | null = null;
  let openIdleStart: Date | null = null;
  let openLoginStart: Date | null = null;
  let breakSecondsAtLoginStart = 0;
  let idleSecondsAtLoginStart = 0;
  let interruptedSecondsAtLoginStart = 0;

  for (const log of sorted) {
    if (log.log_type === "BREAK_START") {
      openBreakStart = log.timestamp;
    } else if (log.log_type === "BREAK_END" && openBreakStart) {
      totalBreakSeconds += Math.round((log.timestamp.getTime() - openBreakStart.getTime()) / 1000);
      openBreakStart = null;
    } else if (log.log_type === "IDLE" && !openIdleStart) {
      openIdleStart = log.timestamp;
    } else if (log.log_type === "ACTIVE" && openIdleStart) {
      totalIdleSeconds += Math.round((log.timestamp.getTime() - openIdleStart.getTime()) / 1000);
      openIdleStart = null;
    } else if (log.log_type === "BREAK" && log.break_type === "TRACKING_INTERRUPTED") {
      // Logged after the fact (app was closed / offline), so it carries its own
      // start/end instead of arriving as a live BREAK_START/BREAK_END pair.
      totalInterruptedSeconds +=
        log.duration_seconds ??
        (log.end_time ? Math.round((log.end_time.getTime() - log.timestamp.getTime()) / 1000) : 0);
    } else if (log.log_type === "CHECKIN" && !openLoginStart) {
      openLoginStart = log.timestamp;
      breakSecondsAtLoginStart = totalBreakSeconds;
      idleSecondsAtLoginStart = totalIdleSeconds;
      interruptedSecondsAtLoginStart = totalInterruptedSeconds;
    } else if (log.log_type === "CHECKOUT" && openLoginStart) {
      // Breaks, idle, and interrupted time that fell inside this specific
      // LOGIN → LOGOUT segment only.
      const segmentBreakSeconds = totalBreakSeconds - breakSecondsAtLoginStart;
      const segmentIdleSeconds = totalIdleSeconds - idleSecondsAtLoginStart;
      const segmentInterruptedSeconds = totalInterruptedSeconds - interruptedSecondsAtLoginStart;
      const segmentSeconds =
        Math.round((log.timestamp.getTime() - openLoginStart.getTime()) / 1000) -
        segmentBreakSeconds -
        segmentIdleSeconds -
        segmentInterruptedSeconds;
      workingSeconds += Math.max(0, segmentSeconds);
      openLoginStart = null;
    }
  }

  const lastLog = sorted[sorted.length - 1];
  const status = lastLog?.log_type === "CHECKOUT" ? "Completed" : "In Progress";
  const workingTime = workingSeconds;

  const firstCheckIn = sorted.find((l) => l.log_type === "CHECKIN");
  const checkOuts = sorted.filter((l) => l.log_type === "CHECKOUT");
  const lastCheckOut = checkOuts[checkOuts.length - 1];
  const totalCheckinTime =
    firstCheckIn && lastCheckOut
      ? Math.max(0, Math.round((lastCheckOut.timestamp.getTime() - firstCheckIn.timestamp.getTime()) / 1000))
      : 0;

  return {
    user: { _id: user._id, name: user.name, email: user.email, department: user.department ?? null },
    date,
    logs: sorted.map((l) => ({
      type: l.log_type,
      time: timeKey(l.timestamp),
      createdAt: createdAtKey(l.createdAt ?? l.timestamp),
      ip_address: l.ip_address,
      os_info: l.os_info,
      device_name: l.device_name,
      break_type: l.break_type,
      end_time: l.end_time ? createdAtKey(l.end_time) : undefined,
      duration_seconds: l.duration_seconds,
      is_auto_checkout: l.log_type === "CHECKOUT" ? !!l.is_auto_checkout : undefined,
    })),
    firstCheckIn: firstCheckIn ? timeKey(firstCheckIn.timestamp) : null,
    lastCheckOut: lastCheckOut ? timeKey(lastCheckOut.timestamp) : null,
    // True when the day's closing CHECKOUT was inserted by the auto-checkout
    // cron rather than the employee checking out themselves.
    autoCheckedOut: !!lastCheckOut?.is_auto_checkout,
    totalCheckinTime,
    totalBreakTime: totalBreakSeconds,
    totalIdleTime: totalIdleSeconds,
    totalInterruptedTime: totalInterruptedSeconds,
    workingTime,
    status,
  };
};

const groupByUserAndDate = (
  rawSessions: Array<{ user: PopulatedUser } & LogEntry>,
) => {
  const groups = new Map<
    string,
    { user: PopulatedUser; date: string; logs: LogEntry[] }
  >();

  for (const session of rawSessions) {
    const date = dateKey(session.timestamp);
    const key = `${session.user._id.toString()}_${date}`;
    if (!groups.has(key)) groups.set(key, { user: session.user, date, logs: [] });
    groups.get(key)!.logs.push({
      log_type: session.log_type,
      timestamp: session.timestamp,
      createdAt: session.createdAt,
      ip_address: session.ip_address,
      os_info: session.os_info,
      device_name: session.device_name,
      break_type: session.break_type,
      end_time: session.end_time,
      duration_seconds: session.duration_seconds,
      is_auto_checkout: session.is_auto_checkout,
    });
  }

  return Array.from(groups.values())
    .map((g) => buildDaySummary(g.user, g.date, g.logs))
    .sort((a, b) =>
      a.date === b.date ? a.user.name.localeCompare(b.user.name) : a.date < b.date ? 1 : -1,
    );
};

// ─── Employee ────────────────────────────────────────────────────────────────

export const getBreakTypes = () => [...BREAK_TYPES];

export const createBreak = async (
  userId: string,
  type: string,
  start_time: string | undefined,
  end_time: string | undefined,
  duration_seconds: unknown,
  ip_address?: string,
  os_info?: IOsInfo,
  device_name?: string,
) => {
  if (!ALL_BREAK_TYPES.includes(type as AnyBreakType)) {
    throw err(`type must be one of: ${ALL_BREAK_TYPES.join(", ")}`);
  }

  const seconds = Number(duration_seconds);
  if (!Number.isFinite(seconds) || seconds <= 0) {
    throw err("duration_seconds must be a positive number");
  }

  let startTime = new Date();
  if (start_time !== undefined) {
    try {
      startTime = fromKolkataTime(start_time);
    } catch {
      throw err('Invalid start_time format, expected "yyyy-MM-dd HH:mm:ss"');
    }
  }

  if (end_time === undefined) {
    throw err("end_time is required");
  }
  let endTime: Date;
  try {
    endTime = fromKolkataTime(end_time);
  } catch {
    throw err('Invalid end_time format, expected "yyyy-MM-dd HH:mm:ss"');
  }

  if (endTime <= startTime) {
    throw err("end_time must be after start_time");
  }

  // A break only counts against an active working session, so the employee
  // must currently be checked in (last CHECKIN/CHECKOUT event is a CHECKIN).
  const lastCheck = await DesktopSession.findOne({
    user: userId,
    log_type: { $in: ["CHECKIN", "CHECKOUT"] },
  })
    .sort({ timestamp: -1 })
    .select("log_type")
    .lean();

  if (!lastCheck || lastCheck.log_type !== "CHECKIN") {
    throw err("You must be checked in to log a break");
  }

  const session = await DesktopSession.create({
    user: userId,
    log_type: "BREAK" as LogType,
    timestamp: startTime,
    break_type: type as AnyBreakType,
    end_time: endTime,
    duration_seconds: seconds,
    ip_address,
    os_info: os_info ? { os: os_info.os, network: os_info.network } : undefined,
    device_name,
  });

  return {
    _id: session._id,
    log_type: session.log_type,
    type: session.break_type,
    start_time: toKolkataTime(session.timestamp, "yyyy-MM-dd HH:mm:ss"),
    end_time: session.end_time ? toKolkataTime(session.end_time, "yyyy-MM-dd HH:mm:ss") : undefined,
    duration_seconds: session.duration_seconds,
    createdAt: toKolkataTime(session.createdAt ?? session.timestamp, "yyyy-MM-dd HH:mm:ss"),
    ip_address: session.ip_address,
    os_info: session.os_info,
    device_name: session.device_name,
  };
};

export const createSession = async (
  userId: string,
  log_type: string,
  timestamp?: string,
  ip_address?: string,
  os_info?: IOsInfo,
  device_name?: string,
  isAutoCheckout = false,
) => {
  if (!LOG_TYPES.includes(log_type as LogType)) {
    throw err(`log_type must be one of: ${LOG_TYPES.join(", ")}`);
  }

  let eventTime = new Date();
  if (timestamp !== undefined) {
    try {
      eventTime = fromKolkataTime(timestamp);
    } catch {
      throw err('Invalid timestamp format, expected "yyyy-MM-dd HH:mm:ss"');
    }
  }

  const session = await DesktopSession.create({
    user: userId,
    log_type: log_type as LogType,
    timestamp: eventTime,
    ip_address,
    os_info: os_info ? { os: os_info.os, network: os_info.network } : undefined,
    device_name,
    is_auto_checkout: log_type === "CHECKOUT" ? isAutoCheckout : undefined,
  });

  return {
    _id: session._id,
    log_type: session.log_type,
    timestamp: toKolkataTime(session.timestamp, "yyyy-MM-dd HH:mm:ss"),
    createdAt: toKolkataTime(session.createdAt ?? session.timestamp, "yyyy-MM-dd HH:mm:ss"),
    ip_address: session.ip_address,
    os_info: session.os_info,
    device_name: session.device_name,
    is_auto_checkout: session.is_auto_checkout,
  };
};

export const getMySessions = async (userId: string, page = 1, limit = 50) => {
  const skip = (page - 1) * limit;
  const [rawSessions, total] = await Promise.all([
    DesktopSession.find({ user: userId })
      .select("log_type timestamp end_time createdAt ip_address os_info device_name break_type duration_seconds is_auto_checkout")
      .sort({ timestamp: -1 })
      .skip(skip)
      .limit(limit)
      .lean(),
    DesktopSession.countDocuments({ user: userId }),
  ]);

  const sessions = rawSessions.map((s) => ({
    _id: s._id,
    log_type: s.log_type,
    timestamp: toKolkataTime(s.timestamp, "yyyy-MM-dd HH:mm:ss"),
    createdAt: toKolkataTime(s.createdAt ?? s.timestamp, "yyyy-MM-dd HH:mm:ss"),
    ip_address: s.ip_address,
    os_info: s.os_info,
    device_name: s.device_name,
    break_type: s.break_type,
    end_time: s.end_time ? toKolkataTime(s.end_time, "yyyy-MM-dd HH:mm:ss") : undefined,
    duration_seconds: s.duration_seconds,
    is_auto_checkout: s.is_auto_checkout,
  }));

  return { sessions, pagination: paginationMeta(total, page, limit) };
};

// @desc  Per-day attendance card for the logged-in employee (checkin/checkout/
// work/break/idle/interrupted/total + status), mirroring the admin day-summary
// but scoped to the caller and shaped for direct table display.
// Validates an optional from_date/to_date pair ("yyyy-MM-dd", Asia/Kolkata days)
// and returns the matching UTC timestamp range, or null when neither is given.
const parseDayRange = (fromDate?: string, toDate?: string) => {
  if (fromDate === undefined && toDate === undefined) return null;
  if (!fromDate || !toDate) throw err("from_date and to_date must be provided together");

  const toDayStart = (value: string, field: string): Date => {
    // Round-trip through the day key so impossible dates like 2026-02-30 fail.
    if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
      throw err(`Invalid ${field}, expected "yyyy-MM-dd"`);
    }
    const start = fromKolkataTime(`${value} 00:00:00`);
    if (dateKey(start) !== value) throw err(`Invalid ${field}, expected "yyyy-MM-dd"`);
    return start;
  };

  const start = toDayStart(fromDate, "from_date");
  const toStart = toDayStart(toDate, "to_date");
  if (start > toStart) throw err("from_date must be on or before to_date");

  return { start, end: new Date(toStart.getTime() + 24 * 60 * 60 * 1000) };
};

export const getMyDaySummary = async (
  userId: string,
  page = 1,
  limit = 30,
  fromDate?: string,
  toDate?: string,
) => {
  const range = parseDayRange(fromDate, toDate);
  const rawSessions = await DesktopSession.find({
    user: userId,
    ...(range && { timestamp: { $gte: range.start, $lt: range.end } }),
  })
    .populate<{ user: PopulatedUser }>("user", "name email department")
    .sort({ timestamp: 1 })
    .lean();

  const grouped = groupByUserAndDate(rawSessions);
  const total = grouped.length;
  const skip = (page - 1) * limit;

  const days = grouped.slice(skip, skip + limit).map((day) => {
    const checkIn = day.logs.find((l) => l.type === "CHECKIN");
    const checkOuts = day.logs.filter((l) => l.type === "CHECKOUT");
    const lastCheckOut = checkOuts[checkOuts.length - 1];

    return {
      date: day.date.split("-").reverse().join("-"), // yyyy-MM-dd -> dd-MM-yyyy
      checkin: checkIn ? checkIn.time.slice(0, 5) : "-",
      checkout: lastCheckOut ? lastCheckOut.time.slice(0, 5) : "-",
      auto_checkout: !!lastCheckOut?.is_auto_checkout,
      work: day.workingTime,
      break: day.totalBreakTime,
      idle: day.totalIdleTime,
      interrupted: day.totalInterruptedTime,
      total: day.totalCheckinTime,
      status: day.status,
    };
  });

  return { days, pagination: paginationMeta(total, page, limit) };
};

// ─── Admin ───────────────────────────────────────────────────────────────────

export const getAllSessions = async (page = 1, limit = 50) => {
  const rawSessions = await DesktopSession.find()
    .populate<{ user: PopulatedUser }>("user", "name email department")
    .sort({ timestamp: 1 })
    .lean();

  const grouped = groupByUserAndDate(rawSessions);
  const total = grouped.length;
  const skip = (page - 1) * limit;
  const sessions = grouped.slice(skip, skip + limit);

  return { sessions, pagination: paginationMeta(total, page, limit) };
};

export const getEmployeeSessions = async (
  employeeId: string,
  page = 1,
  limit = 50,
) => {
  if (!Types.ObjectId.isValid(employeeId)) throw err("Invalid employee_id");

  const employee = await User.findById(employeeId).select("name email department").lean();
  if (!employee) throw err("Employee not found", 404);

  const rawSessions = await DesktopSession.find({ user: employeeId })
    .populate<{ user: PopulatedUser }>("user", "name email department")
    .sort({ timestamp: 1 })
    .lean();

  const grouped = groupByUserAndDate(rawSessions);
  const total = grouped.length;
  const skip = (page - 1) * limit;
  const sessions = grouped.slice(skip, skip + limit);

  return { sessions, pagination: paginationMeta(total, page, limit) };
};

export const deleteOldSessions = async () => {
  const cutoff = new Date();
  cutoff.setMonth(cutoff.getMonth() - 1);

  const result = await DesktopSession.deleteMany({
    timestamp: { $lt: cutoff },
  });
  return { deletedCount: result.deletedCount || 0, cutoff };
};

// ─── Cron: Daily Attendance Summary ──────────────────────────────────────────

export interface DailyAttendanceRow {
  userId: string;
  name: string;
  email: string;
  department: string | null;
  firstCheckIn: string | null;
  lastCheckOut: string | null;
  workingTime: number;
  totalBreakTime: number;
  totalIdleTime: number;
  totalInterruptedTime: number;
  autoCheckedOut: boolean;
  status: "Completed" | "In Progress" | "Absent";
}

// @desc  Per-employee attendance for one Asia/Kolkata calendar day
// (`date` = "yyyy-MM-dd"). Present employees go through the same
// groupByUserAndDate → buildDaySummary path as the admin dashboard so the
// numbers match. Active employees with no logs that day are added as Absent.
export const getDailyAttendanceReport = async (date: string): Promise<DailyAttendanceRow[]> => {
  const dayStart = fromKolkataTime(`${date} 00:00:00`);
  const dayEnd = new Date(dayStart.getTime() + 24 * 60 * 60 * 1000);

  const [rawSessions, activeUsers] = await Promise.all([
    DesktopSession.find({ timestamp: { $gte: dayStart, $lt: dayEnd } })
      .populate<{ user: PopulatedUser | null }>("user", "name email department")
      .sort({ timestamp: 1 })
      .lean(),
    User.find({ status: "active" }).select("name email department").lean(),
  ]);

  // Logs whose user has since been deleted populate to null — skip them.
  const withUser = rawSessions.filter(
    (s): s is typeof s & { user: PopulatedUser } => s.user !== null,
  );

  const present: DailyAttendanceRow[] = groupByUserAndDate(withUser).map((day) => ({
    userId: day.user._id.toString(),
    name: day.user.name,
    email: day.user.email,
    department: day.user.department,
    firstCheckIn: day.firstCheckIn,
    lastCheckOut: day.lastCheckOut,
    workingTime: day.workingTime,
    totalBreakTime: day.totalBreakTime,
    totalIdleTime: day.totalIdleTime,
    totalInterruptedTime: day.totalInterruptedTime,
    autoCheckedOut: day.autoCheckedOut,
    status: day.status as DailyAttendanceRow["status"],
  }));

  const presentIds = new Set(present.map((r) => r.userId));
  const absent: DailyAttendanceRow[] = activeUsers
    .filter((u) => !presentIds.has(u._id.toString()))
    .map((u) => ({
      userId: u._id.toString(),
      name: u.name,
      email: u.email,
      department: u.department ?? null,
      firstCheckIn: null,
      lastCheckOut: null,
      workingTime: 0,
      totalBreakTime: 0,
      totalIdleTime: 0,
      totalInterruptedTime: 0,
      autoCheckedOut: false,
      status: "Absent",
    }));

  return [...present, ...absent].sort((a, b) => a.name.localeCompare(b.name));
};

// ─── Cron: Auto Checkout ─────────────────────────────────────────────────────

// @desc  Auto-checkout employees who checked in today (Asia/Kolkata) and are
// still checked in. Only today's CHECKIN/CHECKOUT events are considered — the
// user's last one decides: CHECKOUT means already checked out (skipped),
// CHECKIN means still open. IDLE/ACTIVE/BREAK events are ignored since they
// keep arriving after a check-in. Open check-ins from previous days are left
// untouched. Each open session is closed at 20:00 of today via the same
// `createSession` path a manual checkout uses, so `buildDaySummary` computes
// its break/idle/working time exactly as it would for a real checkout.
export const autoCheckoutTodaySessions = async () => {
  const todayKey = dateKey(new Date());
  const dayStart = fromKolkataTime(`${todayKey} 00:00:00`);
  const dayEnd = new Date(dayStart.getTime() + 24 * 60 * 60 * 1000);

  const lastLogs: Array<{ _id: Types.ObjectId; log_type: LogType; timestamp: Date }> =
    await DesktopSession.aggregate([
      {
        $match: {
          log_type: { $in: ["CHECKIN", "CHECKOUT"] },
          timestamp: { $gte: dayStart, $lt: dayEnd },
        },
      },
      { $sort: { user: 1, timestamp: -1 } },
      {
        $group: {
          _id: "$user",
          log_type: { $first: "$log_type" },
          timestamp: { $first: "$timestamp" },
        },
      },
      { $match: { log_type: "CHECKIN" } },
    ]);

  const eightPm = fromKolkataTime(`${todayKey} 20:00:00`);

  const checkedOutUserIds: string[] = [];
  const failedUserIds: string[] = [];

  for (const log of lastLogs) {
    const userId = log._id.toString();
    const checkinDateKey = todayKey;
    // Guard a check-in logged after 8 PM (late shift) so the auto-checkout
    // never lands before its own check-in.
    const checkoutTime = eightPm > log.timestamp ? eightPm : log.timestamp;
    const checkoutTimeStr = toKolkataTime(checkoutTime, "yyyy-MM-dd HH:mm:ss");

    try {
      await createSession(
        userId,
        "CHECKOUT",
        checkoutTimeStr,
        undefined,
        undefined,
        undefined,
        true,
      );
      checkedOutUserIds.push(userId);
      logger.info("Auto-checkout: closed open session", {
        userId,
        checkinDate: checkinDateKey,
        checkoutTime: checkoutTimeStr,
      });
    } catch (error) {
      failedUserIds.push(userId);
      logger.error("Auto-checkout: failed to close open session", {
        userId,
        checkinDate: checkinDateKey,
        error: (error as Error).message,
      });
    }
  }

  if (checkedOutUserIds.length > 0) {
    logger.info(`Auto-checkout: closed ${checkedOutUserIds.length} open attendance session(s)`, {
      count: checkedOutUserIds.length,
      userIds: checkedOutUserIds,
    });
  }

  return {
    checkedOut: checkedOutUserIds.length,
    userIds: checkedOutUserIds,
    failed: failedUserIds.length,
    failedUserIds,
  };
};
