import { getToken } from "next-auth/jwt";
import { authOptions } from './auth/[...nextauth]'
import { getServerSession } from "next-auth/next"
import { NextApiRequest, NextApiResponse } from 'next'
import { DayOfWeek, AvailabilityType, UserRole, EventLogType } from '@prisma/client';
const common = require('../../src/helpers/common');
const dataHelper = require('../../src/helpers/data');
const subq = require('../../prisma/bl/subqueries');
import { set, format, addMinutes, addDays } from 'date-fns';

import fs from 'fs';
import path from 'path';
import { all } from "axios";
import { logger } from "src/helpers/common";
import { generatePublishersExcel } from "src/helpers/excel";

/**
 * 
 * @param req import { NextApiRequest, NextApiResponse } from 'next'
 * @param res import { NextApiRequest, NextApiResponse } from 'next'
 */
export default async function handler(req, res) {
  const prisma = common.getPrismaClient();

  // Retrieve and validate the JWT token
  const token = await getToken({ req: req });
  if (!token) {
    // If no token or invalid token, return unauthorized status
    return res.status(401).json({ message: "Unauthorized to call this API endpoint" });
  }
  else {
    // If token is valid, log the user
    //console.log("JWT | User: " + token.email);
  }

  var action = req.query.action;
  var filter = req.query.filter;
  let day: Date;
  let isExactTime;
  if (req.query.date) {
    day = new Date(req.query.date);
    //date.setDate(date.getDate()); // Subtract one day to get the correct date, as calendar sends wrong date (one day ahead)
    //date.setHours(0, 0, 0, 0);
  }
  if (req.query.filterDate) {
    day = new Date(req.query.filterDate);
    isExactTime = true;
  }

  let monthInfo = common.getMonthDatesInfo(day);
  const searchText = req.query.searchText?.normalize('NFC');

  const sessionServer = await getServerSession(req, res, authOptions)
  var isAdmin = sessionServer?.user.role == UserRole.ADMIN

  try {
    switch (action) {
      case "initDb":
        // Read the SQL script from the file
        const sqlFilePath = path.join(process.cwd(), 'prisma', 'data.sql');
        const sql = fs.readFileSync(sqlFilePath, 'utf8');

        // Execute the SQL script
        await prisma.$executeRawUnsafe(sql);
        res.status(200).json({ message: "SQL script executed successfully" });
        break;

      case "settings":
        try {
          const key = req.query.key;
          switch (req.method) {
            case "LIST":
              let s1 = await prisma.settings.findMany();
              res.status(200).json(s1.map(setting => setting.key));
              break;
            case "GET":
              const s2 = await prisma.settings.findUnique({
                where: {
                  key: key
                }
              });
              res.status(200).json(s2);

              break;
            case "PUT": //create or update
              const value = req.query.value;
              const results = await prisma.settings.upsert({
                where: {
                  key: key
                },
                update: {
                  value: value
                },
                create: {
                  key: key,
                  value: value
                }
              });
              res.status(200).json(results);
              break;
            default:
              res.status(405).json({ message: "Method Not Allowed" });
              break;
          }
        } catch (error) {
          console.error("Error getting settings: " + error);
          res.status(500).json({ error });
        }
        break;


      case "deleteAllPublishers":
        //get filter and delete all publishers containing that in first name or last name
        await prisma.publisher.deleteMany({
          where: {
            OR: [
              { firstName: { contains: filter } },
              { lastName: { contains: filter } },
            ],
          },
        });
        res.status(200).json({ "message": "ok" });
        break;
      case "deleteAllAvailabilities":
        //get filter and delete all publishers containing that in first name or last name
        await prisma.availability.deleteMany({
          where: filter ? {
            OR: [
              // { name: { contains: filter } },
              { starTime: { lte: day } }
            ]
          } : {}
        });
        res.status(200).json({ "message": "ok" });
        break;
      //gets publisher by names with availabilities and assignments 
      case "deleteAvailabilityForPublisher":
        let publisherId = req.query.publisherId;
        let dateFor;
        if (req.query.date) {
          dateFor = new Date(req.query.date);
          //get month info from date
          monthInfo = common.getMonthDatesInfo(dateFor);
        }
        const deleteFromPreviousAssignments = common.parseBool(req.query.deleteFromPreviousAssignments);
        // if datefor is not null/undefined, delete availabilities for that month
        try {
          await prisma.availability.deleteMany({
            where: {
              publisherId: publisherId,
              startTime: { gte: monthInfo?.firstMonday },
              endTime: { lte: monthInfo?.lastSunday }
            }
          });
          if (deleteFromPreviousAssignments) {
            await prisma.availability.deleteMany({
              where: {
                publisherId: publisherId,
                isFromPreviousAssignment: true
              }
            });
          }
          // await prisma.availability.deleteMany({
          //   where: {
          //     publisherId: publisherId
          //   }
          // });
          res.status(200).json({ "message": "ok" });
        } catch (error) {
          console.error("Error deleting availability for publisher: " + publisherId + " error: " + error);
          res.status(500).json({ error });
        }
        break;

      case "createAvailabilities": {
        const availabilities = req.body;
        //! console.log("createAvailabilities: " + JSON.stringify(availabilities));
        try {
          let createResults = await prisma.availability.createMany({
            data: availabilities
          });
          res.status(200).json({ "message": "ok", "results": createResults });
        } catch (error) {
          console.error("Error creating availabilities: " + error);
          res.status(500).json({ error });
        }
      }
        break;

      case "getCalendarEvents":
        let events = await dataHelper.getCalendarEvents(req.query.publisherId, true, true, isAdmin);
        res.status(200).json(events);

      case "getPublisherInfo":

        // let pubs = await filterPublishers("id,firstName,lastName,email".split(","), "", null, req.query.assignments || true, req.query.availabilities || true, false, req.query.id);
        let pubs = await dataHelper.filterPublishersNew("id,firstName,lastName,email,isActive,assignments,availabilities", day, false, true, false, true, false, req.query.id, true);
        let pub = pubs[0] || {};
        if (pub) {
          let dayOfWeekQuery = common.getDayOfWeek(day);

          pub.availabilities = pub.availabilities.map(avail => {
            if (avail.dayOfMonth == null) {
              let dayOfWeek = common.getDayOfWeekIndex(avail.dayofweek);
              let newStart = new Date(day);
              newStart = addDays(newStart, dayOfWeek - dayOfWeekQuery);
              newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0);
              let newEnd = new Date(newStart);
              newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
              return {
                ...avail,
                startTime: newStart,
                endTime: newEnd
              }
            }
            return avail;
          });
        }
        //let pubs = await dataHelper.filterPublishersNew("id,firstName,lastName,email,isActive,assignments,availabilities", day, false, true, false, true, false, req.query.id);
        res.status(200).json(pub);
        break;
      case "filterPublishersNew":
        let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities);
        let results = await filterPublishersNew_Available(req.query.select, day,
          common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth), true, includeOldAvailabilities, req.query.id);
        res.status(200).json(results);
        break;

      case "getMonthlyStatistics":
        let allpubs = await getMonthlyStatistics("id,firstName,lastName,email", day);
        res.status(200).json(allpubs);
        break;

      case "getUnassignedPublishers":
        //let monthInfo = common.getMonthDatesInfo(date);
        let allPubs = await filterPublishers("id,firstName,lastName,email,isActive".split(","), "", day, true, true, false);
        let unassignedPubs = allPubs.filter(pub => pub.currentMonthAssignments == 0 && pub.availabilities.length > 0);
        res.status(200).json(unassignedPubs);
        break;
      case "filterPublishers":
        const fetchAssignments = common.parseBool(req.query.assignments);
        const fetchAvailabilities = common.parseBool(req.query.availabilities);
        let publishers = await filterPublishers(req.query.select, searchText, day, fetchAssignments, fetchAvailabilities);
        //!console.log("publishers: (" + publishers.length + ") " + JSON.stringify(publishers.map(pub => pub.firstName + " " + pub.lastName)));
        res.status(200).json(publishers);
        break;

      // find publisher by full name or email
      case "findPublisher":
        const getAll = common.parseBool(req.query.all) || false;
        let publisher = await dataHelper.findPublisher(filter, req.query.email, req.query.select, getAll);
        res.status(200).json(publisher);
        break;

      case "getShiftsForDay":

        // Setting the range for a day: starting from the beginning of the date and ending just before the next date.
        let startOfDay = new Date(day.getFullYear(), day.getMonth(), day.getDate());
        let endOfDay = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 23, 59, 59, 999);

        const pubAvCount = await prisma.publisher.findMany({
          select: {
            id: true,
            firstName: true,
            lastName: true,
            _count: {
              select: {
                availabilities: true,
              }
            }
          }
        });

        let shiftsForDate = await prisma.shift.findMany({
          where: {
            startTime: {
              gte: startOfDay,
              lt: endOfDay
            },
          },
          include: {
            assignments: {
              include: {
                publisher: subq.publisherSelect
              },
            },
          },
        });

        shiftsForDate.forEach(shift => {
          shift.assignments.forEach(assignment => {
            assignment.publisher.availabilityCount = pubAvCount.find(pub => pub.id === assignment.publisher.id)?._count?.availabilities || 0;
          });
        }
        );

        console.log("shiftsForDate(" + day + ") - " + shiftsForDate.length + " : " + JSON.stringify(shiftsForDate.map(shift => shift.id)));

        res.status(200).json(shiftsForDate);
        break;

      case "copyOldAvailabilities":
        //get all publishers that don't have availabilities for the current month
        monthInfo = common.getMonthDatesInfo(day);
        // await prisma.availability.deleteMany({
        //   where: {
        //     startTime: {
        //       gte: monthInfo.firstMonday,
        //     },
        //     isFromPreviousMonth: true
        //   }
        // });
        let outdatedPubs = await prisma.publisher.findMany({
          where: {
            availabilities: {
              none: {
                startTime: {
                  gte: monthInfo.firstMonday,
                }
              }
            }
          },
          select: {
            id: true,
            firstName: true,
            lastName: true,
            availabilities: true
          }
        });
        outdatedPubs.forEach(async pub => {
          //  avail.startTime >= monthInfo.firstMonday
          //get prev month date:
          let prevMonth = new Date(monthInfo.firstMonday);
          prevMonth.setMonth(prevMonth.getMonth() - 1);
          let prevMonthInfo = common.getMonthDatesInfo(prevMonth);
          pub.availabilities = pub.availabilities.filter(avail => avail.startTime > prevMonthInfo.firstMonday);
          console.log("" + pub.firstName + " " + pub.lastName + " copying " + pub.availabilities.length + " availabilities from previous months.");
          pub.availabilities.forEach(async avail => {
            //get the new date based on the day of week and week of month
            if (!avail.weekOfMonth) {
              avail.weekOfMonth = common.getWeekOfMonth(avail.startTime)
            }
            let origMonthInfo = common.getMonthDatesInfo(avail.startTime);

            let newStart = common.getDateFromWeekNrAndDayOfWeek(monthInfo.firstMonday, avail.weekOfMonth, avail.dayofweek, avail.startTime);
            //ToDo: fix double check. also check if we're in 5th week and the month has 4 weeks
            // const availability = await data.findPublisherAvailability(publisher.id, newStart);
            // if (availability) {
            //   return;
            // }
            let newEnd = new Date(newStart.getTime());
            newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
            let data = {
              publisherId: pub.id,
              dayOfMonth: null,
              dayofweek: avail.dayofweek || common.getDayOfWeekNameEnEnumForDate(avail.startTime),
              weekOfMonth: avail.weekofMonth || common.getWeekOfMonth(avail.startTime),
              // null for auto generated availabilities
              //dateOfEntry: new Date(), //avail.dateOfEntry || avail.startTime,
              startTime: newStart,
              endTime: newEnd,
              type: AvailabilityType.Monthly,
              isFromPreviousMonth: true,
              name: avail.name || "старо предпочитание",
              parentAvailabilityId: avail.id,
              // parentAvailability: {
              //   connect: {
              //     id: avail.id
              //   }
              // },
            }
            await prisma.availability.create({ data: data });

            //if month has 5 weeks and the monthInfo has 4 weeks copy the availabilities also from the 1st week to the 5th week
            if (monthInfo.nrOfWeeks == 5 && avail.weekOfMonth == 1 && origMonthInfo.nrOfWeeks == 4) {
              newStart = common.getDateFromWeekNrAndDayOfWeek(monthInfo.firstMonday, 5, avail.dayofweek, avail.startTime);
              newEnd = new Date(newStart.getTime());
              newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
              data.weekOfMonth = 5;
              data.startTime = newStart;
              data.endTime = newEnd;
              await prisma.availability.create({ data: data });
            }

          });
        });

        //convert old assignments to availabilities

        res.status(200).json({ "message": "ok" });
        break;
      case "deleteCopiedAvailabilities":
        //delete all availabilities that are copied from previous months
        monthInfo = common.getMonthDatesInfo(day);
        await prisma.availability.deleteMany({
          where: {
            startTime: {
              gte: monthInfo.firstMonday,
            },
            isFromPreviousMonth: true
          }
        });

      case "replaceInAssignment":
        const { oldPublisherId, newPublisherId, shiftId } = req.method === "POST" ? req.body : req.query;

        const result = await replaceInAssignment(oldPublisherId, newPublisherId, shiftId);
        res.status(200).json(result);
        break;

      case "updateShifts":
        //get all shifts for the month and publish them (we pass date )
        let isPublished = common.parseBool(req.query.isPublished);
        let updated = await prisma.shift.updateMany({
          where: {
            startTime: {
              gte: new Date(monthInfo.firstMonday.getFullYear(), monthInfo.firstMonday.getMonth(), monthInfo.firstMonday.getDate()),
              lt: new Date(monthInfo.lastSunday.getFullYear(), monthInfo.lastSunday.getMonth() + 1, 1),
            }
          },
          data: {
            isPublished: isPublished
          }
        });
        console.log("Updated shifts: " + updated.count);
        res.status(200).json({ "message": "ok" });

        break;
      case "getPossibleShiftPublisherEmails":
        let data = await dataHelper.getCoverMePublisherEmails(parseInt(req.query.shiftId));
        res.status(200).json(data);
        break;
      case "getAllPublishersWithStatistics":
        let noEndDate = common.parseBool(req.query.noEndDate);
        res.status(200).json(await dataHelper.getAllPublishersWithStatisticsMonth(day, noEndDate));
      case "exportPublishersExcel":
        try {
          const today = new Date();
          const dateStr = today.toISOString().split('T')[0]; // Gets YYYY-MM-DD format

          const excelBuffer = await generatePublishersExcel();
          res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
          res.setHeader("Content-Disposition", "attachment; filename=" + encodeURI(`Publishers_${dateStr}.xlsx`));
          res.send(excelBuffer);
        } catch (error) {
          console.error(JSON.stringify(error));
          res.status(500).json({ error: "Failed to generate Excel file" });
        }
        break;
      default:
        res.status(200).json({
          "message": "no action '" + action + "' found"
        });
        break;
    }
  } catch (error) {
    console.error("API: Error executing action: " + action + " with filter: " + filter + " error: " + error);
    res.status(500).json({ error });
  }
}



