import prisma from '../../config/database';
import { AppError } from '../../shared/middleware/errorHandler';
import { hrService } from './hr.service';
import { notificationsService } from '../notifications/notifications.service';
import path from 'path';
import fs from 'fs';

// Pattern that indicates a UTF-8 string was misinterpreted as latin1
// (the legacy multer/busboy default). A UTF-8 2-byte sequence is a starter
// byte in 0xC2-0xDF followed by a continuation byte in 0x80-0xBF, which is
// exactly what we look for here. Properly-stored Arabic text has code
// points U+0600-U+06FF and won't match this pattern, so the check is safe.
const MOJIBAKE_PATTERN = /[\u00C2-\u00DF][\u0080-\u00BF]/;

/**
 * Heuristically repair a string that was stored after multer interpreted
 * the file's UTF-8 filename bytes as latin1. Returns the input unchanged
 * if it doesn't look like mojibake or if re-decoding would lose data.
 */
function repairFilenameEncoding(value: string | null | undefined): string {
  if (!value) return value ?? '';
  if (!MOJIBAKE_PATTERN.test(value)) return value;
  try {
    const decoded = Buffer.from(value, 'latin1').toString('utf8');
    // If the re-decode produced replacement chars, the original wasn't
    // actually mojibake — bail out and keep the existing value.
    if (decoded.includes('\uFFFD')) return value;
    return decoded;
  } catch {
    return value;
  }
}

class PortalService {
  private requireEmployeeId(employeeId: string | undefined): string {
    if (!employeeId) {
      throw new AppError(403, 'يجب ربط المستخدم بموظف للوصول للبوابة - Employee link required for portal access');
    }
    return employeeId;
  }

  async getMe(employeeId: string | undefined) {
    const empId = this.requireEmployeeId(employeeId);
    const employee = await prisma.employee.findUnique({
      where: { id: empId },
      select: {
        id: true,
        uniqueEmployeeId: true,
        firstName: true,
        lastName: true,
        firstNameAr: true,
        lastNameAr: true,
        email: true,
        department: { select: { name: true, nameAr: true } },
        position: { select: { title: true, titleAr: true } },
      },
    });

    if (!employee) {
      throw new AppError(404, 'الموظف غير موجود - Employee not found');
    }

    const [loansCount, pendingLeaves, pendingPurchaseRequests, pendingExpenseClaims, leaveBalance] = await Promise.all([
      prisma.loan.count({ where: { employeeId: empId, status: { in: ['PENDING', 'ACTIVE'] } } }),
      prisma.leave.count({ where: { employeeId: empId, status: 'PENDING' } }),
      prisma.purchaseRequest.count({ where: { employeeId: empId, status: 'PENDING' } }),
      prisma.expenseClaim.count({ where: { employeeId: empId, status: 'PENDING' } }),
      hrService.getLeaveBalance(empId, new Date().getFullYear()),
    ]);

    return {
      employee,
      stats: {
        activeLoansCount: loansCount,
        pendingLeavesCount: pendingLeaves,
        pendingPurchaseRequestsCount: pendingPurchaseRequests,
        pendingExpenseClaimsCount: pendingExpenseClaims,
        leaveBalance,
      },
    };
  }

  private buildOvertimeRequestNote(
    hours: number,
    reason: string,
    status: 'PENDING' | 'APPROVED' | 'REJECTED',
    rejectedReason?: string
  ) {
    const safeReason = String(reason || '').replace(/\|/g, '/').trim();
    const safeRejectedReason = String(rejectedReason || '').replace(/\|/g, '/').trim();

    let note = `OT_REQUEST|status=${status}|hours=${hours}|reason=${safeReason}`;
    if (status === 'REJECTED' && safeRejectedReason) {
      note += `|rejectedReason=${safeRejectedReason}`;
    }
    return note;
  }

  private isOvertimeRequestNote(notes?: string | null) {
    return !!notes && notes.startsWith('OT_REQUEST|');
  }

  private parseOvertimeRequestNote(notes?: string | null) {
    if (!notes || !notes.startsWith('OT_REQUEST|')) return null;

    const parts = notes.split('|');
    const data: Record<string, string> = {};

    for (const part of parts.slice(1)) {
      const idx = part.indexOf('=');
      if (idx > -1) {
        const key = part.slice(0, idx);
        const value = part.slice(idx + 1);
        data[key] = value;
      }
    }

    return {
      status: data.status || 'PENDING',
      hours: Number(data.hours || 0),
      reason: data.reason || '',
      rejectedReason: data.rejectedReason || '',
    };
  }

  private formatOvertimeRequest(attendance: any) {
    const parsed = this.parseOvertimeRequestNote(attendance.notes);
    if (!parsed) return null;

    return {
      id: attendance.id,
      attendanceId: attendance.id,
      date: attendance.date,
      hours: parsed.hours || Number(attendance.overtimeHours || 0),
      reason: parsed.reason,
      status: parsed.status,
      rejectedReason: parsed.rejectedReason || '',
      createdAt: attendance.createdAt,
      employee: attendance.employee,
    };
  }

