import express from "express";
import fs from "fs";
import path from "path";
import crypto from "crypto";
import multer from "multer";

import { requireAuth, requireRole } from "../../middleware/auth.js";
import { validate } from "../../shared/validate.js";
import { httpError } from "../../shared/errors.js";
import { Attendance } from "./attendance.model.js";
import { User } from "../users/user.model.js";
import { Channel, Message } from "../chat/chat.model.js";
import { LeaveRequest } from "../leave/leave.model.js";
import {
  AttendancePolicy,
  getAttendancePolicy,
} from "./attendancePolicy.model.js";
import {
  checkInSchema,
  checkOutSchema,
  listAttendanceSchema,
  listMyAttendanceSchema,
  listAllAttendanceSchema,
  approveAttendanceSchema,
  updateAttendancePolicySchema,
  monthlyReportSchema,
} from "./attendance.schemas.js";

function yyyyMmDd(date) {
  const year = date.getFullYear();
  const month = `${date.getMonth() + 1}`.padStart(2, "0");
  const day = `${date.getDate()}`.padStart(2, "0");
  return `${year}-${month}-${day}`;
}

function parseTimeToHoursMinutes(time) {
  const [hh, mm] = String(time || "").split(":");
  const h = Number(hh);
  const m = Number(mm);
  if (!Number.isFinite(h) || !Number.isFinite(m)) return null;
  return { h, m };
}

function calculateLateStatus(checkInTime, expectedStartTime) {
  const parsed = parseTimeToHoursMinutes(expectedStartTime);
  if (!parsed) return { isLate: false, lateByMinutes: 0 };

  const expectedTime = new Date(checkInTime);
  expectedTime.setHours(parsed.h, parsed.m, 0, 0);

  if (checkInTime > expectedTime) {
    const diffMs = checkInTime - expectedTime;
    const diffMins = Math.floor(diffMs / (1000 * 60));
    return { isLate: true, lateByMinutes: diffMins };
  }
  return { isLate: false, lateByMinutes: 0 };
}

function yyyyMmDdToUtcDate(day) {
  // day: YYYY-MM-DD
  const [y, m, d] = String(day)
    .split("-")
    .map((n) => Number(n));
  return new Date(Date.UTC(y, (m || 1) - 1, d || 1));
}

function addDaysUtc(date, days) {
  const ms = date.getTime() + days * 24 * 60 * 60 * 1000;
  return new Date(ms);
}

function toYyyyMmDdUtc(date) {
  const y = date.getUTCFullYear();
  const m = `${date.getUTCMonth() + 1}`.padStart(2, "0");
  const d = `${date.getUTCDate()}`.padStart(2, "0");
  return `${y}-${m}-${d}`;
}

function isWorkingDayUtc(dateUtc, policy) {
  const dow = dateUtc.getUTCDay();
  // 6 = Saturday
  if (policy?.saturdayOff && dow === 6) return false;
  return true;
}

function expectedStartForUtcDate(dateUtc, policy) {
  const dow = dateUtc.getUTCDay();
  // 5 = Friday
  if (dow === 5) return policy?.fridayStart || "09:00";
  return policy?.weekdayStart || "10:00";
}

export const attendanceRouter = express.Router();
attendanceRouter.use(requireAuth);

const uploadsDir = path.join(process.cwd(), "uploads");
fs.mkdirSync(uploadsDir, { recursive: true });

const upload = multer({
  storage: multer.diskStorage({
    destination: (_req, _file, cb) => cb(null, uploadsDir),
    filename: (_req, file, cb) => {
      const original = String(file.originalname || "file");
      const ext = path.extname(original).slice(0, 10);
      const safeExt = ext && ext.startsWith(".") ? ext : "";
      const name = `${Date.now()}-${crypto.randomBytes(6).toString("hex")}${
        safeExt || ".jpg"
      }`;
      cb(null, name);
    },
  }),
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
  },
});

attendanceRouter.use((req, _res, next) => {
  if (req.user?.role === "customer") {
    return next(httpError(403, "Customers cannot access attendance"));
  }
  return next();
});

// Get attendance policy (office hours, Saturday off, free absences)
attendanceRouter.get("/policy", async (_req, res, next) => {
  try {
    const policy = await getAttendancePolicy();
    res.json({ policy });
  } catch (err) {
    next(err);
  }
});