export async function getMonthlyStatistics(selectFields, filterDate) {

  let publishers = [];
  selectFields = selectFields?.split(",");
  let selectBase = selectFields.reduce((acc, curr) => {
    acc[curr] = true;
    return acc;
  }, {});
  selectBase.assignments = {
    select: {
      id: true,
      shift: {
        select: {
          id: true,
          startTime: true,
          endTime: true
        }
      }
    }
  };
  let currentWeekStart: Date, currentWeekEnd: Date,
    currentMonthStart: Date, currentMonthEnd: Date,
    previousMonthStart: Date, previousMonthEnd: Date;


  let date = new Date(filterDate);
  date.setDate(filterDate.getDate());
  currentWeekStart = common.getStartOfWeek(date);
  currentWeekEnd = common.getEndOfWeek(date);

  var monthInfo = common.getMonthDatesInfo(date);
  currentMonthStart = monthInfo.firstMonday;
  currentMonthEnd = monthInfo.lastSunday;
  date.setMonth(date.getMonth() - 1);
  monthInfo = common.getMonthDatesInfo(date);
  previousMonthStart = monthInfo.firstMonday;
  previousMonthEnd = monthInfo.lastSunday;


  const prisma = common.getPrismaClient();
  publishers = await prisma.publisher.findMany({
    select: {
      ...selectBase,
    }
  });

  publishers.forEach(pub => {
    // Debug logs to help identify issues
    pub.currentWeekAssignments = pub.assignments.filter(assignment => {
      return assignment.shift.startTime >= currentWeekStart && assignment.shift.startTime <= currentWeekEnd;
    }).length;
    pub.currentMonthAssignments = pub.assignments.filter(assignment => {
      return assignment.shift.startTime >= currentMonthStart && assignment.shift.startTime <= currentMonthEnd;
    }).length;
    pub.previousMonthAssignments = pub.assignments.filter(assignment => {
      return assignment.shift.startTime >= previousMonthStart && assignment.shift.startTime <= previousMonthEnd;
    }).length;
  });
  return publishers;
}


