import {
    FACE_ASSIGNMENT_EVENT,
    HUMAND_CONFIG_DI_NAME,
    HumandConfig,
    LoggerPort,
    MessagingQueue,
    QueueMessageBody,
    QueueMessageTypes,
    QueuePort,
    SqsMessagingQueue,
    UserStatus,
} from '@humand-packages/common';
import { KafkaMessage } from 'kafkajs';
import { Knex } from 'knex';
import { DateTime } from 'luxon';
import { Inject, Service } from 'typedi';
import { v7 as uuidv7 } from 'uuid';
import { getConfig } from '../../../../apiConfig/configSetup';
import { PortsDI } from '../../../../config/portsDI';
import { CdcEntityOperations } from '../../../changeDataCapture/business/enums/entityOperations';
import { BaseCDCService } from '../../../changeDataCapture/business/services/baseCDCService';
import { UserMessage } from '../../../changeDataCapture/business/types/cdcUserMessage';
import { ParsedMessageOfSoftDeletedEntityMessage } from '../../../changeDataCapture/business/types/softDeleteEntity';
import { FeatureFlagCode } from '../../../featureFlags/business/enums/featureFlagCode';
import { FeatureFlagService } from '../../../featureFlags/business/services/featureFlagService';
import { TimeTrackingConfigRepositoryPort } from '../../infrastructure/ports/timeTracking/timeTrackingConfigRepositoryPort';
import { TimeTrackingPolicy } from '../models/timeTrackingPolicy';

const BACKGROUND_LOGGED_USER_ID = 0;

const LUXON_ISO_DATE = 'yyyy-MM-dd';

interface KioskUserForCreation {
    createdAt: Date;
    id: string;
    instanceId: number;
    regionId: number | null;
    siteId: number | null;
    updatedAt: Date;
    userId: number;
}

interface UserPolicyKioskConfig {
    clockingInKioskEnabled: boolean;
}

interface SiteUserWithRegion {
    siteId: number;
    regionId: number;
}

const validUserStatusSet = new Set([UserStatus.ACTIVE, UserStatus.UNCLAIMED]);

@Service()
export class TimeTrackingCDCService extends BaseCDCService {
    constructor(
        @Inject(HUMAND_CONFIG_DI_NAME) config: HumandConfig,
        logger: LoggerPort,
        @PortsDI(TimeTrackingConfigRepositoryPort) private readonly timeTrackingConfigService: TimeTrackingConfigRepositoryPort,
        private readonly queuePort: QueuePort,
        private readonly featureFlagService: FeatureFlagService,
    ) {
        super(config, logger, 'TimeTrackingCDCService', config.kafkaTimeTrackingGroupId, true);
        this.faceAssignmentQueue = new SqsMessagingQueue(getConfig().awsSqsTimeTrackingFaceAssignmentsQueueUrl!, false);
    }

    private readonly faceAssignmentQueue: MessagingQueue;

    private readonly deadLetterTopic = `${this.config.kafkaTimeTrackingGroupId}.dlt`;

    private enqueueFaceAssignmentSqs = async (
        messageType: QueueMessageTypes.TIME_TRACKING_FACE_ASSIGNMENT,
        body: QueueMessageBody[QueueMessageTypes.TIME_TRACKING_FACE_ASSIGNMENT],
    ): Promise<void> => {
        try {
            await this.queuePort.sendMessage(this.faceAssignmentQueue, messageType, body);
        } catch (err) {
            this.logger.error({ err }, '[time-tracking-cdc] face-assignment SQS enqueue failed; reconcile cron will heal');
        }
    };

    protected getTopicsToSubscribe(): string[] {
        return [this.config.kafkaUsersTopicName];
    }

