import { HttpStatus, Injectable } from '@nestjs/common';
import {
  assertScopedAction,
  assertScope,
  assertToggle,
  resolveDataScope,
} from '../../common/access-control/permission-resolver.helpers';
import { PermissionUserContext } from '../../common/access-control/permission-context.types';
import { PermissionResolverService } from '../../common/access-control/permission-resolver.service';
import { OrgTimezoneService } from '../../common/timezone/org-timezone.service';
import { DatabaseService, ErrorResponse, logger } from '../../config';
import { NotificationTypeEnum } from '../../config/database/entities';
import {
  createGeneralError,
  getUserTimzoneDate,
  getUtcStartEndOfDate,
} from '../../utils';
import {
  buildHolidayRecurringRange,
  buildHolidaySummary,
  buildHolidayWhereFilter,
  buildUpcomingHolidayRecurringDates,
  ensureHolidayDepartmentIdsExist,
  getHolidayUtcMonthStartEndFromDate,
  includeHolidayDepartments,
} from '../../utils/holiday';
import { NotificationsService } from '../notifications/notifications.service';
import {
  createHolidayDto,
  holidayFilterDto,
  holidayResponseSchema,
  holidaysResponseSchema,
  updateHolidayDto,
} from './holidays.dto';

@Injectable()
export class HolidaysService {
  constructor(
    private readonly db: DatabaseService,
    private readonly notificationService: NotificationsService,
    private readonly permissionResolver: PermissionResolverService,
    private readonly orgTimezone: OrgTimezoneService,
  ) {}

  private async getPermissions(ctx: PermissionUserContext) {
    return this.permissionResolver.resolveForUser(ctx);
  }

  async createHoliday(
    data: createHolidayDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const timeZone = await this.orgTimezone.getTimezone(owner_id);
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'holidays.access');
      assertScopedAction(permissions, 'holidays.create', ctx.sub, ctx.sub);
      const { startUtc } = getUtcStartEndOfDate(data.start_date, timeZone);
      const { endUtc } = getUtcStartEndOfDate(data.end_date, timeZone);
      const startDate = data?.start_date;
      const endDate = data?.end_date;
      const departmentIds = data.department_ids ?? [];

      if (endDate < startDate) {
        throw createGeneralError(
          'End date must be after or equal to start date',
          'end_date',
          HttpStatus.BAD_REQUEST,
        );
      }

      await ensureHolidayDepartmentIdsExist(this.db, departmentIds, owner_id);

      const existingHoliday = await this.db.holidays.findFirst({
        where: {
          owner_id,
          name: data.name,
          start_date: startUtc,
          end_date: endUtc,
          is_recurring: data.is_recurring ?? false,
        },
        select: {
          id: true,
        },
      });

      if (existingHoliday) {
        throw createGeneralError(
          'Holiday with the same name and date range already exists',
          'name',
          HttpStatus.CONFLICT,
        );
      }

      const holiday = await this.db.holidays.create({
        data: {
          name: data.name,
          holiday_type: data.holiday_type ?? 'public',
          start_date: startDate,
          end_date: endDate,
          is_recurring: data.is_recurring ?? false,
          ...(data.description ? { description: data.description } : {}),
          ...(departmentIds.length > 0
            ? {
                departments: {
                  connect: departmentIds.map((id) => ({ id })),
                },
              }
            : {}),
          owner_id,
        },
        include: includeHolidayDepartments(),
      });

      this.notificationService.sendNotification({
        owner_id,
        title: data?.name,
        start_time: data?.start_date,
        end_time: data?.end_date,
        message: `A new holiday has been scheduled from ${getUserTimzoneDate(data.start_date, timeZone)} to ${getUserTimzoneDate(data.end_date, timeZone)}. Please review the holiday schedule and plan your work activities accordingly.`,
        type: NotificationTypeEnum.holiday,
        reference_id: holiday?.id,
        include_owner: true,
        include_admins: true,
        department_ids: departmentIds,
        meta: {
          holiday_id: holiday?.id,
          holiday_type: data?.holiday_type,
        },
      });