  async getMyLoans(employeeId: string | undefined) {
    const empId = this.requireEmployeeId(employeeId);
    return prisma.loan.findMany({
      where: { employeeId: empId },
      include: { installmentsList: { orderBy: { installmentNumber: 'asc' } } },
      orderBy: { createdAt: 'desc' },
    });
  }

  async getMyOvertimeRequests(employeeId: string | undefined) {
    const empId = this.requireEmployeeId(employeeId);

    const rows = await prisma.attendance.findMany({
      where: {
        employeeId: empId,
        notes: {
          startsWith: 'OT_REQUEST|',
        },
      },
      orderBy: {
        date: 'desc',
      },
      take: 100,
    });

    return rows
      .map((row) => this.formatOvertimeRequest(row))
      .filter(Boolean);
  }

  async submitOvertimeRequest(
    employeeId: string | undefined,
    data: { date: string; hours: number; reason: string },
    userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);

    if (!data.date) {
      throw new AppError(400, 'تاريخ الساعات الإضافية مطلوب');
    }

    if (!data.hours || Number(data.hours) <= 0) {
      throw new AppError(400, 'عدد الساعات غير صالح');
    }

    if (!data.reason || !data.reason.trim()) {
      throw new AppError(400, 'سبب الساعات الإضافية مطلوب');
    }

    const requestDate = new Date(data.date);
    const note = this.buildOvertimeRequestNote(Number(data.hours), data.reason.trim(), 'PENDING');

    const existing = await prisma.attendance.findFirst({
      where: {
        employeeId: empId,
        date: requestDate,
      },
    });

    let attendance;

    if (existing) {
      attendance = await prisma.attendance.update({
        where: { id: existing.id },
        data: {
          overtimeHours: Number(data.hours),
          notes: note,
        },
        include: {
          employee: {
            select: {
              id: true,
              firstName: true,
              lastName: true,
              uniqueEmployeeId: true,
              reportingToId: true,
            },
          },
        },
      });
    } else {
      attendance = await prisma.attendance.create({
        data: {
          employeeId: empId,
          date: requestDate,
          status: 'PRESENT',
          overtimeHours: Number(data.hours),
          notes: note,
        },
        include: {
          employee: {
            select: {
              id: true,
              firstName: true,
              lastName: true,
              uniqueEmployeeId: true,
              reportingToId: true,
            },
          },
        },
      });
    }

    const employeeFullName = `${attendance.employee.firstName} ${attendance.employee.lastName}`;

       await notificationsService.notifyApprovalRecipients({
        resource: 'overtime_requests',
        fallbackEmployeeId: attendance.employeeId,
        fallbackToManager: true,
        type: 'OVERTIME_REQUEST_SUBMITTED',
        title: 'طلب ساعات إضافية جديد بانتظار الموافقة',
        message: `قام الموظف ${employeeFullName} بإرسال طلب ساعات إضافية جديد.`,
        entityType: 'OVERTIME_REQUEST',
        entityId: attendance.id,
        excludeUserIds: [userId],
      });

    await notificationsService.notifyEmployeeUser({
      employeeId: attendance.employeeId,
      type: 'OVERTIME_REQUEST_CREATED',
      title: 'تم إرسال طلب الساعات الإضافية',
      message: 'تم إرسال طلب الساعات الإضافية الخاص بك بنجاح وهو الآن بانتظار المراجعة.',
      entityType: 'OVERTIME_REQUEST',
      entityId: attendance.id,
      excludeUserIds: [],
    });

    return this.formatOvertimeRequest(attendance);
    }


    private async attachExpenseClaimFiles<T extends { id: string }>(claims: T[]) {
  const claimIds = claims.map((claim) => claim.id);

  if (claimIds.length === 0) {
    return claims.map((claim) => ({ ...claim, attachments: [] }));
  }

  const attachments = await prisma.attachment.findMany({
    where: {
      entityType: 'EXPENSE_CLAIM',
      entityId: { in: claimIds },
    },
    orderBy: { uploadedAt: 'desc' },
  });

  return claims.map((claim) => ({
    ...claim,
    attachments: attachments
      .filter((attachment) => attachment.entityId === claim.id)
      .map((attachment) => ({
        ...attachment,
        // Repair mojibake in records uploaded before the multer fix.
        originalName: repairFilenameEncoding(attachment.originalName),
      })),
  }));
}