    protected async handleMessage(topic: string, message: KafkaMessage): Promise<void> {
        try {
            this.logger.debug(`${this.serviceName} processing message of topic: ${topic}`);
            if (topic === this.config.kafkaUsersTopicName) {
                const parsedMessage = this.parseMessageOfSoftDeleteEntity<UserMessage>(message);
                if (!parsedMessage) {
                    this.logger.warn({ topic }, 'Message could not be parsed or something went wrong');
                    await this.sendMessageToDeadLetterTopic(message, topic, this.deadLetterTopic);
                    return;
                }
                await this.handleUserMessage(parsedMessage).catch(this.handleMessageError(topic, message));
            }
        } catch (error) {
            this.logger.error({ error, topic }, `Unknown error processing message`);
        }
    }

    private handleMessageError =
        (topic: string, message: KafkaMessage) =>
        async (error: Error): Promise<void> => {
            this.logger.error({ error, topic }, 'Error processing message, sending to dead letter topic');
            await this.sendMessageToDeadLetterTopic(message, topic, this.deadLetterTopic);
        };

    private async handleUserMessage(parsedMessage: ParsedMessageOfSoftDeletedEntityMessage<UserMessage>): Promise<void> {
        const { operation, afterEntity, beforeEntity } = parsedMessage!;
        switch (operation) {
            case CdcEntityOperations.CREATE:
                await this.handleCreateUser(afterEntity!);
                break;
            case CdcEntityOperations.UPDATE:
                await this.handleUpdateUser(afterEntity!, beforeEntity!);
                break;
            case CdcEntityOperations.DELETE:
                await this.handleUserDeletion(beforeEntity!, afterEntity?.deletedAt);
                break;
            case CdcEntityOperations.READ:
                if (!afterEntity?.deletedAt) await this.handleCreateUser(afterEntity!);
                break;
            default:
        }
    }

    private async handleUpdateUser(afterUser: UserMessage, beforeUser: UserMessage): Promise<void> {
        const isStatusChange = beforeUser.status !== afterUser.status;
        const updatedAt = new Date(afterUser.updatedAt ?? Date.now());
        if (isStatusChange) {
            const isStatusActive = validUserStatusSet.has(afterUser.status);
            if (isStatusActive) {
                await this.createKioskUsersForUser(afterUser.instanceId, afterUser.id);
            } else {
                await this.knexClient('TimeTrackingKioskUsers')
                    .update({ deletedAt: updatedAt, updatedAt: updatedAt })
                    .where('userId', afterUser.id)
                    .where('instanceId', afterUser.instanceId)
                    .whereNull('deletedAt');
            }
        } else {
            await this.knexClient('TimeTrackingKioskUsers')
                .update({ updatedAt: updatedAt })
                .where('userId', afterUser.id)
                .where('instanceId', afterUser.instanceId)
                .whereNull('deletedAt');
        }
    }

    private async getKioskUsersToCreate(
        instanceId: number,
        userId: number,
        isNewUser: boolean = false,
    ): Promise<{ base: KioskUserForCreation; withSite: KioskUserForCreation[] } | null> {
        let siteUsers: Array<{ siteId: number; regionId: number }> = [];

        if (isNewUser) {
            // For new users, use ALL_USERS sites directly (SiteUserRegion doesn't exist yet due to race condition with RegionsCDCService)
            siteUsers = await this.getAllUsersSites(instanceId);
        } else {
            // For existing users (status change), SiteUserRegion already exists
            siteUsers = await this.knexClient('SiteUserRegion')
                .select('SiteUserRegion.siteId', 'Sites.regionId')
                .join('Sites', 'SiteUserRegion.siteId', 'Sites.id')
                .where('SiteUserRegion.userId', userId)
                .whereNull('SiteUserRegion.deletedAt')
                .whereNull('Sites.deletedAt');
        }

        const now = new Date();

        const base: KioskUserForCreation = {
            createdAt: now,
            id: uuidv7(),
            instanceId,
            regionId: null,
            siteId: null,
            updatedAt: now,
            userId,
        };

        const withSite: KioskUserForCreation[] = siteUsers.map((siteUser) => ({
            createdAt: now,
            id: uuidv7(),
            instanceId,
            regionId: siteUser.regionId,
            siteId: siteUser.siteId,
            updatedAt: now,
            userId,
        }));

        return { base, withSite };
    }