// Admin: update attendance policy
attendanceRouter.put(
  "/policy",
  requireRole("admin"),
  validate(updateAttendancePolicySchema),
  async (req, res, next) => {
    try {
      const key = "default";
      const updates = req.validated.body;

      const updated = await AttendancePolicy.findOneAndUpdate(
        { key },
        { $set: updates, $setOnInsert: { key } },
        { upsert: true, new: true }
      ).lean();

      res.json({ policy: updated });
    } catch (err) {
      next(err);
    }
  }
);

// Upload attendance image (check-in/check-out photo)
attendanceRouter.post(
  "/uploads",
  upload.single("file"),
  async (req, res, next) => {
    try {
      if (!req.file) throw httpError(400, "No file uploaded");

      const mimeType = req.file.mimetype || "application/octet-stream";
      if (!mimeType.startsWith("image/")) {
        try {
          fs.unlinkSync(req.file.path);
        } catch {
          // ignore
        }
        throw httpError(400, "Only image uploads are allowed");
      }

      const url = `/uploads/${req.file.filename}`;
      res.status(201).json({
        url,
        mimeType,
        size: req.file.size,
        filename: req.file.originalname,
      });
    } catch (err) {
      next(err);
    }
  }
);

// Backward-compatible endpoint: some clients call GET /attendance.
// - Admins get the same data as /attendance/all (supports filtering)
// - Non-admins get the same data as /attendance/me
// Returns both `items` and `records` to support older frontend parsing.
attendanceRouter.get(
  "/",
  validate(listAttendanceSchema),
  async (req, res, next) => {
    try {
      const { from, to, userId, status } = req.validated.query;

      // Regular users: only their own attendance
      if (req.user.role !== "admin") {
        const query = { userId: req.user._id };
        if (from || to) {
          query.day = {};
          if (from) query.day.$gte = from;
          if (to) query.day.$lte = to;
        }
        const items = await Attendance.find(query).sort({ day: -1 }).lean();
        return res.json({ items, records: items });
      }

      // Admins: all attendance (with optional filters)
      const query = {};
      if (userId) query.userId = userId;
      if (status) query.status = status;
      if (from || to) {
        query.day = {};
        if (from) query.day.$gte = from;
        if (to) query.day.$lte = to;
      }

      const items = await Attendance.find(query)
        .populate("userId", "fullName email role")
        .populate("approvedBy", "fullName")
        .sort({ day: -1, createdAt: -1 })
        .lean();

      res.json({ items, records: items });
    } catch (err) {
      next(err);
    }
  }
);

attendanceRouter.post(
  "/check-in",
  validate(checkInSchema),
  async (req, res, next) => {
    try {
      const now = new Date();
      const day = yyyyMmDd(now);

      const existing = await Attendance.findOne({ userId: req.user._id, day });
      if (existing?.checkInAt) throw httpError(409, "Already checked in today");

      const { note, location, imageUrl } = req.validated.body || {};
      const policy = await getAttendancePolicy();
      const dayUtc = yyyyMmDdToUtcDate(day);
      const lateStatus = calculateLateStatus(
        now,
        expectedStartForUtcDate(dayUtc, policy)
      );

      const doc = await Attendance.findOneAndUpdate(
        { userId: req.user._id, day },
        {
          $set: {
            checkInAt: now,
            ...(note ? { note } : {}),
            ...(imageUrl ? { checkInImageUrl: imageUrl } : {}),
            ...(location ? { checkInLocation: location } : {}),
            ...lateStatus,
            status: "pending",
          },
        },
        { upsert: true, new: true }
      ).lean();

      res.status(201).json({ attendance: doc });
    } catch (err) {
      next(err);
    }
  }
);