async getExpenseClaimAttachmentFile(attachmentId: string) {
  const attachment = await prisma.attachment.findUnique({
    where: { id: attachmentId },
  });

  if (!attachment || attachment.entityType !== 'EXPENSE_CLAIM') {
    throw new AppError(404, 'الملف غير موجود');
  }

  // Repair mojibake so the Content-Disposition filename* the controller
  // generates uses the real Arabic name when opening/downloading.
  return {
    ...attachment,
    originalName: repairFilenameEncoding(attachment.originalName),
  };
}

  async getManagedOvertimeRequests(employeeId: string | undefined) {
  this.requireEmployeeId(employeeId);

  const rows = await prisma.attendance.findMany({
    where: {
      notes: {
        startsWith: 'OT_REQUEST|',
      },
    },
    include: {
      employee: {
        select: {
          id: true,
          firstName: true,
          lastName: true,
          uniqueEmployeeId: true,
          reportingToId: true,
        },
      },
    },
    orderBy: {
      date: 'desc',
    },
    take: 100,
  });

  return rows
    .map((row) => this.formatOvertimeRequest(row))
    .filter((row: any) => row && row.status === 'PENDING');
}

  async approveManagedOvertimeRequest(
  managerEmployeeId: string | undefined,
  attendanceId: string,
  userId: string
) {
  this.requireEmployeeId(managerEmployeeId);

  const attendance = await prisma.attendance.findUnique({
    where: { id: attendanceId },
    include: {
      employee: {
        select: {
          id: true,
          reportingToId: true,
          firstName: true,
          lastName: true,
          uniqueEmployeeId: true,
        },
      },
    },
  });

  if (!attendance) {
    throw new AppError(404, 'طلب الساعات الإضافية غير موجود');
  }

  if (!this.isOvertimeRequestNote(attendance.notes)) {
    throw new AppError(400, 'هذا السجل ليس طلب ساعات إضافية');
  }

  const parsed = this.parseOvertimeRequestNote(attendance.notes);
  if (!parsed || parsed.status !== 'PENDING') {
    throw new AppError(400, 'هذا الطلب ليس بحالة معلقة');
  }

  const updatedNote = this.buildOvertimeRequestNote(parsed.hours, parsed.reason, 'APPROVED');

  const updated = await prisma.attendance.update({
    where: { id: attendanceId },
    data: {
      overtimeHours: parsed.hours,
      notes: updatedNote,
    },
    include: {
      employee: {
        select: {
          id: true,
          firstName: true,
          lastName: true,
          uniqueEmployeeId: true,
          reportingToId: true,
        },
      },
    },
  });

  await notificationsService.notifyEmployeeUser({
    employeeId: updated.employeeId,
    type: 'OVERTIME_REQUEST_APPROVED',
    title: 'تمت الموافقة على طلب الساعات الإضافية',
    message: 'تمت الموافقة على طلب الساعات الإضافية الخاص بك.',
    entityType: 'OVERTIME_REQUEST',
    entityId: updated.id,
    excludeUserIds: [userId],
  });

  return this.formatOvertimeRequest(updated);
}


 async rejectManagedOvertimeRequest(
  managerEmployeeId: string | undefined,
  attendanceId: string,
  rejectedReason: string,
  userId: string
) {
  this.requireEmployeeId(managerEmployeeId);

  if (!rejectedReason || !rejectedReason.trim()) {
    throw new AppError(400, 'سبب الرفض مطلوب');
  }

  const attendance = await prisma.attendance.findUnique({
    where: { id: attendanceId },
    include: {
      employee: {
        select: {
          id: true,
          reportingToId: true,
          firstName: true,
          lastName: true,
          uniqueEmployeeId: true,
        },
      },
    },
  });

  if (!attendance) {
    throw new AppError(404, 'طلب الساعات الإضافية غير موجود');
  }

  if (!this.isOvertimeRequestNote(attendance.notes)) {
    throw new AppError(400, 'هذا السجل ليس طلب ساعات إضافية');
  }

  const parsed = this.parseOvertimeRequestNote(attendance.notes);
  if (!parsed || parsed.status !== 'PENDING') {
    throw new AppError(400, 'هذا الطلب ليس بحالة معلقة');
  }

  const updatedNote = this.buildOvertimeRequestNote(
    parsed.hours,
    parsed.reason,
    'REJECTED',
    rejectedReason.trim()
  );

  const updated = await prisma.attendance.update({
    where: { id: attendanceId },
    data: {
      notes: updatedNote,
    },
    include: {
      employee: {
        select: {
          id: true,
          firstName: true,
          lastName: true,
          uniqueEmployeeId: true,
          reportingToId: true,
        },
      },
    },
  });

  await notificationsService.notifyEmployeeUser({
    employeeId: updated.employeeId,
    type: 'OVERTIME_REQUEST_REJECTED',
    title: 'تم رفض طلب الساعات الإضافية',
    message: rejectedReason?.trim()
      ? `تم رفض طلب الساعات الإضافية الخاص بك. السبب: ${rejectedReason.trim()}`
      : 'تم رفض طلب الساعات الإضافية الخاص بك.',
    entityType: 'OVERTIME_REQUEST',
    entityId: updated.id,
    excludeUserIds: [userId],
  });

  return this.formatOvertimeRequest(updated);
}


  async submitLoanRequest(
    employeeId: string | undefined,
    data: { type: string; amount: number; installments?: number; reason?: string },
    userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);
    return hrService.createLoan({ ...data, employeeId: empId }, userId);
  }

  async getMyLeaveBalance(employeeId: string | undefined, year?: number) {
    const empId = this.requireEmployeeId(employeeId);
    const y = year || new Date().getFullYear();
    return hrService.getLeaveBalance(empId, y);
  }

  async getMyLeaves(employeeId: string | undefined) {
    const empId = this.requireEmployeeId(employeeId);
    return prisma.leave.findMany({
      where: { employeeId: empId },
      orderBy: { createdAt: 'desc' },
      take: 50,
    });
  }

  async getManagedLeaves(employeeId: string | undefined, status?: string) {
    this.requireEmployeeId(employeeId);
    return hrService.findManagedLeaves(status);
  }

  async approveManagedLeave(employeeId: string | undefined, leaveId: string, userId: string) {
    this.requireEmployeeId(employeeId);
    return hrService.managerApproveLeave(leaveId, userId);
  }

  async rejectManagedLeave(
    employeeId: string | undefined,
    leaveId: string,
    rejectedReason: string,
    userId: string
  ) {
    this.requireEmployeeId(employeeId);
    return hrService.managerRejectLeave(leaveId, rejectedReason, userId);
  }

  async submitLeaveRequest(
    employeeId: string | undefined,
    data: { leaveType: string; startDate: Date; endDate: Date; reason?: string },
    userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);
    return hrService.createLeaveRequest({ ...data, employeeId: empId }, userId);
  }

  async getMyPurchaseRequests(employeeId: string | undefined) {
    const empId = this.requireEmployeeId(employeeId);
    return prisma.purchaseRequest.findMany({
      where: { employeeId: empId },
      orderBy: { createdAt: 'desc' },
    });
  }

  async submitPurchaseRequest(
    employeeId: string | undefined,
    data: { items: any[]; reason?: string; priority?: string },
    userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);
    return hrService.createPurchaseRequest({ ...data, employeeId: empId }, userId);
  }

    private async generateExpenseClaimNumber(): Promise<string> {
    const year = new Date().getFullYear();
    const prefix = `EC-${year}-`;

    const last = await prisma.expenseClaim.findFirst({
      where: {
        claimNumber: {
          startsWith: prefix,
        },
      },
      orderBy: {
        createdAt: 'desc',
      },
      select: {
        claimNumber: true,
      },
    });

    let next = 1;
    if (last?.claimNumber) {
      const parts = last.claimNumber.split('-');
      next = parseInt(parts[2] || '0', 10) + 1;
    }

    return `${prefix}${next.toString().padStart(4, '0')}`;
  }

  async getMyExpenseClaims(employeeId: string | undefined) {
  const empId = this.requireEmployeeId(employeeId);

  const claims = await prisma.expenseClaim.findMany({
    where: { employeeId: empId },
    include: {
      employee: {
        select: {
          id: true,
          firstName: true,
          lastName: true,
          uniqueEmployeeId: true,
        },
      },
    },
    orderBy: { createdAt: 'desc' },
  });

  return this.attachExpenseClaimFiles(claims);
}