    private async getAllUsersSites(instanceId: number): Promise<SiteUserWithRegion[]> {
        // Get sites with ALL_USERS mode
        const region = await this.knexClient('Regions')
            .select('id')
            .where({ assignedAllUsers: true, deletedAt: null, instanceId })
            .orderBy('updatedAt', 'desc')
            .first()
            .returning<{ id: number }>('id');

        if (!region) return [];

        const sites = await this.knexClient('Sites')
            .select<SiteUserWithRegion[]>('id as siteId', 'regionId')
            .where({ assignedAllUsers: true, deletedAt: null, instanceId, regionId: region.id })
            .orderBy('updatedAt', 'desc');

        return sites;
    }

    private async insertKioskUsers(
        trx: Knex.Transaction,
        kioskUsers: { base: KioskUserForCreation; withSite: KioskUserForCreation[] },
    ): Promise<void> {
        const now = new Date();
        // Insert base record (no site) - unique index on userId WHERE siteId IS NULL
        await trx('TimeTrackingKioskUsers')
            .insert(kioskUsers.base)
            .onConflict(trx.raw('("userId") WHERE ("siteId" IS NULL)'))
            .merge({ deletedAt: null, updatedAt: now });
        // Insert site-specific records - unique index on (userId, siteId) WHERE siteId IS NOT NULL
        if (kioskUsers.withSite.length > 0) {
            await trx('TimeTrackingKioskUsers')
                .insert(kioskUsers.withSite)
                .onConflict(trx.raw('("userId", "siteId") WHERE ("siteId" IS NOT NULL)'))
                .merge({ deletedAt: null, updatedAt: now });
        }
    }

    private async createKioskUsersForUser(instanceId: number, userId: number): Promise<void> {
        const policy = await this.getUserPolicy(instanceId, userId);
        if (!policy || !policy.clockingInKioskEnabled) return;
        const kioskUsers = await this.getKioskUsersToCreate(instanceId, userId);
        if (!kioskUsers) return;
        await this.knexClient.transaction(async (trx) => {
            await this.insertKioskUsers(trx, kioskUsers);
        });
    }

    private async getUserPolicy(instanceId: number, userId: number): Promise<UserPolicyKioskConfig | null> {
        const policy = await this.knexClient('TimeTrackingPoliciesUsers')
            .select<UserPolicyKioskConfig>('TimeTrackingPolicies.clockingInKioskEnabled')
            .join('TimeTrackingPolicies', 'TimeTrackingPoliciesUsers.policyId', 'TimeTrackingPolicies.id')
            .where({
                'TimeTrackingPolicies.clockingInKioskEnabled': true,
                'TimeTrackingPolicies.deleted_at': null,
                'TimeTrackingPolicies.instanceId': instanceId,
                'TimeTrackingPoliciesUsers.userId': userId,
            })
            .whereNull('TimeTrackingPoliciesUsers.deleted_at')
            .whereNull('TimeTrackingPoliciesUsers.toDate')
            .first();

        return policy ?? null;
    }