      const { data: parsedHoliday } = holidayResponseSchema.safeParse(holiday);

      return {
        data: parsedHoliday,
        message: 'Holiday created successfully',
        status: HttpStatus.CREATED,
        details: null,
      };
    } catch (error) {
      logger.error(`Error in createHoliday: ${String(error)}`);
      if (error instanceof ErrorResponse) {
        throw error;
      }
      throw createGeneralError(String(error), 'root', HttpStatus.BAD_REQUEST);
    }
  }

  async getHolidays(
    query: holidayFilterDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const timeZone = await this.orgTimezone.getTimezone(owner_id);
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'holidays.access');
      const scopeFilter = resolveDataScope(
        permissions,
        'holidays.view',
        query.data_scope,
        ctx,
      );

      let effectiveQuery = query;
      if (scopeFilter === 'own') {
        const member = await this.db.members.findUnique({
          where: { id: ctx.sub, owner_id },
          select: { department_id: true },
        });

        if (!member?.department_id) {
          const page = query.page ?? 1;
          const limit = query.limit ?? 10;
          return {
            data: [],
            message: 'Holidays retrieved successfully',
            status: HttpStatus.OK,
            details: {
              summary: {
                total_holidays: 0,
                total_recurring: 0,
                total_public: 0,
                total_company: 0,
              },
              pagination: {
                total_items: 0,
                total_pages: 0,
                current_page: page,
                items_per_page: limit,
                has_next_page: false,
                has_previous_page: false,
                next_page: null,
                previous_page: null,
              },
            },
          };
        }

        effectiveQuery = {
          ...query,
          department_id: member.department_id,
        };
      }

      const { startUtc, endUtc } = getHolidayUtcMonthStartEndFromDate(
        undefined,
        timeZone,
      );
      const from = effectiveQuery.start_date
        ? getUtcStartEndOfDate(effectiveQuery.start_date, timeZone).startUtc
        : startUtc;
      const to = effectiveQuery.end_date
        ? getUtcStartEndOfDate(effectiveQuery.end_date, timeZone).endUtc
        : endUtc;

      if (from > to) {
        throw createGeneralError(
          'Invalid date range. start date must be before end date',
          'start_date',
          HttpStatus.BAD_REQUEST,
        );
      }

      console.log(startUtc, endUtc, from, to);

      const page = effectiveQuery.page ?? 1;
      const limit = effectiveQuery.limit ?? 10;
      const where = { owner_id, ...buildHolidayWhereFilter(effectiveQuery) };

      const [nonRecurring, recurring] = await Promise.all([
        this.db.holidays.findMany({
          where: {
            ...where,
            is_recurring: false,
            start_date: {
              lte: to,
            },
            end_date: {
              gte: from,
            },
          },
          include: includeHolidayDepartments(),
        }),
        this.db.holidays.findMany({
          where: {
            ...where,
            is_recurring: true,
          },
          include: includeHolidayDepartments(),
        }),
      ]);

      // Filter by department on client-side if department_id is provided
      // This ensures all departments are included in the result
      const filterByDepartment = (holidays: any[]) => {
        if (!effectiveQuery.department_id) {
          return holidays;
        }
        return holidays.filter((holiday) =>
          holiday?.departments.some(
            (dept: any) => dept.id === effectiveQuery.department_id,
          ),
        );
      };

      const filteredNonRecurring = filterByDepartment(nonRecurring);
      const filteredRecurring = filterByDepartment(recurring);

      const recurringExpanded = filteredRecurring.flatMap((holiday) => {
        const baseStartDate = holiday?.start_date;
        const baseEndDate = holiday?.end_date;
        const startYear = from.getUTCFullYear();
        const endYear = to.getUTCFullYear();
        const rows: Array<any> = [];

        for (let year = startYear; year <= endYear; year += 1) {
          const occurrence = buildHolidayRecurringRange(
            baseStartDate,
            baseEndDate,
            year,
            timeZone,
          );
          if (!occurrence) {
            continue;
          }

          if (
            occurrence.occurrenceEnd < from ||
            occurrence.occurrenceStart > to
          ) {
            continue;
          }

          rows.push({
            ...holiday,
            start_date: occurrence.occurrenceStart,
            end_date: occurrence.occurrenceEnd,
            occurrence_date: occurrence.occurrenceStart.toISOString(),
            generated_from_recurring: true,
          });
        }

        return rows;
      });

      const fixedRows = filteredNonRecurring.map((holiday) => ({
        ...holiday,
        start_date: holiday?.start_date,
        end_date: holiday?.end_date,
        occurrence_date: holiday?.start_date?.toISOString(),
        generated_from_recurring: false,
      }));

      const merged = [...fixedRows, ...recurringExpanded]
        .sort((a, b) => {
          const aTime = new Date(a.start_date).getTime();
          const bTime = new Date(b.start_date).getTime();
          return aTime - bTime;
        })
        .map((holiday) => ({
          ...holiday,
          start_date: holiday?.start_date,
          end_date: holiday?.end_date,
        }));

      const total = merged.length;
      const totalPages = Math.ceil(total / limit);
      const start = (page - 1) * limit;
      const paginated = merged.slice(start, start + limit);
      const summary = buildHolidaySummary(
        merged.map((holiday) => ({
          start_date: holiday?.start_date,
          end_date: holiday?.end_date,
          is_recurring: holiday?.is_recurring,
          holiday_type: holiday?.holiday_type,
        })),
      );
      const { data: parsedHolidays } =
        holidaysResponseSchema.safeParse(paginated);

      return {
        data: parsedHolidays,
        message: 'Holidays retrieved successfully',
        status: HttpStatus.OK,
        details: {
          summary,
          pagination: {
            total_items: total,
            total_pages: totalPages,
            current_page: page,
            items_per_page: limit,
            has_next_page: page < totalPages,
            has_previous_page: page > 1,
            next_page: page < totalPages ? page + 1 : null,
            previous_page: page > 1 ? page - 1 : null,
          },
        },
      };
    } catch (error) {
      logger.error(`Error in getHolidays: ${String(error)}`);
      if (error instanceof ErrorResponse) {
        throw error;
      }
      throw createGeneralError(String(error), 'root', HttpStatus.BAD_REQUEST);
    }
  }

  async getHolidayById(
    id: string,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const timeZone = await this.orgTimezone.getTimezone(owner_id);
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'holidays.access');
      assertScope(permissions, 'holidays.view');

      const holiday = await this.db.holidays.findUnique({
        where: { id, owner_id },
        include: includeHolidayDepartments(),
      });

      if (!holiday) {
        throw createGeneralError(
          'Holiday not found',
          'holiday',
          HttpStatus.NOT_FOUND,
        );
      }

      const { data: parsedHoliday } = holidayResponseSchema.safeParse({
        ...holiday,
        occurrence_date: holiday?.start_date.toISOString(),
        generated_from_recurring: false,
      });

      return {
        data: parsedHoliday,
        message: 'Holiday retrieved successfully',
        status: HttpStatus.OK,
        details: holiday?.is_recurring
          ? {
              upcoming_occurrences: buildUpcomingHolidayRecurringDates(
                holiday?.start_date,
                5,
                timeZone,
              ),
            }
          : null,
      };
    } catch (error) {
      logger.error(`Error in getHolidayById: ${String(error)}`);
      if (error instanceof ErrorResponse) {
        throw error;
      }
      throw createGeneralError(String(error), 'root', HttpStatus.BAD_REQUEST);
    }
  }

  async updateHoliday(
    id: string,
    data: updateHolidayDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const timeZone = await this.orgTimezone.getTimezone(owner_id);
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'holidays.access');
      assertScope(permissions, 'holidays.update');

      const existingHoliday = await this.db.holidays.findUnique({
        where: { id, owner_id },
        include: includeHolidayDepartments(),
      });

      if (!existingHoliday) {
        throw createGeneralError(
          'Holiday not found',
          'holiday',
          HttpStatus.NOT_FOUND,
        );
      }

      const nextName = data.name ?? existingHoliday.name;
      const nextStartDate = data?.start_date
        ? data.start_date
        : existingHoliday.start_date;
      const nextEndDate = data?.end_date
        ? data.end_date
        : existingHoliday.end_date;
      const { startUtc } = getUtcStartEndOfDate(nextStartDate, timeZone);
      const { endUtc } = getUtcStartEndOfDate(nextEndDate, timeZone);

      if (nextEndDate < nextStartDate) {
        throw createGeneralError(
          'End date must be after or equal to start date',
          'end_date',
          HttpStatus.BAD_REQUEST,
        );
      }

      const conflictingHoliday = await this.db.holidays.findFirst({
        where: {
          owner_id,
          name: nextName,
          start_date: startUtc,
          end_date: endUtc,
          id: {
            not: id,
          },
        },
        select: {
          id: true,
        },
      });

      if (conflictingHoliday) {
        throw createGeneralError(
          'Holiday with the same name and date range already exists',
          'name',
          HttpStatus.CONFLICT,
        );
      }

      if (data.department_ids) {
        await ensureHolidayDepartmentIdsExist(
          this.db,
          data.department_ids,
          owner_id,
        );
      }

      console.log({ existingHoliday });

      const updatedHoliday = await this.db.holidays.update({
        where: { id },
        data: {
          ...(data.name ? { name: data.name } : {}),
          ...(data.holiday_type ? { holiday_type: data.holiday_type } : {}),
          ...(data.start_date ? { start_date: nextStartDate } : {}),
          ...(data.end_date ? { end_date: nextEndDate } : {}),
          ...(typeof data.is_recurring === 'boolean'
            ? { is_recurring: data.is_recurring }
            : {}),
          ...(data.description !== undefined
            ? { description: data.description }
            : {}),
          ...(data.department_ids !== undefined
            ? {
                departments: {
                  set: data.department_ids.map((id) => ({ id })),
                },
              }
            : {}),
        },
        include: includeHolidayDepartments(),
      });

      this.notificationService.sendNotification({
        owner_id,
        title: updatedHoliday?.name,
        start_time: data?.start_date,
        end_time: data?.end_date,
        message: `The holiday schedule has been updated for the period between ${getUserTimzoneDate(data.start_date)} and ${getUserTimzoneDate(data.end_date)}. Please check the latest holiday information for any changes.`,
        type: NotificationTypeEnum.holiday,
        reference_id: updatedHoliday?.id,
        include_owner: true,
        include_admins: true,
        department_ids:
          data?.department_ids ??
          existingHoliday?.departments?.map((dept) => dept.id) ??
          [],
        meta: {
          holiday_id: updatedHoliday?.id,
          holiday_type: updatedHoliday?.holiday_type,
        },
      });

      const { data: parsedHoliday } =
        holidayResponseSchema.safeParse(updatedHoliday);

      return {
        data: parsedHoliday,
        message: 'Holiday updated successfully',
        status: HttpStatus.OK,
        details: null,
      };
    } catch (error) {
      logger.error(`Error in updateHoliday: ${String(error)}`);
      if (error instanceof ErrorResponse) {
        throw error;
      }
      throw createGeneralError(String(error), 'root', HttpStatus.BAD_REQUEST);
    }
  }

  async deleteHoliday(
    id: string,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'holidays.access');
      assertScope(permissions, 'holidays.delete');

      const existingHoliday = await this.db.holidays.findUnique({
        where: { id, owner_id },
      });

      if (!existingHoliday) {
        throw createGeneralError(
          'Holiday not found',
          'holiday',
          HttpStatus.NOT_FOUND,
        );
      }

      await this.db.holidays.delete({
        where: { id },
      });

      return {
        data: null,
        message: 'Holiday deleted successfully',
        status: HttpStatus.OK,
        details: null,
      };
    } catch (error) {
      logger.error(`Error in deleteHoliday: ${String(error)}`);
      if (error instanceof ErrorResponse) {
        throw error;
      }
      throw createGeneralError(String(error), 'root', HttpStatus.BAD_REQUEST);
    }
  }
}