export async function filterPublishersNew_Available(selectFields, filterDate, isExactTime = false, isForTheMonth = false, isWithStats = true, includeOldAvailabilities = false) {
  return dataHelper.filterPublishersNew(selectFields, filterDate, isExactTime, isForTheMonth, false, isWithStats, includeOldAvailabilities);
  // async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, noEndDateFilter = false, isWithStats = true, includeOldAvailabilities = false, id = null, filterAvailabilitiesByDate = true) 
}

// availabilites filter:
// 1. if dayOfMonth is null, match by day of week (enum)
// 2. if dayOfMonth is not null, match by date
// 3. if date is 00:00:00, match by date only (without time)
// 4. if date is not 00:00:00, it should be in the range of start and end times
// this way we distinguish between weekly availabiillities (entered without dayOfMonth) and old availabilities from previous months (entered with dayOfMonth, but we set it to null),
// (To validate) we use useDateFilter in combination with the filterDate to get publishers without availabilities for the day:
// 1: useDateFilter = false, filterDate = null - get all publishers with availabilities for the current month
// 2: useDateFilter = false, filterDate = date - get all publishers with availabilities for the current month
// 3: useDateFilter = true, filterDate = null - get all publishers with availabilities for the current month
// 4: useDateFilter = true, filterDate = date - get all publishers with availabilities for the current month and filter by date