    private async handleCreateUser(afterUser: UserMessage): Promise<void> {
        const instanceId = afterUser.instanceId;

        const policy = await this.knexClient('TimeTrackingPolicies')
            .select(
                'id',
                'allUsers',
                'autoCloseLimitHours',
                'autoCloseMode',
                'clockingInAppEnabled',
                'clockingInKioskEnabled',
                'clockingInWebEnabled',
                'description',
                'faceRecognitionBlocking',
                'faceRecognitionEnabled',
                'instanceId',
                'latenessTolerance',
                'name',
                'nightShift',
                'notificationsEnabled',
            )
            .where({ allUsers: true, deleted_at: null, instanceId })
            .orderBy('updatedAt', 'desc')
            .first()
            .returning<TimeTrackingPolicy>('*');
        if (!policy) return;

        const instance = await this.knexClient('Instances')
            .select('timezone')
            .where({ deletedAt: null, id: instanceId })
            .first()
            .returning<{ timezone: string }>('timezone');

        const date = new Date();
        const currentDate = DateTime.now().setZone(instance.timezone).toFormat(LUXON_ISO_DATE);
        const policyUser = {
            createdAt: afterUser.createdAt ?? date,
            fromDate: currentDate,
            policyId: policy.id,
            updatedAt: afterUser.updatedAt ?? date,
            userId: afterUser.id,
        };

        const faceRecognitionConfiguration = {
            createdAt: afterUser.createdAt ?? date,
            enabled: !!policy.faceRecognitionEnabled,
            instanceId,
            isBlocking: !!policy.faceRecognitionBlocking,
            updatedAt: afterUser.updatedAt ?? date,
            userId: afterUser.id,
        };

        const isActive = validUserStatusSet.has(afterUser.status);

        const kioskUsers = isActive && policy.clockingInKioskEnabled ? await this.getKioskUsersToCreate(instanceId, afterUser.id, true) : null;

        await this.knexClient.transaction(async (trx) => {
            await trx('TimeTrackingPoliciesUsers')
                .insert(policyUser)
                .onConflict(trx.raw('("userId","policyId","fromDate") WHERE (deleted_at IS NULL) AND ("toDate" IS NULL)'))
                .merge(['createdAt', 'updatedAt']);
            await trx('FaceRecognitionConfigurations')
                .insert(faceRecognitionConfiguration)
                .onConflict(trx.raw('("instanceId","userId") WHERE ("deletedAt" IS NULL)'))
                .merge(['enabled', 'isBlocking']);
            if (kioskUsers) {
                await this.insertKioskUsers(trx, kioskUsers);
            }
        });

        const faceConfiguration = await this.knexClient('FaceRecognitionConfigurations')
            .select('referenceImage')
            .where('instanceId', instanceId)
            .where('userId', afterUser.id)
            .whereNull('deletedAt')
            .first()
            .returning<{ referenceImage: string }>('referenceImage');

        // Rekognition work (instance-collection enrollment + per-site indexing) is
        // delegated to the face-assignments SQS consumer. The consumer handles
        // rate-limiting, idempotent pending-work computation, and compensating
        // delete on persist failure. CDC service stays Rekognition-free.
        if (faceConfiguration?.referenceImage) {
            await this.enqueueFaceAssignmentSqs(QueueMessageTypes.TIME_TRACKING_FACE_ASSIGNMENT, {
                event: FACE_ASSIGNMENT_EVENT.CDC_TIME_TRACKING_USER_CREATED,
                instanceId,
                loggedUserId: BACKGROUND_LOGGED_USER_ID,
                userId: afterUser.id,
            });
        }

        const hasDesertEagleFF = await this.getTimeTrackingDesertEagleEnabledFeatureFlag(instanceId);
        if (hasDesertEagleFF) {
            const policyModel = new TimeTrackingPolicy({
                ...policy,
            });
            policyModel.id = policy.id;
            await this.timeTrackingConfigService.associatePolicy(instanceId, policyModel, [afterUser.id], []);
        }
    }

