import {
    BadRequestError,
    BaseController,
    ENV_OPTIONAL_NUMBER_KEYS_MAPPING,
    ErrorCodes,
    ISO_DATE,
    LoggerPort,
    NotFoundError,
    RequestUnprocessableEntityError,
} from '@humand-packages/common';
import { NextFunction, Request } from 'express';
import { CREATED, NO_CONTENT, OK } from 'http-status-codes';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import { PortsDI } from '../../../../config/portsDI';
import { getSequelize } from '../../../../config/sequelize';
import { FeatureFlagCode } from '../../../featureFlags/business/enums/featureFlagCode';
import { FeatureFlagService } from '../../../featureFlags/business/services/featureFlagService';
import { Instance } from '../../../instances/business/models/instance';
import { PermissionsService } from '../../../permissions/business/services/permissionsService';
import { RegionsService } from '../../../regions/business/services/regionService';
import { UsersService } from '../../../users/business/services/usersService';
import { UserDAO } from '../../../users/infrastructure/dataAccessObjects/userDAO';
import { userDAOtoUser } from '../../../users/infrastructure/mappers/usersMapper';
import { TimeTrackingEntry, TimeTrackingEntryPrototype } from '../../business/models/timeTrackingEntry';
import { PublicTimeTrackingServiceAdapter } from '../../business/services/publicTimeTracking';
import TimeTrackingService from '../../business/services/timeTracking';
import { TimeTrackingIncidenceNotificationService } from '../../business/services/timeTracking/timeTrackingIncidenceNotificationService';
import { TimeTrackingEntryType } from '../../business/types/entry';
import { isSourceAutoClosed, TimeTrackingEntrySource } from '../../business/types/source';
import { TimeTrackingDaySummaryApiSC } from '../serializationClasses/TimeTrackingDaySummarySC';
import { TimeTrackingEntryApiSC } from '../serializationClasses/TimeTrackingEntryApiSC';
import { TimeTrackingResponse } from '../types/response';
import { CreateTimeTrackingEntryAPIRequestsVC } from '../validationClasses/createTimeTrackingEntryAPIRequestVC';
import { CreateTimeTrackingEntryRequestsVC } from '../validationClasses/createTimeTrackingEntryRequestsVC';
import { GetTimeTrackingDaySummaryAPIRequestsVC } from '../validationClasses/getTimeTrackingDaySummaryAPIRequestVC';
import { GetTimeTrackingEntryAPIRequestsVC } from '../validationClasses/getTimeTrackingEntryAPIRequestVC';

export const DEFAULT_LIMIT = 20; // 10 pairs
export const DEFAULT_OFFSET = 0;
export const DEFAULT_BATCH_ENTRIES_LIMIT = 10000;
export const DATE_FORMAT = 'DD/MM/YYYY';
export const DEFAULT_AUTO_CLOSE_DAYS = 3;
export const DEFAULT_DAYS_LIMIT = 31;
const DUPLICATE_TYPE_ERROR = "Can't create two entries of the same type in a row";
@Service()
export class PublicTimeTrackingController extends BaseController {
    constructor(
        @PortsDI(TimeTrackingService) private readonly timeTrackingService: TimeTrackingService,
        private readonly publicTimeTrackingService: PublicTimeTrackingServiceAdapter,
        private readonly permissionService: PermissionsService,
        private readonly usersService: UsersService,
        protected readonly regionsService: RegionsService,
        private readonly featureFlagService: FeatureFlagService,
        @Inject(ENV_OPTIONAL_NUMBER_KEYS_MAPPING.NOTIFICATION_DELAY) notificationDelay: number,
        logger: LoggerPort,
        private readonly incidenceNotificationService: TimeTrackingIncidenceNotificationService,
    ) {
        super(logger, notificationDelay);
    }