// Monthly attendance report (admin can request all employees or a specific user)
attendanceRouter.get(
  "/reports/monthly",
  validate(monthlyReportSchema),
  async (req, res, next) => {
    try {
      const { month, year, userId, scope } = req.validated.query;
      const policy = await getAttendancePolicy();

      const startDate = `${year}-${String(month).padStart(2, "0")}-01`;
      const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
      const endDate = `${year}-${String(month).padStart(2, "0")}-${String(
        lastDay
      ).padStart(2, "0")}`;

      const buildUserReport = async (targetUser) => {
        const [attendanceItems, leaveItems] = await Promise.all([
          Attendance.find({
            userId: targetUser._id,
            day: { $gte: startDate, $lte: endDate },
          })
            .sort({ day: 1 })
            .lean(),
          LeaveRequest.find({
            userId: targetUser._id,
            status: "approved",
            fromDate: { $lte: endDate },
            toDate: { $gte: startDate },
          })
            .sort({ fromDate: 1 })
            .lean(),
        ]);

        const attendanceByDay = new Map(attendanceItems.map((a) => [a.day, a]));

        // Expand approved leave dates into a set of YYYY-MM-DD
        const leaveDays = new Set();
        for (const leave of leaveItems) {
          const from = yyyyMmDdToUtcDate(leave.fromDate);
          const to = yyyyMmDdToUtcDate(leave.toDate);
          for (
            let d = from;
            d.getTime() <= to.getTime();
            d = addDaysUtc(d, 1)
          ) {
            const dayStr = toYyyyMmDdUtc(d);
            if (dayStr < startDate || dayStr > endDate) continue;
            if (!isWorkingDayUtc(d, policy)) continue;
            leaveDays.add(dayStr);
          }
        }

        const days = [];
        let workingDays = 0;
        let presentDays = 0;
        let approvedLeaveDays = 0;
        let absentDays = 0;
        let totalHours = 0;
        let lateDays = 0;

        for (let dayNum = 1; dayNum <= lastDay; dayNum++) {
          const dateUtc = new Date(Date.UTC(year, month - 1, dayNum));
          const dayStr = toYyyyMmDdUtc(dateUtc);
          const dow = dateUtc.getUTCDay();
          const weekdayNames = [
            "Sunday",
            "Monday",
            "Tuesday",
            "Wednesday",
            "Thursday",
            "Friday",
            "Saturday",
          ];

          const working = isWorkingDayUtc(dateUtc, policy);
          const attendance = attendanceByDay.get(dayStr) || null;
          const onLeave = leaveDays.has(dayStr);

          let status = "off";
          if (!working) {
            status = "off";
          } else if (onLeave) {
            status = "leave";
          } else if (attendance?.checkInAt) {
            status = "present";
          } else {
            status = "absent";
          }

          if (working) workingDays++;
          if (status === "present") {
            presentDays++;
            totalHours += attendance?.totalHours || 0;
            if (attendance?.isLate) lateDays++;
          }
          if (status === "leave") approvedLeaveDays++;
          if (status === "absent") absentDays++;

          days.push({
            day: dayStr,
            weekday: weekdayNames[dow],
            working,
            expectedStart: expectedStartForUtcDate(dateUtc, policy),
            expectedEnd: dow === 5 ? policy.fridayEnd : policy.weekdayEnd,
            status,
            attendance,
          });
        }

        const freeAbsences = Number(policy?.freeAbsencesPerMonth ?? 2);
        const deductionDays = Math.max(0, absentDays - freeAbsences);

        return {
          user: {
            _id: targetUser._id,
            fullName: targetUser.fullName,
            email: targetUser.email,
            role: targetUser.role,
          },
          range: { startDate, endDate, month, year },
          summary: {
            workingDays,
            presentDays,
            approvedLeaveDays,
            absentDays,
            freeAbsences,
            deductionDays,
            lateDays,
            totalHours: Math.round(totalHours * 100) / 100,
          },
          days,
        };
      };

      // Admin: all users
      if (req.user.role === "admin" && scope === "all") {
        const users = await User.find({
          status: "active",
          role: { $ne: "admin" },
        })
          .select("fullName email role")
          .lean();

        const reports = [];
        for (const u of users) {
          // eslint-disable-next-line no-await-in-loop
          reports.push(await buildUserReport(u));
        }
        return res.json({
          policy,
          range: { startDate, endDate, month, year },
          reports,
        });
      }

      // Admin can request a specific user report
      if (req.user.role === "admin" && userId) {
        const target = await User.findById(userId).select(
          "fullName email role"
        );
        if (!target) throw httpError(404, "User not found");
        const report = await buildUserReport(target);
        return res.json({ policy, report });
      }

      // Regular users: only their own report
      const me = await User.findById(req.user._id).select(
        "fullName email role"
      );
      const report = await buildUserReport(me);
      return res.json({ policy, report });
    } catch (err) {
      next(err);
    }
  }
);

attendanceRouter.post(
  "/check-out",
  validate(checkOutSchema),
  async (req, res, next) => {
    try {
      const now = new Date();
      const day = yyyyMmDd(now);

      const doc = await Attendance.findOne({ userId: req.user._id, day });
      if (!doc?.checkInAt) throw httpError(409, "You must check in first");
      if (doc.checkOutAt) throw httpError(409, "Already checked out today");

      const { note, workSummary, location, imageUrl } =
        req.validated.body || {};

      const updateData = {
        checkOutAt: now,
        ...(note ? { note } : {}),
        ...(workSummary ? { workSummary } : {}),
        ...(imageUrl ? { checkOutImageUrl: imageUrl } : {}),
        ...(location ? { checkOutLocation: location } : {}),
      };

      const updated = await Attendance.findByIdAndUpdate(
        doc._id,
        { $set: updateData },
        { new: true }
      ).lean();

      res.json({ attendance: updated });
    } catch (err) {
      next(err);
    }
  }
);