    private async handleUserDeletion(user: UserMessage, deletedAt?: string | null): Promise<void> {
        const deletedAtDate = new Date(deletedAt ?? Date.now());
        const instance = await this.knexClient('Instances')
            .select('timezone')
            .where('id', user.instanceId)
            .first()
            .returning<{ timezone: string }>('timezone');
        const currentDate = DateTime.now().setZone(instance.timezone).toFormat(LUXON_ISO_DATE);

        // Rekognition deletion (instance-collection + per-site) and the related
        // FaceRecognitionConfigurations.faceId nullify + TimeTrackingFaceAssignments
        // soft-delete are owned by the face-assignments SQS consumer. Local tx
        // below keeps only the non-face state (policies, settings, kiosk users).
        // Gate: only enqueue if there's a referenceImage — without one, there's
        // nothing in Rekognition (FACE_CONFIG_DELETED would have cleaned site
        // assignments + faceId on reset; never-enrolled users have nothing).
        const faceConfiguration = await this.knexClient('FaceRecognitionConfigurations')
            .select('referenceImage')
            .where('instanceId', user.instanceId)
            .where('userId', user.id)
            .whereNull('deletedAt')
            .first()
            .returning<{ referenceImage: string }>('referenceImage');

        if (faceConfiguration?.referenceImage) {
            await this.enqueueFaceAssignmentSqs(QueueMessageTypes.TIME_TRACKING_FACE_ASSIGNMENT, {
                event: FACE_ASSIGNMENT_EVENT.CDC_TIME_TRACKING_USER_DELETED,
                instanceId: user.instanceId,
                loggedUserId: BACKGROUND_LOGGED_USER_ID,
                userId: user.id,
            });
        }

        const userPolicy = await this.knexClient('TimeTrackingPoliciesUsers')
            .select(
                'TimeTrackingPolicies.id',
                'TimeTrackingPolicies.allUsers',
                'TimeTrackingPolicies.autoCloseLimitHours',
                'TimeTrackingPolicies.autoCloseMode',
                'TimeTrackingPolicies.clockingInAppEnabled',
                'TimeTrackingPolicies.clockingInKioskEnabled',
                'TimeTrackingPolicies.clockingInWebEnabled',
                'TimeTrackingPolicies.description',
                'TimeTrackingPolicies.faceRecognitionBlocking',
                'TimeTrackingPolicies.faceRecognitionEnabled',
                'TimeTrackingPolicies.instanceId',
                'TimeTrackingPolicies.latenessTolerance',
                'TimeTrackingPolicies.name',
                'TimeTrackingPolicies.nightShift',
                'TimeTrackingPolicies.notificationsEnabled',
            )
            .join('TimeTrackingPolicies', 'TimeTrackingPoliciesUsers.policyId', 'TimeTrackingPolicies.id')
            .where('TimeTrackingPoliciesUsers.userId', user.id)
            .whereNull('TimeTrackingPoliciesUsers.deleted_at')
            .whereNull('TimeTrackingPoliciesUsers.toDate')
            .whereNull('TimeTrackingPolicies.deleted_at')
            .first()
            .returning<TimeTrackingPolicy>('*');

        // FaceRecognitionConfigurations.faceId nullify and TimeTrackingFaceAssignments
        // soft-delete are NOT in this tx — the face-assignments SQS consumer owns
        // those writes (via setInstanceCollectionFaceId + per-site delete). This
        // tx is now strictly the non-face cleanup.
        await this.knexClient.transaction(async (trx) => {
            await trx('TimeTrackingPoliciesUsers').update({ toDate: currentDate }).where('userId', user.id);
            await trx('TimeTrackingUserSettings')
                .update({ deletedAt: deletedAtDate })
                .where('userId', user.id)
                .where('instanceId', user.instanceId)
                .whereNull('deletedAt');
            await trx('TimeTrackingKioskUsers')
                .update({ deletedAt: deletedAtDate, updatedAt: deletedAtDate })
                .where('userId', user.id)
                .where('instanceId', user.instanceId)
                .whereNull('deletedAt');
        });

        if (userPolicy) {
            const hasDesertEagleFF = await this.getTimeTrackingDesertEagleEnabledFeatureFlag(user.instanceId);
            if (hasDesertEagleFF) {
                await this.timeTrackingConfigService.associatePolicy(user.instanceId, userPolicy, [], [user.id]);
            }
        }
    }

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