async submitExpenseClaim(
  employeeId: string | undefined,
  data: {
    items?: Array<{
      expenseDate?: string;
      amount?: number | string;
      entityName?: string;
      description?: string;
      projectOrTender?: string;
      proofRef?: string;
    }>;
    description?: string;
  },
  userId: string,
  files?: Express.Multer.File[]
) {

    const empId = this.requireEmployeeId(employeeId);

    const items = Array.isArray(data.items) ? data.items : [];

    const normalizedItems = items
      .map((item) => ({
        expenseDate: item.expenseDate || '',
        amount: Number(item.amount || 0),
        entityName: item.entityName?.trim() || '',
        description: item.description?.trim() || '',
        projectOrTender: item.projectOrTender?.trim() || '',
        proofRef: item.proofRef?.trim() || '',
      }))
      .filter((item) => item.description && item.amount > 0 && item.expenseDate);

    if (normalizedItems.length === 0) {
      throw new AppError(400, 'يجب إضافة بند واحد على الأقل مع التاريخ والمبلغ والبيان');
    }

    const claimNumber = await this.generateExpenseClaimNumber();

    const totalAmount = normalizedItems.reduce((sum, item) => sum + Number(item.amount || 0), 0);

    const firstItem = normalizedItems[0];

    const claim = await prisma.expenseClaim.create({
      data: {
        claimNumber,
        employeeId: empId,
        items: normalizedItems as any,
        totalAmount,
        expenseDate: new Date(firstItem.expenseDate),
        amount: totalAmount,
        description: data.description?.trim() || null,
        projectOrTender: firstItem.projectOrTender || null,
        status: 'PENDING',
      },
      include: {
        employee: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            uniqueEmployeeId: true,
          },
        },
      },
    });

    if (files && files.length > 0) {
    await Promise.all(
      files.map((file) =>
        prisma.attachment.create({
          data: {
            entityType: 'EXPENSE_CLAIM',
            entityId: claim.id,
            fileName: path.basename(file.path),
            originalName: (file as any).decodedOriginalName || file.originalname,
            mimeType: file.mimetype,
            size: file.size,
            path: file.path,
            category: 'EXPENSE_CLAIM_ATTACHMENT',
            uploadedBy: userId,
          },
        })
      )
    );
}
    const employeeFullName = `${claim.employee.firstName} ${claim.employee.lastName}`;

    await notificationsService.notifyUsersWithPermission({
      module: 'department_expense_claims',
      resource: '*',
      action: 'approve',
      type: 'EXPENSE_CLAIM_SUBMITTED',
      title: 'كشف مصاريف جديد بانتظار الموافقة',
      message: `قام الموظف ${employeeFullName} بإرسال كشف مصاريف جديد برقم ${claim.claimNumber}.`,
      entityType: 'EXPENSE_CLAIM',
      entityId: claim.id,
      excludeUserIds: [userId],
    });
    await notificationsService.notifyEmployeeUser({
      employeeId: claim.employeeId,
      type: 'EXPENSE_CLAIM_CREATED',
      title: 'تم إرسال كشف المصاريف',
      message: `تم إرسال كشف المصاريف الخاص بك برقم ${claim.claimNumber} وهو الآن بانتظار المراجعة.`,
      entityType: 'EXPENSE_CLAIM',
      entityId: claim.id,
      excludeUserIds: [],
    });