attendanceRouter.get(
  "/me",
  validate(listMyAttendanceSchema),
  async (req, res, next) => {
    try {
      const { from, to } = req.validated.query;
      const query = { userId: req.user._id };

      if (from || to) {
        query.day = {};
        if (from) query.day.$gte = from;
        if (to) query.day.$lte = to;
      }

      const items = await Attendance.find(query).sort({ day: -1 }).lean();
      res.json({ items });
    } catch (err) {
      next(err);
    }
  }
);

// Admin: Get all attendance records with filtering
attendanceRouter.get(
  "/all",
  validate(listAllAttendanceSchema),
  async (req, res, next) => {
    try {
      if (req.user.role !== "admin") {
        throw httpError(403, "Admin access required");
      }

      const { from, to, userId, status } = req.validated.query;
      const query = {};

      if (userId) query.userId = userId;
      if (status) query.status = status;

      if (from || to) {
        query.day = {};
        if (from) query.day.$gte = from;
        if (to) query.day.$lte = to;
      }

      const items = await Attendance.find(query)
        .populate("userId", "fullName email role")
        .populate("approvedBy", "fullName")
        .sort({ day: -1, createdAt: -1 })
        .lean();

      res.json({ items });
    } catch (err) {
      next(err);
    }
  }
);

// Admin: Approve or reject attendance
attendanceRouter.patch(
  "/:id/approve",
  validate(approveAttendanceSchema),
  async (req, res, next) => {
    try {
      if (req.user.role !== "admin") {
        throw httpError(403, "Admin access required");
      }

      const { id } = req.validated.params;
      const { status, approvalNote } = req.validated.body;

      const attendance = await Attendance.findById(id).populate(
        "userId",
        "fullName email _id"
      );
      if (!attendance) throw httpError(404, "Attendance record not found");

      const previousStatus = attendance.status;
      attendance.status = status;
      attendance.approvedBy = req.user._id;
      attendance.approvedAt = new Date();
      if (approvalNote) attendance.approvalNote = approvalNote;

      await attendance.save();

      // Send notification if rejected
      if (status === "rejected" && previousStatus !== "rejected") {
        try {
          // Find or create a private channel for admin-user communication
          let channel = await Channel.findOne({
            type: "private",
            members: { $all: [req.user._id, attendance.userId._id] },
          });

          if (!channel) {
            channel = await Channel.create({
              name: `Admin - ${attendance.userId.fullName}`,
              type: "private",
              isPrivate: true,
              members: [req.user._id, attendance.userId._id],
              createdBy: req.user._id,
            });
          }

          // Send rejection message
          const rejectionMessage = `📋 Your attendance for ${
            attendance.day
          } has been rejected.\n\n${
            approvalNote
              ? `Reason: ${approvalNote}`
              : "Please contact admin for details."
          }\n\nCheck In: ${
            attendance.checkInAt
              ? new Date(attendance.checkInAt).toLocaleTimeString()
              : "N/A"
          }\nCheck Out: ${
            attendance.checkOutAt
              ? new Date(attendance.checkOutAt).toLocaleTimeString()
              : "N/A"
          }`;

          await Message.create({
            channelId: channel._id,
            senderId: req.user._id,
            text: rejectionMessage,
          });
        } catch (notifErr) {
          console.error("Failed to send rejection notification:", notifErr);
          // Don't fail the whole request if notification fails
        }
      }

      const updated = await Attendance.findById(id)
        .populate("userId", "fullName email role")
        .populate("approvedBy", "fullName")
        .lean();

      res.json({ attendance: updated });
    } catch (err) {
      next(err);
    }
  }
);

// Admin: Get attendance statistics
attendanceRouter.get("/stats/overview", async (req, res, next) => {
  try {
    if (req.user.role !== "admin") {
      throw httpError(403, "Admin access required");
    }

    const today = yyyyMmDd(new Date());

    const [totalToday, pendingApproval, lateToday, totalUsers] =
      await Promise.all([
        Attendance.countDocuments({ day: today }),
        Attendance.countDocuments({ status: "pending" }),
        Attendance.countDocuments({ day: today, isLate: true }),
        User.countDocuments({ status: "active", role: { $ne: "admin" } }),
      ]);

    res.json({
      stats: {
        totalToday,
        pendingApproval,
        lateToday,
        totalUsers,
        attendanceRate:
          totalUsers > 0 ? Math.round((totalToday / totalUsers) * 100) : 0,
      },
    });
  } catch (err) {
    next(err);
  }
});