    public clockIn = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        await this.createEntryApi(res, next, TimeTrackingEntryType.START);
    };

    public clockOut = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        await this.createEntryApi(res, next, TimeTrackingEntryType.END);
    };

    private async createEntryApi(res: TimeTrackingResponse, next: NextFunction, type: TimeTrackingEntryType) {
        const { bodyParams, loggedUser, instanceOfLoggedUser } = res.locals;
        const params = bodyParams as CreateTimeTrackingEntryAPIRequestsVC;
        if (typeof params.now === 'string') params.now = new Date(params.now);
        await this.endpointHandlerWithGenerics(
            next,
            async () => {
                this.permissionService.validateViewTimeTracking(userDAOtoUser(loggedUser));
                if (moment().isBefore(params.now)) {
                    throw new RequestUnprocessableEntityError('Cannot create future entries', ErrorCodes.TIME_TRACKING_ENTRY_FUTURE);
                }
                const entryUser = await this.usersService.findByEmployeeInternalId(loggedUser.instanceId, params.employeeId, true);
                if (!entryUser) {
                    throw new NotFoundError('Employee not found', ErrorCodes.USER_NOT_FOUND);
                } else if (entryUser.id !== loggedUser.id) {
                    this.permissionService.validateManageTimeTracking(userDAOtoUser(loggedUser));
                }

                const hasDesertEagleEnabled = await this.getTimeTrackingDesertEagleEnabledFeatureFlag(loggedUser.instanceId);

                // gRPC call runs before the DB transaction intentionally: createEntryTransactional opens an
                // independent inner transaction (parentTransaction: false), so running it inside an outer
                // transaction causes stale reads (e.g. missing deletes from preventDuplicateEntry).
                // If the DB transaction fails afterwards, the Desert Eagle entry is NOT orphaned — Desert
                // Eagle reconciles its state and will integrate the entry on its own.
                //
                // Skip the gRPC dual-write when the user already has entries on a day after this one:
                // Desert Eagle is a state machine keyed on the user's current entry and would reject a
                // backdated insert with TIME_TRACKING_ENTRY_DUPLICATE. Same trade-off as updateEntries.
                let desertEagleEntryId: string | undefined;
                if (hasDesertEagleEnabled) {
                    const tz = instanceOfLoggedUser.getTimezone();
                    const entryDateString = moment.tz(params.now, tz).format(ISO_DATE);
                    // hasEntriesAfterDate scans [nextDay_start, now]. If the entry's day is today
                    // in the instance TZ, nextDay_start > now and the range is empty — the query
                    // is guaranteed to return false. Skip it to save the DB round-trip in the
                    // common real-time case; only backdated inserts crossing a day need it.
                    const todayDateString = moment.tz(tz).format(ISO_DATE);
                    const userHasLaterEntries =
                        entryDateString !== todayDateString &&
                        (await this.timeTrackingService.hasEntriesAfterDate(entryUser.id, entryDateString, instanceOfLoggedUser));
                    if (userHasLaterEntries) {
                        this.logger.info(
                            { userId: entryUser.id, entryDateString, instanceId: instanceOfLoggedUser.getId() },
                            'Skipping Desert Eagle sync for public API create — user has entries after entry date',
                        );
                    } else {
                        desertEagleEntryId = await this.timeTrackingService.createDesertEagleEntry(loggedUser as UserDAO, {
                            comment: params.comment,
                            source: TimeTrackingEntrySource.INTEGRATION,
                            time: params.now,
                            type,
                            userId: entryUser.id,
                        });
                    }
                }

                const createdEntry = await getSequelize().transaction(async () => {
                    await this.preventDuplicateEntry(entryUser.id, params.now, instanceOfLoggedUser, type, loggedUser.id, desertEagleEntryId);
                    if (desertEagleEntryId) {
                        return this.timeTrackingService.createEntryTransactional(
                            loggedUser as UserDAO,
                            instanceOfLoggedUser,
                            { comment: params.comment, source: TimeTrackingEntrySource.INTEGRATION, time: params.now, type, userId: entryUser.id },
                            undefined,
                            true,
                            desertEagleEntryId,
                        );
                    }
                    return this.timeTrackingService.createEntry(
                        this.createTimeTrackingEntryModel(
                            loggedUser,
                            { comment: params.comment, source: TimeTrackingEntrySource.INTEGRATION, type },
                            params.now,
                            entryUser.id,
                        ),
                        instanceOfLoggedUser,
                        undefined,
                    );
                });
                res.status(CREATED).json(createdEntry);
                return { createdEntry, instance: instanceOfLoggedUser };
            },
            async ({ createdEntry, instance }) => {
                await this.incidenceNotificationService.evaluateEntry(createdEntry, instance);
            },
        );
    }

    private createTimeTrackingEntryModel(
        loggedUser: UserDAO,
        params: CreateTimeTrackingEntryRequestsVC,
        time: Date,
        userId: number,
    ): TimeTrackingEntryPrototype {
        return {
            comment: params.comment,
            createdBy: loggedUser.id,
            facialRecognitionSkipped: params.facialRecognitionSkipped ?? false,
            instanceId: loggedUser.instanceId,
            location: params.location,
            siteId: params.siteId,
            source: params.source ?? TimeTrackingEntrySource.APP,
            time: time,
            type: params.type,
            userId: userId,
        };
    }

    private async preventDuplicateEntry(
        userId: number,
        time: Date,
        instanceOfLoggedUser: Instance,
        type: TimeTrackingEntryType,
        deletedByUserId: number,
        desertEagleEntryId?: string,
    ) {
        const date = moment.tz(time, instanceOfLoggedUser.getTimezone()).format(ISO_DATE);
        const previousEntries = await this.timeTrackingService.listDayEntries(userId, date, instanceOfLoggedUser);
        if (!previousEntries?.length) return;

        // Race condition: request arrives → entry is created in Desert Eagle → DE's Kinesis consumer
        // wins the race and integrates the same entry back via gRPC (createEntryTransactional with
        // externalDesertEagleEntryId, which skips this guard by design). Without filtering, this guard
        // sees an entry of the same type and throws 422, unaware it is the one we just created.
        const filteredEntries = desertEagleEntryId
            ? previousEntries.filter((entry) => entry.desertEagleEntryId !== desertEagleEntryId)
            : previousEntries;
        if (!filteredEntries.length) return;

        type EntryWithMeta = { time: Date; type: TimeTrackingEntryType; isIncoming: boolean; entry?: TimeTrackingEntry };
        const entries: EntryWithMeta[] = [
            ...filteredEntries.map((entry) => ({ time: entry.time, type: entry.type, isIncoming: false, entry })),
            { time, type, isIncoming: true },
        ];
        entries.sort((a, b) => b.time.getTime() - a.time.getTime());

        const duplicateIndex = entries.findIndex(
            (entry, index) => index > 0 && entry.type === entries[index - 1].type && (entry.isIncoming || entries[index - 1].isIncoming),
        );
        if (duplicateIndex === -1) return;

        const current = entries[duplicateIndex];
        const previous = entries[duplicateIndex - 1];
        const existingEntry = (current.isIncoming ? previous.entry : current.entry) as TimeTrackingEntry | undefined;

        if (
            type === TimeTrackingEntryType.END &&
            existingEntry &&
            isSourceAutoClosed(existingEntry.source) &&
            time.getTime() < existingEntry.time.getTime()
        ) {
            await this.timeTrackingService.deleteEntryForPublicApi(existingEntry, deletedByUserId, instanceOfLoggedUser);
            return;
        }

        throw new RequestUnprocessableEntityError(DUPLICATE_TYPE_ERROR, ErrorCodes.TIME_TRACKING_ENTRY_DUPLICATE);
    }

    private getTimeTrackingDesertEagleEnabledFeatureFlag = async (instanceId: number): Promise<boolean> => {
        const featureFlag = await this.featureFlagService.getBooleanFeatureFlag(FeatureFlagCode.TIME_TRACKING_DESERT_EAGLE_ENABLED, instanceId);
        return featureFlag.value;
    };

    public listEntries = async (_req: Request, res: TimeTrackingResponse, next: NextFunction) => {
        const { loggedUser, instanceOfLoggedUser, requiredLimitOffsetPagination: limitOffsetPagination, queryParams } = res.locals;
        const params = queryParams as GetTimeTrackingEntryAPIRequestsVC;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateManageTimeTracking(user);

            const startDateMoment = moment(params.startDate, 'YYYY-MM-DD', true);
            const endDateMoment = moment(params.endDate, 'YYYY-MM-DD', true);

            if (endDateMoment.diff(startDateMoment, 'days') > DEFAULT_DAYS_LIMIT) {
                throw new BadRequestError(`Date range must not exceed ${DEFAULT_DAYS_LIMIT} days`, ErrorCodes.BAD_REQUEST);
            }

            const { rows, count } = await this.timeTrackingService.listEntriesApi(instanceOfLoggedUser, params, limitOffsetPagination);
            res.status(OK).json(
                new TimeTrackingEntryApiSC().serializePaginatedList({
                    count,
                    limit: limitOffsetPagination.limit!,
                    offset: limitOffsetPagination.offset!,
                    rows,
                }),
            );
        });
    };

    public deleteEntries = async (_req: Request, res: TimeTrackingResponse, next: NextFunction) => {
        const { loggedUser, instanceOfLoggedUser, queryParams } = res.locals;
        const params = queryParams as GetTimeTrackingEntryAPIRequestsVC;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateManageTimeTracking(user);

            const startDateMoment = moment(params.startDate, 'YYYY-MM-DD', true);
            const endDateMoment = moment(params.endDate, 'YYYY-MM-DD', true);

            if (startDateMoment.isAfter(endDateMoment)) {
                throw new BadRequestError('startDate must not be after endDate', ErrorCodes.INVALID_DATE_RANGE);
            }

            if (endDateMoment.diff(startDateMoment, 'days') > DEFAULT_DAYS_LIMIT) {
                throw new BadRequestError(`Date range must not exceed ${DEFAULT_DAYS_LIMIT} days`, ErrorCodes.BAD_REQUEST);
            }

            const entryUser = await this.usersService.findByEmployeeInternalId(loggedUser.instanceId, params.employeeId, true);
            if (!entryUser) {
                throw new NotFoundError('Employee not found', ErrorCodes.USER_NOT_FOUND);
            }

            await this.timeTrackingService.deleteEntriesInRangeForPublicApi(
                instanceOfLoggedUser,
                user,
                entryUser.id,
                params.startDate,
                params.endDate,
            );
            res.status(NO_CONTENT).end();
        });
    };

    public listDaySummaries = async (_req: Request, res: TimeTrackingResponse, next: NextFunction) => {
        const { loggedUser, instanceOfLoggedUser, requiredLimitOffsetPagination: limitOffsetPagination, queryParams } = res.locals;
        const params = queryParams as GetTimeTrackingDaySummaryAPIRequestsVC;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateManageTimeTracking(user);

            const startDateMoment = moment(params.startDate, 'YYYY-MM-DD', true);
            const endDateMoment = moment(params.endDate, 'YYYY-MM-DD', true);

            if (endDateMoment.diff(startDateMoment, 'days') > DEFAULT_DAYS_LIMIT) {
                throw new BadRequestError(`Date range must not exceed ${DEFAULT_DAYS_LIMIT} days`, ErrorCodes.BAD_REQUEST);
            }
            const { rows, count } = await this.publicTimeTrackingService.listDaySummaries(instanceOfLoggedUser, params, limitOffsetPagination);
            res.status(OK).json(
                new TimeTrackingDaySummaryApiSC().serializePaginatedList({
                    count,
                    limit: limitOffsetPagination.limit!,
                    offset: limitOffsetPagination.offset!,
                    rows,
                }),
            );
        });
    };
}