const [claimWithAttachments] = await this.attachExpenseClaimFiles([claim]);
return claimWithAttachments;
  }

  async getManagedExpenseClaims(
    employeeId: string | undefined,
    status?: string,
    search?: string,
    paid?: string,
  ) {
    this.requireEmployeeId(employeeId);

    const where: any = {};

    if (status && status !== 'all') {
      where.status = status;
    }

    if (paid === 'paid') {
      where.isPaid = true;
    } else if (paid === 'unpaid') {
      where.isPaid = false;
    }

    const trimmedSearch = search?.trim();
    if (trimmedSearch) {
      where.employee = {
        OR: [
          { firstName: { contains: trimmedSearch, mode: 'insensitive' } },
          { lastName: { contains: trimmedSearch, mode: 'insensitive' } },
          { firstNameAr: { contains: trimmedSearch, mode: 'insensitive' } },
          { lastNameAr: { contains: trimmedSearch, mode: 'insensitive' } },
          { uniqueEmployeeId: { contains: trimmedSearch, mode: 'insensitive' } },
        ],
      };
    }

    const claims = await prisma.expenseClaim.findMany({
      where,
      include: {
        employee: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            uniqueEmployeeId: true,
            reportingToId: true,
          },
        },
      },
      orderBy: { createdAt: 'desc' },
    });

    return this.attachExpenseClaimFiles(claims);
  }
  async approveManagedExpenseClaim(
    managerEmployeeId: string | undefined,
    claimId: string,
    userId: string,
    approvalNote?: string,
  ) {
    this.requireEmployeeId(managerEmployeeId);

    const existing = await prisma.expenseClaim.findUnique({
      where: { id: claimId },
      include: {
        employee: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            uniqueEmployeeId: true,
          },
        },
      },
    });

    if (!existing) {
      throw new AppError(404, 'كشف المصاريف غير موجود');
    }

    if (existing.status !== 'PENDING') {
      throw new AppError(400, 'هذا الطلب ليس بحالة معلقة');
    }

    const claim = await prisma.expenseClaim.update({
      where: { id: claimId },
      data: {
        status: 'APPROVED',
        approvedBy: userId,
        approvedAt: new Date(),
        rejectedReason: null,
        approvalNote: approvalNote?.trim()?.slice(0, 1000) || null, 
      },
      include: {
        employee: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            uniqueEmployeeId: true,
          },
        },
      },
    });
  const note = approvalNote?.trim()?.slice(0, 1000);
    await notificationsService.notifyEmployeeUser({
      employeeId: claim.employeeId,
      type: 'EXPENSE_CLAIM_APPROVED',
      title: 'تمت الموافقة على كشف المصاريف',
      message: `تمت الموافقة على كشف المصاريف الخاص بك برقم ${claim.claimNumber}.${note ? ` ملاحظة المعتمِد: ${note}` : ''}`,
      entityType: 'EXPENSE_CLAIM',
      entityId: claim.id,
      excludeUserIds: [userId],
    });

    return claim;
  }

  async rejectManagedExpenseClaim(
    managerEmployeeId: string | undefined,
    claimId: string,
    rejectedReason: string,
    userId: string
  ) {
    this.requireEmployeeId(managerEmployeeId);

    if (!rejectedReason || !rejectedReason.trim()) {
      throw new AppError(400, 'سبب الرفض مطلوب');
    }

    const existing = await prisma.expenseClaim.findUnique({
      where: { id: claimId },
      include: {
        employee: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            uniqueEmployeeId: true,
          },
        },
      },
    });

    if (!existing) {
      throw new AppError(404, 'كشف المصاريف غير موجود');
    }

    if (existing.status !== 'PENDING') {
      throw new AppError(400, 'هذا الطلب ليس بحالة معلقة');
    }

    const claim = await prisma.expenseClaim.update({
      where: { id: claimId },
      data: {
        status: 'REJECTED',
        rejectedReason: rejectedReason.trim(),
      },
      include: {
        employee: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            uniqueEmployeeId: true,
          },
        },
      },
    });

    await notificationsService.notifyEmployeeUser({
      employeeId: claim.employeeId,
      type: 'EXPENSE_CLAIM_REJECTED',
      title: 'تم رفض كشف المصاريف',
      message: `تم رفض كشف المصاريف الخاص بك برقم ${claim.claimNumber}. السبب: ${rejectedReason.trim()}`,
      entityType: 'EXPENSE_CLAIM',
      entityId: claim.id,
      excludeUserIds: [userId],
    });

    return claim;
  }

  async markExpenseClaimPaid(
    managerEmployeeId: string | undefined,
    claimId: string,
    isPaid: boolean,
    userId: string
  ) {
    this.requireEmployeeId(managerEmployeeId);

    const existing = await prisma.expenseClaim.findUnique({
      where: { id: claimId },
      select: { id: true, status: true, isPaid: true, claimNumber: true },
    });

    if (!existing) {
      throw new AppError(404, 'كشف المصاريف غير موجود - Expense claim not found');
    }

    if (existing.status !== 'APPROVED') {
      throw new AppError(
        400,
        'يمكن تعليم القبض فقط على الكشوف المعتمدة - Only approved claims can be marked as paid'
      );
    }

    if (existing.isPaid === isPaid) {
      // Idempotent: no change needed.
      const claim = await prisma.expenseClaim.findUnique({
        where: { id: claimId },
        include: {
          employee: {
            select: {
              id: true,
              firstName: true,
              lastName: true,
              uniqueEmployeeId: true,
            },
          },
        },
      });
      return claim;
    }

    const claim = await prisma.expenseClaim.update({
      where: { id: claimId },
      data: { isPaid },
      include: {
        employee: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            uniqueEmployeeId: true,
          },
        },
      },
    });

    return claim;
  }

  async getMyAttendance(employeeId: string | undefined, month?: number, year?: number) {
    const empId = this.requireEmployeeId(employeeId);
    const now = new Date();
    const m = month ?? now.getMonth() + 1;
    const y = year ?? now.getFullYear();
    return hrService.getAttendance(empId, m, y);
  }

  async getMySalaries(employeeId: string | undefined) {
    const empId = this.requireEmployeeId(employeeId);
    return prisma.salary.findMany({
      where: { employeeId: empId },
      orderBy: [{ year: 'desc' }, { month: 'desc' }],
      take: 24,
    });
  }

  // ========== PERSONAL PORTAL EDIT/DELETE (PENDING-only) ==========
  // These actions are restricted to the request owner and only while the
  // request is still in its initial pending state.

  // ---------- Leaves ----------
  async updateMyLeave(
    employeeId: string | undefined,
    leaveId: string,
    data: { leaveType?: string; startDate?: Date; endDate?: Date; reason?: string },
    userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);
    const leave = await prisma.leave.findUnique({ where: { id: leaveId } });
    if (!leave) throw new AppError(404, 'طلب الإجازة غير موجود');
    if (leave.employeeId !== empId) {
      throw new AppError(403, 'لا يمكنك تعديل طلب لا يخصك');
    }
    if (leave.status !== 'PENDING') {
      throw new AppError(400, 'لا يمكن تعديل الطلب بعد مراجعته');
    }

    // Delete and re-create through the normal validated path so we
    // benefit from leave-balance checks and audit logging.
    await prisma.leave.delete({ where: { id: leaveId } });

    return hrService.createLeaveRequest(
      {
        employeeId: empId,
        leaveType: data.leaveType ?? leave.leaveType,
        startDate: data.startDate ?? leave.startDate,
        endDate: data.endDate ?? leave.endDate,
        reason: data.reason !== undefined ? data.reason : leave.reason || undefined,
      },
      userId
    );
  }

  async deleteMyLeave(employeeId: string | undefined, leaveId: string, _userId: string) {
    const empId = this.requireEmployeeId(employeeId);
    const leave = await prisma.leave.findUnique({ where: { id: leaveId } });
    if (!leave) throw new AppError(404, 'طلب الإجازة غير موجود');
    if (leave.employeeId !== empId) {
      throw new AppError(403, 'لا يمكنك حذف طلب لا يخصك');
    }
    if (leave.status !== 'PENDING') {
      throw new AppError(400, 'لا يمكن حذف الطلب بعد مراجعته');
    }
    await prisma.leave.delete({ where: { id: leaveId } });
    return { success: true };
  }

  // ---------- Purchase requests ----------
  async updateMyPurchaseRequest(
    employeeId: string | undefined,
    requestId: string,
    data: { items?: any[]; reason?: string; priority?: string },
    _userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);
    const existing = await prisma.purchaseRequest.findUnique({ where: { id: requestId } });
    if (!existing) throw new AppError(404, 'طلب الشراء غير موجود');
    if (existing.employeeId !== empId) {
      throw new AppError(403, 'لا يمكنك تعديل طلب لا يخصك');
    }
    if (existing.status !== 'PENDING') {
      throw new AppError(400, 'لا يمكن تعديل الطلب بعد مراجعته');
    }

    const items = Array.isArray(data.items) ? data.items : (existing.items as any[]) || [];
    const totalAmount = items.reduce(
      (s: number, i: any) =>
        s + (Number(i.estimatedPrice || 0) * Number(i.quantity || 1)),
      0
    );

    return prisma.purchaseRequest.update({
      where: { id: requestId },
      data: {
        items,
        totalAmount,
        reason: data.reason !== undefined ? data.reason : existing.reason,
        priority: data.priority ?? existing.priority,
      },
    });
  }

  async deleteMyPurchaseRequest(
    employeeId: string | undefined,
    requestId: string,
    _userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);
    const existing = await prisma.purchaseRequest.findUnique({ where: { id: requestId } });
    if (!existing) throw new AppError(404, 'طلب الشراء غير موجود');
    if (existing.employeeId !== empId) {
      throw new AppError(403, 'لا يمكنك حذف طلب لا يخصك');
    }
    if (existing.status !== 'PENDING') {
      throw new AppError(400, 'لا يمكن حذف الطلب بعد مراجعته');
    }
    await prisma.purchaseRequest.delete({ where: { id: requestId } });
    return { success: true };
  }

  // ---------- Loans ----------
  async updateMyLoan(
    employeeId: string | undefined,
    loanId: string,
    data: { type?: string; amount?: number; installments?: number; reason?: string },
    _userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);
    const existing = await prisma.loan.findUnique({ where: { id: loanId } });
    if (!existing) throw new AppError(404, 'طلب القرض غير موجود');
    if (existing.employeeId !== empId) {
      throw new AppError(403, 'لا يمكنك تعديل طلب لا يخصك');
    }
    if (existing.status !== 'PENDING_HR') {
      throw new AppError(400, 'لا يمكن تعديل الطلب بعد مراجعته');
    }

    const amount = data.amount !== undefined ? Number(data.amount) : Number(existing.amount);
    const installments =
      data.installments !== undefined ? Number(data.installments) : existing.installments;
    const monthlyAmount = installments > 0 ? amount / installments : amount;

    return prisma.loan.update({
      where: { id: loanId },
      data: {
        type: data.type ?? existing.type,
        amount,
        installments,
        monthlyAmount,
        reason: data.reason !== undefined ? data.reason : existing.reason,
      },
    });
  }

  async deleteMyLoan(employeeId: string | undefined, loanId: string, _userId: string) {
    const empId = this.requireEmployeeId(employeeId);
    const existing = await prisma.loan.findUnique({ where: { id: loanId } });
    if (!existing) throw new AppError(404, 'طلب القرض غير موجود');
    if (existing.employeeId !== empId) {
      throw new AppError(403, 'لا يمكنك حذف طلب لا يخصك');
    }
    if (existing.status !== 'PENDING_HR') {
      throw new AppError(400, 'لا يمكن حذف الطلب بعد مراجعته');
    }
    await prisma.loan.delete({ where: { id: loanId } });
    return { success: true };
  }

  // ---------- Overtime requests (stored as attendance rows) ----------
  async updateMyOvertimeRequest(
    employeeId: string | undefined,
    attendanceId: string,
    data: { hours?: number; reason?: string },
    _userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);
    const att = await prisma.attendance.findUnique({ where: { id: attendanceId } });
    if (!att) throw new AppError(404, 'طلب الساعات الإضافية غير موجود');
    if (att.employeeId !== empId) {
      throw new AppError(403, 'لا يمكنك تعديل طلب لا يخصك');
    }
    const parsed = this.parseOvertimeRequestNote(att.notes);
    if (!parsed) throw new AppError(400, 'هذا السجل ليس طلب ساعات إضافية');
    if (parsed.status !== 'PENDING') {
      throw new AppError(400, 'لا يمكن تعديل الطلب بعد مراجعته');
    }

    const hours = data.hours !== undefined ? Number(data.hours) : parsed.hours;
    const reason = data.reason !== undefined ? data.reason : parsed.reason;

    if (!hours || hours <= 0) throw new AppError(400, 'عدد الساعات غير صالح');
    if (!reason || !String(reason).trim()) throw new AppError(400, 'سبب الساعات الإضافية مطلوب');

    const updatedNote = this.buildOvertimeRequestNote(hours, String(reason).trim(), 'PENDING');
    const updated = await prisma.attendance.update({
      where: { id: attendanceId },
      data: { overtimeHours: hours, notes: updatedNote },
      include: {
        employee: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            uniqueEmployeeId: true,
            reportingToId: true,
          },
        },
      },
    });
    return this.formatOvertimeRequest(updated);
  }

  async deleteMyOvertimeRequest(
    employeeId: string | undefined,
    attendanceId: string,
    _userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);
    const att = await prisma.attendance.findUnique({ where: { id: attendanceId } });
    if (!att) throw new AppError(404, 'طلب الساعات الإضافية غير موجود');
    if (att.employeeId !== empId) {
      throw new AppError(403, 'لا يمكنك حذف طلب لا يخصك');
    }
    const parsed = this.parseOvertimeRequestNote(att.notes);
    if (!parsed) throw new AppError(400, 'هذا السجل ليس طلب ساعات إضافية');
    if (parsed.status !== 'PENDING') {
      throw new AppError(400, 'لا يمكن حذف الطلب بعد مراجعته');
    }
    // Clear the overtime request markers but keep the attendance row intact.
    await prisma.attendance.update({
      where: { id: attendanceId },
      data: { overtimeHours: 0, notes: null },
    });
    return { success: true };
  }

  // ---------- Expense claims ----------
  async updateMyExpenseClaim(
    employeeId: string | undefined,
    claimId: string,
    data: {
      items?: Array<{
        expenseDate?: string;
        amount?: number | string;
        entityName?: string;
        description?: string;
        projectOrTender?: string;
        proofRef?: string;
      }>;
      description?: string;
      removeAttachmentIds?: string[];
    },
    userId: string,
    newFiles?: Express.Multer.File[]
  ) {
    const empId = this.requireEmployeeId(employeeId);
    const existing = await prisma.expenseClaim.findUnique({ where: { id: claimId } });
    if (!existing) throw new AppError(404, 'كشف المصاريف غير موجود');
    if (existing.employeeId !== empId) {
      throw new AppError(403, 'لا يمكنك تعديل طلب لا يخصك');
    }
    if (existing.status !== 'PENDING') {
      throw new AppError(400, 'لا يمكن تعديل الطلب بعد مراجعته');
    }

    const items = Array.isArray(data.items) ? data.items : [];
    const normalizedItems = items
      .map((item) => ({
        expenseDate: item.expenseDate || '',
        amount: Number(item.amount || 0),
        entityName: item.entityName?.trim() || '',
        description: item.description?.trim() || '',
        projectOrTender: item.projectOrTender?.trim() || '',
        proofRef: item.proofRef?.trim() || '',
      }))
      .filter((item) => item.description && item.amount > 0 && item.expenseDate);

    if (normalizedItems.length === 0) {
      throw new AppError(400, 'يجب إضافة بند واحد على الأقل مع التاريخ والمبلغ والبيان');
    }

    const totalAmount = normalizedItems.reduce(
      (sum, item) => sum + Number(item.amount || 0),
      0
    );
    const firstItem = normalizedItems[0];

    // Remove selected attachments (DB + file on disk).
    if (data.removeAttachmentIds && data.removeAttachmentIds.length > 0) {
      const attachments = await prisma.attachment.findMany({
        where: {
          id: { in: data.removeAttachmentIds },
          entityType: 'EXPENSE_CLAIM',
          entityId: claimId,
        },
      });
      for (const a of attachments) {
        try {
          if (a.path && fs.existsSync(a.path)) fs.unlinkSync(a.path);
        } catch {
          /* swallow */
        }
      }
      await prisma.attachment.deleteMany({
        where: { id: { in: data.removeAttachmentIds }, entityType: 'EXPENSE_CLAIM', entityId: claimId },
      });
    }

    const updated = await prisma.expenseClaim.update({
      where: { id: claimId },
      data: {
        items: normalizedItems as any,
        totalAmount,
        expenseDate: new Date(firstItem.expenseDate),
        amount: totalAmount,
        description: data.description?.trim() || null,
        projectOrTender: firstItem.projectOrTender || null,
      },
      include: {
        employee: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            uniqueEmployeeId: true,
          },
        },
      },
    });

    if (newFiles && newFiles.length > 0) {
      await Promise.all(
        newFiles.map((file) =>
          prisma.attachment.create({
            data: {
              entityType: 'EXPENSE_CLAIM',
              entityId: claimId,
              fileName: path.basename(file.path),
              originalName: (file as any).decodedOriginalName || file.originalname,
              mimeType: file.mimetype,
              size: file.size,
              path: file.path,
              category: 'EXPENSE_CLAIM_ATTACHMENT',
              uploadedBy: userId,
            },
          })
        )
      );
    }

    const [withFiles] = await this.attachExpenseClaimFiles([updated]);
    return withFiles;
  }

  async deleteMyExpenseClaim(
    employeeId: string | undefined,
    claimId: string,
    _userId: string
  ) {
    const empId = this.requireEmployeeId(employeeId);
    const existing = await prisma.expenseClaim.findUnique({ where: { id: claimId } });
    if (!existing) throw new AppError(404, 'كشف المصاريف غير موجود');
    if (existing.employeeId !== empId) {
      throw new AppError(403, 'لا يمكنك حذف طلب لا يخصك');
    }
    if (existing.status !== 'PENDING') {
      throw new AppError(400, 'لا يمكن حذف الطلب بعد مراجعته');
    }

    const attachments = await prisma.attachment.findMany({
      where: { entityType: 'EXPENSE_CLAIM', entityId: claimId },
    });
    for (const a of attachments) {
      try {
        if (a.path && fs.existsSync(a.path)) fs.unlinkSync(a.path);
      } catch {
        /* swallow */
      }
    }
    await prisma.attachment.deleteMany({
      where: { entityType: 'EXPENSE_CLAIM', entityId: claimId },
    });
    await prisma.expenseClaim.delete({ where: { id: claimId } });
    return { success: true };
  }
}

export const portalService = new PortalService();