import { HttpStatus, Injectable } from '@nestjs/common';
import { format } from 'date-fns/format';
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 {
  DatabaseService,
  ErrorResponse,
  logger,
  UploadService,
} from '../../config';
import { NotificationTypeEnum } from '../../config/database/entities';
import { createGeneralError, getUserTimzoneDate } from '../../utils';
import { NotificationsService } from '../notifications/notifications.service';
import {
  createNoticeDto,
  noticeFilterDto,
  noticeResponseSchema,
  noticesResponseSchema,
  updateNoticeAcknowledgmentDto,
  updateNoticeDto,
} from './notice.dto';

@Injectable()
export class NoticeService {
  constructor(
    private readonly db: DatabaseService,
    private readonly uploadService: UploadService,
    private readonly notificationService: NotificationsService,
    private readonly permissionResolver: PermissionResolverService,
  ) {}

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

  private buildPaginationDetails(page: number, limit: number, total: number) {
    const totalPages = Math.ceil(total / limit);

    return {
      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,
      },
    };
  }

  private async getNoticeSummary(
    owner_id: string,
    audience?: { memberId?: string; memberDepartmentId?: string },
    adminDepartmentId?: string,
  ) {
    const where: any = {
      owner_id,
    };

    if (audience?.memberId || audience?.memberDepartmentId) {
      const orConditions: any[] = [];

      if (audience.memberId) {
        orConditions.push({
          recipients: {
            some: {
              id: audience.memberId,
            },
          },
        });
      }

      if (audience.memberDepartmentId) {
        orConditions.push({
          departments: {
            some: {
              id: audience.memberDepartmentId,
            },
          },
        });
      }

      if (orConditions.length > 0) {
        where.OR = orConditions;
      }
    } else if (adminDepartmentId) {
      where.departments = {
        some: {
          id: adminDepartmentId,
        },
      };
    }

    const [
      total_notice,
      total_published,
      total_high_priority,
      total_acknowledge,
    ] = await Promise.all([
      this.db.notices.count({ where }),
      this.db.notices.count({
        where: {
          ...where,
          status: 'published',
        },
      }),
      this.db.notices.count({
        where: {
          ...where,
          priority: 'high',
        },
      }),
      this.db.notices.count({
        where: {
          ...where,
          required_acknowledgment: true,
        },
      }),
    ]);

    return {
      total_notice,
      total_published,
      total_high_priority,
      total_acknowledge,
    };
  }

  private includeRelations() {
    return {
      departments: true,
      recipients: true,
      up_acknowledgment: true,
      down_acknowledgment: true,
    };
  }

  private async ensureMemberIdsExist(memberIds: string[], owner_id: string) {
    if (memberIds.length === 0) {
      return;
    }

    const count = await this.db.members.count({
      where: {
        owner_id,
        id: {
          in: memberIds,
        },
      },
    });

    if (count !== memberIds.length) {
      throw createGeneralError(
        'One or more members are invalid',
        'member_ids',
        HttpStatus.BAD_REQUEST,
      );
    }
  }

  private resolveTargetType(data: {
    target_type?: 'department' | 'individual';
    department_ids?: string[];
    member_ids?: string[];
  }): 'department' | 'individual' {
    if (data.target_type) {
      return data.target_type;
    }

    if ((data.member_ids ?? []).length > 0) {
      return 'individual';
    }

    return 'department';
  }

  private buildAudienceFilter(audience?: {
    memberId?: string;
    memberDepartmentId?: string;
  }) {
    if (!audience?.memberId && !audience?.memberDepartmentId) {
      return {};
    }

    const orConditions: any[] = [];

    if (audience.memberId) {
      orConditions.push({
        recipients: {
          some: {
            id: audience.memberId,
          },
        },
      });
    }

    if (audience.memberDepartmentId) {
      orConditions.push({
        departments: {
          some: {
            id: audience.memberDepartmentId,
          },
        },
      });
    }

    return orConditions.length > 0 ? { OR: orConditions } : {};
  }

  private hasAudienceFilter(where: any) {
    if (where?.departments?.some?.id) {
      return true;
    }

    return (
      Array.isArray(where?.OR) &&
      where.OR.some(
        (clause: any) =>
          clause?.recipients?.some?.id || clause?.departments?.some?.id,
      )
    );
  }

  private async ensureDepartmentIdsExist(
    departmentIds: string[],
    owner_id: string,
  ) {
    if (departmentIds.length === 0) {
      return;
    }

    const count = await this.db.departments.count({
      where: {
        owner_id,
        id: {
          in: departmentIds,
        },
      },
    });

    if (count !== departmentIds.length) {
      throw createGeneralError(
        'One or more departments are invalid',
        'department_ids',
        HttpStatus.BAD_REQUEST,
      );
    }
  }

  private async moveNoticeDocument(document: any): Promise<any | undefined> {
    if (
      !document ||
      typeof document !== 'object' ||
      typeof document.file_name !== 'string' ||
      document.file_name.trim() === ''
    ) {
      return undefined;
    }

    const allowedMimeTypes = [
      'application/pdf',
      'application/msword',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      'application/vnd.ms-excel',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    ];

    const mimeType = String(document.mime_type ?? '').toLowerCase();
    const isImage = mimeType.startsWith('image/');
    const isAllowedDoc = allowedMimeTypes.includes(mimeType);

    if (!isImage && !isAllowedDoc) {
      throw createGeneralError(
        'Document must be pdf, doc, excel or image',
        'document',
        HttpStatus.BAD_REQUEST,
      );
    }

    const moved = await this.uploadService.moveImage(document, 'notices');
    return {
      file_name: moved.file_name,
      original_name: moved.original_name,
      file_size: moved.file_size,
      mime_type: moved.mime_type,
    };
  }

  private buildWhereFilter(
    query: noticeFilterDto,
    audience?: { memberId?: string; memberDepartmentId?: string },
  ) {
    const where: any = {};

    if (query.status) {
      where.status = query.status;
    }

    if (query.priority) {
      where.priority = query.priority;
    }

    if (typeof query.required_acknowledgment === 'boolean') {
      where.required_acknowledgment = query.required_acknowledgment;
    }

    if (query.search) {
      where.OR = [
        {
          title: {
            contains: query.search,
          },
        },
        {
          content: {
            contains: query.search,
          },
        },
      ];
    }
    if (query.from || query.to) {
      const fromDate = query.from ? new Date(query.from) : undefined;
      const toDate = query.to ? new Date(query.to) : undefined;

      const dateFilter: any = {};

      if (fromDate && toDate) {
        dateFilter.published_at = {
          gte: fromDate,
          lte: toDate,
        };
      } else if (fromDate) {
        dateFilter.published_at = {
          gte: fromDate,
        };
      } else if (toDate) {
        dateFilter.published_at = {
          lte: toDate,
        };
      }

      where.AND = [...(where.AND || []), dateFilter];
    }

    const audienceFilter = this.buildAudienceFilter(audience);
    if (Object.keys(audienceFilter).length > 0) {
      where.AND = [...(where.AND || []), audienceFilter];
    } else if (query.department_id) {
      where.departments = {
        some: {
          id: query.department_id,
        },
      };
    }

    return where;
  }

  private async getNoticesWithFullRelations(
    where: any,
    skip: number,
    take: number,
  ) {
    const hasAudienceFilterApplied = this.hasAudienceFilter(where);

    if (!hasAudienceFilterApplied) {
      return this.db.notices.findMany({
        where,
        include: this.includeRelations(),
        orderBy: {
          published_at: 'desc',
        },
        skip,
        take,
      });
    }

    // When department filter exists, fetch IDs first with pagination, then get all relations
    const pagedRows = await this.db.notices.findMany({
      where,
      orderBy: {
        published_at: 'desc',
      },
      skip,
      take,
    });

    const ids = pagedRows.map((notice: any) => notice.id).filter(Boolean);
    if (ids.length === 0) {
      return [];
    }

    // Fetch full relations without department filter constraint
    const rowsWithRelations = await this.db.notices.findMany({
      where: {
        id: {
          in: ids,
        },
      },
      include: this.includeRelations(),
    });

    const rowMap = new Map(
      rowsWithRelations.map((notice: any) => [notice.id, notice]),
    );

    return ids
      .map((id) => rowMap.get(id))
      .filter((notice): notice is any => Boolean(notice));
  }

  async createNotice(
    data: createNoticeDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'notice.access');
      assertScopedAction(permissions, 'notice.create', ctx.sub, ctx.sub);
      const targetType = this.resolveTargetType(data);
      const departmentIds = data.department_ids ?? [];
      const memberIds = data.member_ids ?? [];

      if (targetType === 'department') {
        await this.ensureDepartmentIdsExist(departmentIds, owner_id);
      } else {
        await this.ensureMemberIdsExist(memberIds, owner_id);
      }

      const document = await this.moveNoticeDocument(data.document);

      const notice = await this.db.notices.create({
        data: {
          title: data.title,
          ...(data.content ? { content: data.content } : {}),
          document: document ?? {},
          ...(data.status ? { status: data.status } : {}),
          ...(data.priority ? { priority: data.priority } : {}),
          ...(data.created_by ? { created_by: data.created_by } : {}),
          ...(data.published_at ? { published_at: data.published_at } : {}),
          ...(data.expire_at ? { expire_at: data.expire_at } : {}),
          ...(typeof data.required_acknowledgment === 'boolean'
            ? { required_acknowledgment: data.required_acknowledgment }
            : {}),
          ...(targetType === 'department' && departmentIds.length > 0
            ? {
                departments: {
                  connect: departmentIds.map((id) => ({ id })),
                },
              }
            : {}),
          ...(targetType === 'individual' && memberIds.length > 0
            ? {
                recipients: {
                  connect: memberIds.map((id) => ({ id })),
                },
              }
            : {}),
          owner: {
            connect: {
              id: owner_id,
            },
          },
        },
        include: this.includeRelations(),
      });

      const { data: parsedNotice } = noticeResponseSchema.safeParse({
        ...notice,
        target_type: targetType,
      });
      const message = data?.expire_at
        ? `A new notice has been published and will remain active from ${getUserTimzoneDate(data.published_at)} to ${getUserTimzoneDate(data.expire_at)}. Please review the notice for important updates and information.`
        : `A new notice has been published on ${getUserTimzoneDate(data.published_at)}. Please review the notice for important updates and information.`;
      this.notificationService.sendNotification({
        owner_id,
        title: notice.title,
        start_time: data?.published_at,
        end_time: data?.expire_at,
        message: message,
        type: NotificationTypeEnum.notice,
        reference_id: notice.id,
        include_owner: true,
        include_admins: true,
        ...(targetType === 'department'
          ? { department_ids: departmentIds }
          : { member_ids: memberIds }),
        meta: {
          notice_id: notice.id,
          priority: notice.priority,
        },
      });

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

  async getNotices(
    query: noticeFilterDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'notice.access');
      const scopeFilter = resolveDataScope(
        permissions,
        'notice.view',
        query.data_scope,
        ctx,
      );

      const page = query.page ?? 1;
      const limit = query.limit ?? 10;
      const skip = (page - 1) * limit;

      let audience: { memberId?: string; memberDepartmentId?: string } | undefined;
      if (scopeFilter === 'own') {
        const member = await this.db.members.findUnique({
          where: { id: ctx.sub, owner_id },
          select: { department_id: true },
        });

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

        audience = {
          memberId: ctx.sub,
          ...(member.department_id
            ? { memberDepartmentId: member.department_id }
            : {}),
        };
      }

      const where = {
        owner_id,
        ...this.buildWhereFilter(query, audience),
      };

      const [notices, total, summary] = await Promise.all([
        this.getNoticesWithFullRelations(where, skip, limit),
        this.db.notices.count({ where }),
        this.getNoticeSummary(owner_id, audience, query.department_id),
      ]);

      const { data: parsedNotices } = noticesResponseSchema.safeParse(
        notices?.map((notice: any) => ({
          ...notice,
          target_type:
            (notice.recipients?.length ?? 0) > 0 ? 'individual' : 'department',
        })),
      );

      return {
        data: parsedNotices,
        message: 'Notices retrieved successfully',
        status: HttpStatus.OK,
        details: {
          ...this.buildPaginationDetails(page, limit, total),
          summary,
        },
      };
    } catch (error) {
      logger.error(`Error in getNotices: ${String(error)}`);
      if (error instanceof ErrorResponse) {
        throw error;
      }
      throw createGeneralError(String(error), 'root', HttpStatus.BAD_REQUEST);
    }
  }

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

      const notice = await this.db.notices.findUnique({
        where: { id, owner_id },
        include: this.includeRelations(),
      });

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

      const { data: parsedNotice } = noticeResponseSchema.safeParse({
        ...notice,
        target_type:
          (notice.recipients?.length ?? 0) > 0 ? 'individual' : 'department',
      });

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

  async updateNotice(
    id: string,
    data: updateNoticeDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'notice.access');
      assertScope(permissions, 'notice.update');

      const existingNotice = await this.db.notices.findUnique({
        where: { id, owner_id },
        include: this.includeRelations(),
      });

      if (!existingNotice) {
        throw createGeneralError(
          'Notice not found',
          'notice',
          HttpStatus.NOT_FOUND,
        );
      }

      const isTargetingUpdate =
        data.target_type !== undefined ||
        data.department_ids !== undefined ||
        data.member_ids !== undefined;

      let targetType: 'department' | 'individual' | undefined;
      if (isTargetingUpdate) {
        targetType = this.resolveTargetType({
          target_type: data.target_type,
          department_ids: data.department_ids ?? [],
          member_ids: data.member_ids ?? [],
        });

        if (targetType === 'department') {
          await this.ensureDepartmentIdsExist(data.department_ids ?? [], owner_id);
        } else {
          await this.ensureMemberIdsExist(data.member_ids ?? [], owner_id);
        }
      }

      const nextDocument =
        data.document === null
          ? {}
          : data.document
            ? await this.moveNoticeDocument(data.document)
            : undefined;

      const updatedNotice = await this.db.notices.update({
        where: { id },
        data: {
          ...(data.title ? { title: data.title } : {}),
          ...(data.content !== undefined ? { content: data.content } : {}),
          ...(nextDocument !== undefined ? { document: nextDocument } : {}),
          ...(data.status ? { status: data.status } : {}),
          ...(data.priority ? { priority: data.priority } : {}),
          ...(data.created_by !== undefined
            ? { created_by: data.created_by }
            : {}),
          ...(data.published_at !== undefined
            ? { published_at: data.published_at }
            : {}),
          ...(data.expire_at !== undefined
            ? { expire_at: data.expire_at }
            : {}),
          ...(typeof data.required_acknowledgment === 'boolean'
            ? { required_acknowledgment: data.required_acknowledgment }
            : {}),
          ...(targetType === 'department'
            ? {
                departments: {
                  set: (data.department_ids ?? []).map((departmentId) => ({
                    id: departmentId,
                  })),
                },
                recipients: {
                  set: [],
                },
              }
            : {}),
          ...(targetType === 'individual'
            ? {
                recipients: {
                  set: (data.member_ids ?? []).map((memberId) => ({
                    id: memberId,
                  })),
                },
                departments: {
                  set: [],
                },
              }
            : {}),
        },
        include: this.includeRelations(),
      });
      const message = data?.expire_at
        ? `The notice information has been updated and is valid from ${getUserTimzoneDate(data.published_at ?? existingNotice.published_at)} to ${getUserTimzoneDate(data.expire_at ?? existingNotice.expire_at)}. Please check the latest changes and updates.`
        : `The notice information has been updated on ${getUserTimzoneDate(data.published_at ?? existingNotice.published_at)}. Please check the latest changes and updates.`;

      const resolvedTargetType =
        targetType ??
        ((updatedNotice.recipients?.length ?? 0) > 0
          ? 'individual'
          : 'department');
      const notificationDepartmentIds =
        resolvedTargetType === 'department'
          ? (data.department_ids ??
            updatedNotice.departments?.map((dept: any) => dept.id) ??
            [])
          : undefined;
      const notificationMemberIds =
        resolvedTargetType === 'individual'
          ? (data.member_ids ??
            updatedNotice.recipients?.map((member: any) => member.id) ??
            [])
          : undefined;

      this.notificationService.sendNotification({
        owner_id,
        title: updatedNotice.title,
        start_time: data?.published_at ?? existingNotice.published_at,
        end_time: data?.expire_at ?? existingNotice.expire_at,
        message: message,
        type: NotificationTypeEnum.notice,
        reference_id: updatedNotice.id,
        include_owner: true,
        include_admins: true,
        ...(notificationDepartmentIds
          ? { department_ids: notificationDepartmentIds }
          : {}),
        ...(notificationMemberIds ? { member_ids: notificationMemberIds } : {}),
        meta: {
          notice_id: updatedNotice.id,
          priority: updatedNotice.priority,
        },
      });

      const { data: parsedNotice } = noticeResponseSchema.safeParse({
        ...updatedNotice,
        target_type: resolvedTargetType,
      });

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

  async updateAcknowledgeNotice(
    id: string,
    data: updateNoticeAcknowledgmentDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'notice.access');
      assertScopedAction(
        permissions,
        'notice.view',
        data.member_id,
        ctx.sub,
      );

      const { member_id, acknowledgment } = data;
      const notice = await this.db.notices.findUnique({
        where: { id, owner_id },
        include: {
          up_acknowledgment: true,
          down_acknowledgment: true,
        },
      });

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

      const member = await this.db.members.findUnique({
        where: {
          id: member_id,
          owner_id,
        },
        select: {
          id: true,
        },
      });

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

      const alreadyAcknowledgedUp = (notice.up_acknowledgment ?? []).some(
        (member) => member.id === member_id,
      );
      const alreadyAcknowledgedDown = (notice.down_acknowledgment ?? []).some(
        (member) => member.id === member_id,
      );

      const nextUpAcknowledgment = [...(notice.up_acknowledgment ?? [])];
      const nextDownAcknowledgment = [...(notice.down_acknowledgment ?? [])];

      const removeMember = (members: any[]) =>
        members.filter((currentMember) => currentMember.id !== member_id);

      const addMember = (members: any[]) => {
        if (members.some((currentMember) => currentMember.id === member_id)) {
          return members;
        }

        return [...members, member];
      };

      if (acknowledgment === 'up') {
        if (alreadyAcknowledgedUp) {
          const updatedNotice = await this.db.notices.update({
            where: { id },
            data: {
              up_acknowledgment: removeMember(nextUpAcknowledgment),
              down_acknowledgment: removeMember(nextDownAcknowledgment),
            },
          });

          if (!updatedNotice) {
            throw createGeneralError(
              'Failed to update acknowledgment',
              'notice',
              HttpStatus.BAD_REQUEST,
            );
          }
        } else {
          await this.db.notices.update({
            where: { id },
            data: {
              up_acknowledgment: addMember(nextUpAcknowledgment),
              down_acknowledgment: removeMember(nextDownAcknowledgment),
            },
          });
        }
      } else if (acknowledgment === 'down') {
        if (alreadyAcknowledgedDown) {
          await this.db.notices.update({
            where: { id },
            data: {
              up_acknowledgment: removeMember(nextUpAcknowledgment),
              down_acknowledgment: removeMember(nextDownAcknowledgment),
            },
          });
        } else {
          await this.db.notices.update({
            where: {
              id,
            },
            data: {
              down_acknowledgment: addMember(nextDownAcknowledgment),
              up_acknowledgment: removeMember(nextUpAcknowledgment),
            },
          });
        }
      }

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

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

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

      if (!existingNotice) {
        throw createGeneralError(
          'Notice not found',
          'notice',
          HttpStatus.NOT_FOUND,
        );
      }

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

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