// Admin: Get detailed analytics
attendanceRouter.get("/stats/analytics", async (req, res, next) => {
  try {
    if (req.user.role !== "admin") {
      throw httpError(403, "Admin access required");
    }

    const { from, to } = req.query;
    const query = {};

    if (from || to) {
      query.day = {};
      if (from) query.day.$gte = from;
      if (to) query.day.$lte = to;
    }

    const records = await Attendance.find(query)
      .populate("userId", "fullName email")
      .lean();

    // Calculate analytics
    const totalRecords = records.length;
    const approvedCount = records.filter((r) => r.status === "approved").length;
    const rejectedCount = records.filter((r) => r.status === "rejected").length;
    const pendingCount = records.filter((r) => r.status === "pending").length;
    const lateCount = records.filter((r) => r.isLate).length;

    const totalHours = records.reduce((sum, r) => sum + (r.totalHours || 0), 0);
    const avgHours =
      totalRecords > 0 ? (totalHours / totalRecords).toFixed(2) : 0;

    // User-wise summary
    const userStats = {};
    records.forEach((r) => {
      const userId = r.userId?._id?.toString();
      if (!userId) return;

      if (!userStats[userId]) {
        userStats[userId] = {
          userId: r.userId._id,
          fullName: r.userId.fullName,
          email: r.userId.email,
          totalDays: 0,
          approved: 0,
          rejected: 0,
          pending: 0,
          late: 0,
          totalHours: 0,
        };
      }

      userStats[userId].totalDays++;
      if (r.status === "approved") userStats[userId].approved++;
      if (r.status === "rejected") userStats[userId].rejected++;
      if (r.status === "pending") userStats[userId].pending++;
      if (r.isLate) userStats[userId].late++;
      userStats[userId].totalHours += r.totalHours || 0;
    });

    const userSummaries = Object.values(userStats).map((u) => ({
      ...u,
      avgHours: u.totalDays > 0 ? (u.totalHours / u.totalDays).toFixed(2) : 0,
    }));

    res.json({
      analytics: {
        overall: {
          totalRecords,
          approvedCount,
          rejectedCount,
          pendingCount,
          lateCount,
          totalHours: totalHours.toFixed(2),
          avgHours,
        },
        byUser: userSummaries,
      },
    });
  } catch (err) {
    next(err);
  }
});

// Admin: Bulk approve attendances
attendanceRouter.post("/bulk-approve", async (req, res, next) => {
  try {
    if (req.user.role !== "admin") {
      throw httpError(403, "Admin access required");
    }

    const { ids, status, approvalNote } = req.body;

    if (!ids || !Array.isArray(ids) || ids.length === 0) {
      throw httpError(400, "IDs array is required");
    }

    if (!["approved", "rejected"].includes(status)) {
      throw httpError(400, "Invalid status");
    }

    const result = await Attendance.updateMany(
      { _id: { $in: ids }, status: "pending" },
      {
        $set: {
          status,
          approvedBy: req.user._id,
          approvedAt: new Date(),
          ...(approvalNote ? { approvalNote } : {}),
        },
      }
    );

    res.json({
      success: true,
      modifiedCount: result.modifiedCount,
    });
  } catch (err) {
    next(err);
  }
});

// Get attendance notifications for user
attendanceRouter.get("/notifications", async (req, res, next) => {
  try {
    const recentRecords = await Attendance.find({
      userId: req.user._id,
      status: { $in: ["approved", "rejected"] },
    })
      .sort({ approvedAt: -1 })
      .limit(10)
      .populate("approvedBy", "fullName")
      .lean();

    const notifications = recentRecords.map((r) => ({
      id: r._id,
      type: r.status === "approved" ? "success" : "warning",
      title: `Attendance ${r.status === "approved" ? "Approved" : "Rejected"}`,
      message: `Your attendance for ${r.day} has been ${r.status}${
        r.approvalNote ? `: ${r.approvalNote}` : ""
      }`,
      date: r.approvedAt,
      approvedBy: r.approvedBy?.fullName,
    }));

    res.json({ notifications });
  } catch (err) {
    next(err);
  }
});