export async function filterPublishers(selectFields, searchText, filterDate, fetchAssignments: boolean = true, fetchAvailabilities: boolean = true, useDateFilter = true, id = null) {

  let currentWeekStart: Date, currentWeekEnd: Date,
    currentMonthStart: Date, currentMonthEnd: Date,
    previousMonthStart: Date, previousMonthEnd: Date,
    filterDateEnd: Date,
    publishers = [];

  if (!filterDate) {
    useDateFilter = false;
    previousMonthStart = new Date();
    previousMonthStart.setDate(1);
    previousMonthStart.setMonth(previousMonthStart.getMonth() - 1);
  }
  else {

    let date = new Date(filterDate.getTime());
    //date.setDate(filterDate.getDate());
    currentWeekStart = common.getStartOfWeek(date);
    currentWeekEnd = common.getEndOfWeek(date);

    var monthInfo = common.getMonthDatesInfo(date);
    currentMonthStart = monthInfo.firstMonday;
    currentMonthEnd = monthInfo.lastSunday;
    date.setMonth(date.getMonth() - 1);
    monthInfo = common.getMonthDatesInfo(date);
    previousMonthStart = monthInfo.firstMonday;
    previousMonthEnd = monthInfo.lastSunday;

    filterDateEnd = new Date(filterDate);
    filterDateEnd.setHours(23, 59, 59, 999);
  }


  let whereClause = {};
  if (id) {
    whereClause = {
      id: String(id)
    }
  }
  const searchTextString = String(searchText || "").trim();
  if (searchTextString !== "") {
    whereClause = {
      OR: [
        { firstName: { contains: searchTextString } },
        { lastName: { contains: searchTextString } },
      ],
    };
  }

  // Base select fields

  // Only attempt to split if selectFields is a string; otherwise, use it as it is.
  selectFields = typeof selectFields === 'string' ? selectFields.split(",") : selectFields;

  let selectBase = selectFields.reduce((acc, curr) => {
    acc[curr] = true;
    return acc;
  }, {});


  // If assignments flag is true, fetch assignments
  if (fetchAssignments) {

    //!! WORKING CODE, but heavy on the DB !!
    selectBase.assignments = {
      select: {
        id: true,
        shift: {
          select: {
            id: true,
            startTime: true,
            endTime: true,
            isPublished: true
          }
        }
      },
      where: {
        shift: {
          OR: [
            // {
            //   startTime: {
            //     gte: currentWeekStart,
            //     lte: currentWeekEnd
            //   }
            // },
            // {
            //   startTime: {
            //     gte: currentMonthStart,
            //     lte: currentMonthEnd
            //   }
            // },
            {
              startTime: {
                gte: previousMonthStart,
                // lte: previousMonthEnd
              }
            },

          ]
        }
      }
    };
    //selectBase.assignments = true;
  }

  let dayOfWeekEnum: DayOfWeek
  if (filterDate) {
    // Determine day of week using common function
    dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterDate);
    if (filterDate.getHours() > 21 || filterDate.getHours() < 6) {
      filterDate.setHours(0, 0, 0, 0); // Set to midnight
    }
  }
  // console.log(`filterDate: ${filterDate}`);
  // console.log(`filterDateEnd: ${filterDateEnd}`);


  if (filterDate && useDateFilter) {
    // Info, description and ToDo:
    // We should distinguish between availabilities with dayOfMonth and without
    // If dayOfMonth is null, we should match by day of week using the enum
    // If dayOfMonth is not null, we should match by date.
    // if date is 00:00:00, we should match by date only (without time)
    // if date is not 00:00:00, it should be in the range of start and end times
    // we should also include availabilities from previous assignments but not with preference - dayOfMonth is null. we shuold include them only if they match the day of week
    // and distinguish between weekly availabiillities (entered without dayOfMonth) and old availabilities from previous months (entered with dayOfMonth, but we set it to null),
    // which we count as weekly availabilities. We can use the type field for that
    //console.log(`filterDate: ${filterDate}. date: ${filterDate.getDate()}. dayOfWeekEnum: ${dayOfWeekEnum}. useDateFilter: ${useDateFilter}`);

    // we will have 3 cases: up-to date availabilities, old availabilities from previous months and availabilities from previous assignments but not with preference
    // we will use the type field to distinguish between them
    // up-to date availabilities will have type = 1
    // old availabilities from previous months will have type = 2 - we want to drop that function to simplify the code and avoid confusion
    // availabilities from previous assignments but not with preference will have type = 3
    // also, permanent weekly availabilities will have dayOfMonth = null and type = 0
    // for 0 we will match by dayOfWeekEnum and times
    // for 1 we will match by exact date and times
    // for 2 we will match by dayofweek, weekOfMonth and times
    // for 3 we will match by dayofweek, weekOfMonth and times - this is the same as 2, but we will not count them as availabilities for the current month


    // generaion of schedule:
    /*
    option 1: fill from blank - first two places per shift, then more if possible
    option 2: fill from previous schedule , remove all assignments where new availabilities are not available 
    and permanent availabilities to make room for changes (we want to shuffle if possible??? do we?)
    continue with option 1 from there
    which one depends on if we prioritize empty shifts or making sure everyone has up to date availabilities
    */

    //substract the time difference between from ISO string and local time    
    const offset = filterDate.getTimezoneOffset() * 60000; // offset in milliseconds
    var dateAsISO = new Date(filterDate.getTime() + offset);
    //if full day, match by date only
    if (filterDate.getHours() == 0 || dateAsISO.getHours() == 0) {
      whereClause["availabilities"] = {
        some: {
          OR: [
            // Check only by date without considering time ( Assignments on specific days without time)
            {
              startTime: { gte: filterDate },
              endTime: { lte: filterDateEnd },
            }
            ,
            // Check if dayOfMonth is null and match by day of week using the enum (Assigments every week) 
            // This includes availabilities from previous assignments but not with preference 
            {
              dayOfMonth: null, // includes monthly and weekly repeats
              dayofweek: dayOfWeekEnum,
              // ToDo: and weekOfMonth
              startTime: { lte: filterDate },
              AND: [
                {
                  OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever
                    { endDate: { gte: filterDate } },
                    { endDate: null }
                  ]
                }
              ]
            }
          ]
        }
      };
    }
    //if not full day, match by date and time
    else {
      //match exact time (should be same as data.FindPublisherAvailability())
      whereClause["availabilities"] = {
        some: {
          OR: [
            // Check if dayOfMonth is set and filterDate is between start and end dates (Assignments on specific days AND time)
            {
              dayOfMonth: filterDate.getDate(),
              startTime: { lte: filterDate },
              endTime: { gte: filterDate }
            },
            // Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
            {
              dayOfMonth: null,
              dayofweek: dayOfWeekEnum,
              startTime: { gte: filterDate },
            }
          ]
        }
      };
    }
  } else {
    // we use month filter if date is passed and useDateFilter is false to get all publishers with availabilities for the current month
    if (fetchAvailabilities) {
      // If no filter date, return all publishers's availabilities for currentMonthStart
      whereClause["availabilities"] = {
        some: {
          OR: [
            // Check if dayOfMonth is not null and startTime is after currentMonthStart (Assignments on specific days AND time)
            {
              dayOfMonth: { not: null },
              startTime: { gte: currentMonthStart },
              endTime: { lte: currentMonthEnd }
            },
            // Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
            {
              dayOfMonth: null,
            }
          ]
        }
      };
      //try here
      // selectBase.аvailabilities = {
      //   select: {
      //     dayofweek: true,
      //     dayOfMonth: true,
      //     startTime: true,
      //     endTime: true,
      //     weekOfMonth: true,
      //     type: true
      //   },
      //   where: {
      //     OR: [
      //       {
      //         startTime: { gte: currentMonthStart },
      //         endTime: { lte: currentMonthEnd }
      //       }
      //     ]
      //   }
      // }
    }
  }

  //include availabilities if flag is true
  const prisma = common.getPrismaClient(); //why we need to get it again?
  publishers = await prisma.publisher.findMany({
    where: whereClause,
    select: {
      ...selectBase,
      ...(fetchAvailabilities && { availabilities: true }),
      // ...(fetchAssignments && {
      //   assignments: {
      //     include: {
      //       shift: true,
      //     }
      //   }
      // }
      // )
    }
  });

  //console.log(`publishers: ${publishers.length}, WhereClause: ${JSON.stringify(whereClause)}`);

  if (filterDate) {

    if (fetchAssignments) {
      //get if publisher has assignments for current weekday, week, current month, previous month
      publishers.forEach(pub => {
        // Filter assignments for current day
        pub.currentDayAssignments = pub.assignments?.filter(assignment => {
          return assignment.shift.startTime >= filterDate && assignment.shift.startTime <= filterDateEnd;
        }).length;

        // Filter assignments for current week
        pub.currentWeekAssignments = pub.assignments?.filter(assignment => {
          return assignment.shift.startTime >= currentWeekStart && assignment.shift.startTime <= currentWeekEnd;
        }).length;

        // Filter assignments for current month
        pub.currentMonthAssignments = pub.assignments?.filter(assignment => {
          return assignment.shift.startTime >= currentMonthStart && assignment.shift.startTime <= currentMonthEnd;
        }).length;

        // Filter assignments for previous month
        pub.previousMonthAssignments = pub.assignments?.filter(assignment => {
          return assignment.shift.startTime >= previousMonthStart && assignment.shift.startTime <= previousMonthEnd;
        }).length;
      });
    }
  }

  if (fetchAvailabilities) {
    //get the availabilities for the day. Calcullate:
    //1. how many days the publisher is available for the current month - only with dayOfMonth
    //2. how many days the publisher is available without dayOfMonth (previous months count)
    //3. how many hours in total the publisher is available for the current month
    publishers.forEach(pub => {
      pub.currentMonthAvailability = pub.availabilities?.filter(avail => {
        // return avail.dayOfMonth != null && avail.startTime >= currentMonthStart && avail.startTime <= currentMonthEnd;
        return avail.startTime >= currentMonthStart && avail.startTime <= currentMonthEnd;
      })
      pub.currentMonthAvailabilityDaysCount = pub.currentMonthAvailability.length || 0;
      // pub.currentMonthAvailabilityDaysCount += pub.availabilities.filter(avail => {
      //   return avail.dayOfMonth == null;
      // }).length;
      pub.currentMonthAvailabilityHoursCount = pub.currentMonthAvailability.reduce((acc, curr) => {
        return acc + (curr.endTime.getTime() - curr.startTime.getTime()) / (1000 * 60 * 60);
      }, 0);

      //if pub has ever filled the form - if has availabilities which are not from previous assignments
      pub.hasEverFilledForm = pub.availabilities?.some(avail => {
        return avail.isFromPreviousAssignments == false;
      });

      //if pub has up-to-date availabilities (with dayOfMonth) for the current month
      pub.hasUpToDateAvailabilities = pub.availabilities?.some(avail => {
        return avail.dayOfMonth != null && avail.startTime >= currentMonthStart; // && avail.startTime <= currentMonthEnd;
      });

      //if pub has availabilities for the current day
      pub.hasAvailabilityForCurrentDay = pub.availabilities?.some(avail => {
        return avail.startTime >= filterDate && avail.startTime <= filterDateEnd;
      });

    });

    if (filterDate && useDateFilter) {
      // Post filter for time if dayOfMonth is null
      // Modify the availabilities array of the filtered publishers
      publishers.forEach(pub => {
        pub.availabilities = pub.availabilities?.filter(avail => matchesAvailability(avail, filterDate));
      });
    }

  }


  return publishers;
}

