import { CapabilityName, ErrorCodes, ForbiddenError, getMapByGetter, NotFoundError } from '@humand-packages/common';
import { Service } from 'typedi';
import { PortsDI } from '../../../../config/portsDI';
import { AttachmentsService } from '../../../attachments/business/services/attachmentsService';
import { Instance } from '../../../instances/business/models/instance';
import { OrganizationChartsService } from '../../../organizationCharts/business/services/organizationChartsService';
import { PermissionsService } from '../../../permissions/business/services/permissionsService';
import { SiteService } from '../../../regions/business/services/sitesService';
import { User } from '../../../users/business/models/user';
import { UsersService } from '../../../users/business/services/usersService';
import {
    TimeTrackingDaySummaryDTO,
    TimeTrackingDaySummaryScheduleDTO,
    TimeTrackingDaySummaryUserDTO,
} from '../../presentation/dtos/timeTrackingDaySummaryDTO';
import { mapDaySummaryToDetail } from '../../presentation/mappers/mapDaySummaryToDetail';
import { hasLocationIncidence } from '../helpers/siteHelper';
import { mapShiftAssociations } from '../mappers/shiftManagement/mapShiftCalendar';
import { TimeTrackingDaySummary } from '../models/daySummary';
import WorkScheduleServicePort from '../ports/workScheduleServicePort';
import { DaySummaryScheduleSource } from '../types/scheduleSource';
import ShiftCalendarService from './shiftManagement/shiftCalendar';
import TimeTrackingService from './timeTracking';
import { TimeTrackingCategorizedHourService } from './timeTracking/timeTrackingCategorizedHourService';

@Service()
export class DaySummaryDetailService {
    constructor(
        @PortsDI(TimeTrackingService) private readonly timeTrackingService: TimeTrackingService,
        @PortsDI(WorkScheduleServicePort) private readonly workScheduleService: WorkScheduleServicePort,
        private readonly categorizedHourService: TimeTrackingCategorizedHourService,
        private readonly shiftCalendarService: ShiftCalendarService,
        private readonly sitesService: SiteService,
        private readonly usersService: UsersService,
        private readonly attachmentsService: AttachmentsService,
        private readonly permissionsService: PermissionsService,
        private readonly organizationService: OrganizationChartsService,
    ) {}

    public getDaySummaryDetail = async (user: User, instance: Instance, targetUserId: number, date: string): Promise<TimeTrackingDaySummaryDTO> => {
        await this.validateUserCanPerformOverUser(user, instance, targetUserId);

        const daySummaries = await this.timeTrackingService.getOrCreateDaySummariesByUser(targetUserId, instance, date, date, true);

        if (daySummaries.length === 0) {
            throw new NotFoundError('Day summary not found', ErrorCodes.TIME_TRACKING_DAY_SUMMARY_NOT_FOUND);
        }

        const daySummary = daySummaries[0];

        const [categorizedHours, siteByIds, targetUser, schedule] = await Promise.all([
            this.loadCategorizedHours(instance.getId(), targetUserId, daySummary.dateString),
            this.loadSiteMap(instance.getId(), daySummary),
            this.loadUserWithSignedPhoto(instance.getId(), targetUserId),
            this.loadScheduleData(instance.getId(), daySummary),
        ]);

        daySummary.timeTrackingEntries?.forEach((entry) => {
            entry.locationIncidence = hasLocationIncidence(entry, siteByIds);
        });

        return mapDaySummaryToDetail(daySummary, {
            categorizedHours,
            user: targetUser,
            schedule,
            siteByIds,
            timezone: instance.getTimezone(),
        });
    };

    private validateUserCanPerformOverUser = async (user: User, instance: Instance, targetUserId: number): Promise<void> => {
        if (user.getId() === targetUserId) return;

        const hasManageCapability = this.permissionsService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING]);
        if (hasManageCapability) return;

        const subordinateIds = await this.organizationService.getSubordinateIdsRecursive(user.getId(), instance.getId());
        if (!subordinateIds.includes(targetUserId)) {
            throw new ForbiddenError("You do not have access to this user's data", ErrorCodes.ACCESS_DENIED);
        }
    };

    private loadUserWithSignedPhoto = async (instanceId: number, userId: number): Promise<TimeTrackingDaySummaryUserDTO> => {
        const targetUser = await this.usersService.findById(userId);
        await this.attachmentsService.addSignedUrlToModelWithImages(instanceId, [targetUser], ['profilePicture']);
        return {
            id: targetUser.id,
            firstName: targetUser.firstName,
            lastName: targetUser.lastName,
            profilePicture: targetUser.profilePicture,
        };
    };

    private loadCategorizedHours = async (instanceId: number, userId: number, dateString: string) => {
        return this.categorizedHourService.getApprovedCategorizedHoursByUserAndDates(instanceId, [userId], [dateString]);
    };

    private loadScheduleData = async (instanceId: number, daySummary: TimeTrackingDaySummary): Promise<TimeTrackingDaySummaryScheduleDTO> => {
        if (daySummary.scheduleSource === DaySummaryScheduleSource.WORK_SCHEDULE && daySummary.workScheduleId) {
            const { rows } = await this.workScheduleService.listWorkSchedules(
                { ids: [daySummary.workScheduleId], includeDeleted: true, instanceId },
                { limit: 1, offset: 0 },
            );
            const ws = rows[0];
            return {
                source: daySummary.scheduleSource,
                workSchedule: ws ? { id: ws.id, name: ws.name, description: ws.description ?? null } : null,
                shiftAssociations: null,
            };
        }

        if (daySummary.scheduleSource === DaySummaryScheduleSource.SHIFT && daySummary.shiftAssignedDayId) {
            const shiftAssignedDays = await this.shiftCalendarService.getShiftAssignedDaysByIds([daySummary.shiftAssignedDayId]);
            const shiftAssignedDay = shiftAssignedDays[0];
            return {
                source: daySummary.scheduleSource,
                workSchedule: null,
                shiftAssociations: shiftAssignedDay ? mapShiftAssociations(shiftAssignedDay) : null,
            };
        }

        return { source: daySummary.scheduleSource, workSchedule: null, shiftAssociations: null };
    };

    private loadSiteMap = async (instanceId: number, daySummary: TimeTrackingDaySummary) => {
        const siteIds = new Set<number>();
        daySummary.timeTrackingEntries?.forEach((entry) => {
            if (entry.siteId) siteIds.add(entry.siteId);
        });
        const sites = siteIds.size ? await this.sitesService.getByIds(Array.from(siteIds), instanceId, true) : [];
        return getMapByGetter(sites, (site) => site.getId());
    };
}