function matchesAvailability(avail, filterDate) {
  // Setting the start and end time of the filterDate
  filterDate.setHours(0, 0, 0, 0);
  const filterDateEnd = new Date(filterDate);
  filterDateEnd.setHours(23, 59, 59, 999);

  // Return true if avail.startTime is between filterDate and filterDateEnd
  return avail.startTime >= filterDate && avail.startTime <= filterDateEnd;
}



async function replaceInAssignment(oldPublisherId, newPublisherId, shiftId) {
  const prisma = common.getPrismaClient();
  const result = await prisma.assignment.updateMany({
    where: {
      publisherId: oldPublisherId,
      shiftId: shiftId
    },
    data: {
      publisherId: newPublisherId,
      originalPublisherId: oldPublisherId,
      isConfirmed: false,
      isBySystem: true,
      isMailSent: false
    }
  });

  // log the event
  let shift = await prisma.shift.findUnique({
    where: {
      id: shiftId
    },

    include: {
      cartEvent: {
        select: {
          location: {
            select: {
              name: true
            }
          }
        }
      },
      assignments: {
        include: {
          publisher: {
            select: {
              firstName: true,
              lastName: true,
              email: true
            }
          }
        }
      }
    }
  });
  let publishers = await prisma.publisher.findMany({
    where: {
      id: { in: [oldPublisherId, newPublisherId] }
    },
    select: {
      id: true,
      firstName: true,
      lastName: true,
      email: true
    }
  });
  let originalPublisher = publishers.find(p => p.id == oldPublisherId);
  let newPublisher = publishers.find(p => p.id == newPublisherId);
  let eventLog = await prisma.eventLog.create({
    data: {
      date: new Date(),
      publisher: { connect: { id: oldPublisherId } },
      shift: { connect: { id: shiftId } },
      type: EventLogType.AssignmentReplacementManual,
      content: "Заместване въведено от " + originalPublisher.firstName + " " + originalPublisher.lastName + ". Ще го замества " + newPublisher.firstName + " " + newPublisher.lastName + "."

    }
  });

  logger.info("User: " + originalPublisher.email + " replaced his assignment for " + shift.cartEvent.location.name + " " + shift.startTime.toISOString() + " with " + newPublisher.firstName + " " + newPublisher.lastName + "<" + newPublisher.email + ">. EventLogId: " + eventLog.id + "");


  return result;
}