import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Types } from 'mongoose';
import { ModelsService } from 'src/models/models.service';
import { CommonService } from 'src/common/common.service';
import { TranslationService } from 'src/common/services/translation.service';
import { EmailService } from 'src/common/services/email.service';
import { ActivityService } from 'src/common/services/activity.service';
import { STAFF_ROLES, UserType } from 'src/user/schema/users.schema';
import { CreateStaffDto, StaffListDto, StaffStatus, UpdateStaffDto } from './dto/staff.dto';

const INVITE_EXPIRY_HOURS = 48;

@Injectable()
export class StaffService {
    private readonly logger = new Logger(StaffService.name);

    constructor(
        private readonly models: ModelsService,
        private readonly common: CommonService,
        private readonly emailService: EmailService,
        private readonly activity: ActivityService,
        private readonly translationService: TranslationService,
        private readonly configService: ConfigService
    ) { }

    private t(key: string, lang: string, params?: Record<string, string>) {
        return this.translationService.translate(key, lang, params);
    }

    private fail(key: string, lang: string, status: HttpStatus): never {
        throw new HttpException({ message: this.t(key, lang) }, status);
    }

    /** Only the Master Admin can add, change or remove Sub-Admins (ADMIN_STAFF). */
    private assertCanManage(actor: any, targetType: UserType, lang: string) {
        if (targetType === UserType.ADMIN_STAFF && actor.user_type !== UserType.SUPER_ADMIN) {
            this.fail('CANNOT_MANAGE_SUB_ADMIN', lang, HttpStatus.FORBIDDEN);
        }
    }

    private async findStaff(id: string, lang: string, projection?: string) {
        const found = Types.ObjectId.isValid(id)
            ? await this.models.UserModel.findOne({ _id: new Types.ObjectId(id), user_type: { $in: STAFF_ROLES } }, projection)
            : null;
        if (!found) this.fail('STAFF_NOT_FOUND', lang, HttpStatus.NOT_FOUND);
        return found;
    }

    private statusOf(user: { is_active: boolean; is_email_verified: boolean }): StaffStatus {
        if (!user.is_active) return StaffStatus.INACTIVE;
        return user.is_email_verified ? StaffStatus.ACTIVE : StaffStatus.INVITED;
    }

    private toDto(user: any) {
        const { _id, name, email, phone_no, country_code, user_type, is_active, is_email_verified, profile_pic, created_at, updated_at } = user;
        return { _id, name, email, phone_no, country_code, user_type, status: this.statusOf(user), is_active, is_email_verified, profile_pic, created_at, updated_at };
    }

    private async issueInvite(user: any, actor: any, lang: string): Promise<boolean> {
        const token = this.common.generateSecureToken();
        await this.models.UserModel.updateOne(
            { _id: user._id },
            {
                $set: {
                    password_reset_token: this.common.hashToken(token),
                    password_reset_expires_at: +new Date() + INVITE_EXPIRY_HOURS * 60 * 60 * 1000
                }
            }
        );

        const baseUrl = (this.configService.get<string>('ADMIN_PANEL_URL') || '').replace(/\/$/, '');
        try {
            await this.emailService.sendStaffInviteEmail(
                user.email,
                user.name,
                {
                    inviteUrl: `${baseUrl}/accept-invite?token=${token}`,
                    inviterName: actor.name || 'Admin',
                    expiryHours: INVITE_EXPIRY_HOURS,
                    roleLabel: this.t(`ROLE_${user.user_type}`, user.language || lang)
                },
                user.language || lang
            );
            return true;
        } catch (error) {
            this.logger.error(`Failed to send staff invite to ${user.email}`, error.stack);
            return false;
        }
    }

    async create(dto: CreateStaffDto, actor: any, lang: string) {
        this.assertCanManage(actor, dto.user_type, lang);

        if (await this.models.UserModel.exists({ email: dto.email })) {
            this.fail('EMAIL_ALREADY_EXISTS', lang, HttpStatus.BAD_REQUEST);
        }

        const now = +new Date();
        const staff = await this.models.UserModel.create({
            name: dto.name,
            email: dto.email,
            phone_no: dto.phone_no,
            country_code: dto.country_code || '+91',
            user_type: dto.user_type,
            is_email_verified: false,
            is_active: true,
            created_at: now,
            updated_at: now
        });

        await this.activity.log(staff._id, 'STAFF_INVITED', actor, { role: dto.user_type });
        const emailSent = await this.issueInvite(staff, actor, lang);

        return this.common.successResponse(
            this.t(emailSent ? 'STAFF_INVITED' : 'STAFF_CREATED_EMAIL_FAILED', lang, { email: staff.email }),
            { ...this.toDto(staff), email_sent: emailSent }
        );
    }

    async list(query: StaffListDto, lang: string) {
        const { page, limit, search, user_type, status } = query;
        const filter: any = { user_type: user_type || { $in: STAFF_ROLES } };

        if (search) {
            const regex = { $regex: search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' };
            filter.$or = [{ name: regex }, { email: regex }, { phone_no: regex }];
        }
        if (status === StaffStatus.INACTIVE) filter.is_active = false;
        if (status === StaffStatus.ACTIVE) Object.assign(filter, { is_active: true, is_email_verified: true });
        if (status === StaffStatus.INVITED) Object.assign(filter, { is_active: true, is_email_verified: false });

        const [total, staff] = await Promise.all([
            this.models.UserModel.countDocuments(filter),
            this.models.UserModel.find(filter)
                .sort({ created_at: -1 })
                .skip((page - 1) * limit)
                .limit(limit)
                .lean()
        ]);

        return this.common.paginatedResponse(this.t('STAFF_LIST_FETCHED', lang), staff.map(s => this.toDto(s)), total, page, limit);
    }

    async getById(id: string, lang: string) {
        const staff: any = await this.findStaff(id, lang);
        const activity = await this.activity.timeline(staff._id, (key, params) => this.t(key, lang, params));

        return this.common.successResponse(this.t('STAFF_FETCHED', lang), { ...this.toDto(staff), activity });
    }

    async update(id: string, dto: UpdateStaffDto, actor: any, lang: string) {
        const staff: any = await this.findStaff(id, lang);
        const isSelf = staff._id.equals(actor._id);

        this.assertCanManage(actor, staff.user_type, lang);
        if (dto.user_type !== undefined && dto.user_type !== staff.user_type) {
            if (isSelf) this.fail('CANNOT_MODIFY_SELF', lang, HttpStatus.FORBIDDEN);
            this.assertCanManage(actor, dto.user_type, lang);
        }
        if (dto.is_active !== undefined && dto.is_active !== staff.is_active && isSelf) {
            this.fail('CANNOT_MODIFY_SELF', lang, HttpStatus.FORBIDDEN);
        }

        const set: any = { updated_at: +new Date() };
        const entries: { action: string; meta?: Record<string, any> }[] = [];

        if (dto.name !== undefined && dto.name !== staff.name) set.name = dto.name;
        if (dto.phone_no !== undefined && dto.phone_no !== staff.phone_no) set.phone_no = dto.phone_no;
        if (dto.country_code !== undefined && dto.country_code !== staff.country_code) set.country_code = dto.country_code;
        if (set.name || set.phone_no || set.country_code) entries.push({ action: 'STAFF_PROFILE_UPDATED' });

        if (dto.user_type !== undefined && dto.user_type !== staff.user_type) {
            set.user_type = dto.user_type;
            entries.push({ action: 'STAFF_ROLE_CHANGED', meta: { role: dto.user_type } });
        }
        if (dto.is_active !== undefined && dto.is_active !== staff.is_active) {
            set.is_active = dto.is_active;
            entries.push({ action: dto.is_active ? 'STAFF_ACTIVATED' : 'STAFF_DEACTIVATED' });
        }

        await this.models.UserModel.updateOne({ _id: staff._id }, { $set: set });
        await this.activity.logMany(staff._id, entries, actor);
        if (dto.is_active === false || set.user_type) {
            await this.models.SessionModel.deleteMany({ user_id: staff._id });
        }

        const updated = await this.models.UserModel.findById(staff._id).lean();
        return this.common.successResponse(this.t('STAFF_UPDATED', lang), this.toDto(updated));
    }

    async resendInvite(id: string, actor: any, lang: string) {
        const staff: any = await this.findStaff(id, lang);
        this.assertCanManage(actor, staff.user_type, lang);

        if (staff.is_email_verified) this.fail('INVITE_ALREADY_ACCEPTED', lang, HttpStatus.BAD_REQUEST);

        const emailSent = await this.issueInvite(staff, actor, lang);
        await this.activity.log(staff._id, 'STAFF_INVITE_RESENT', actor);

        return this.common.successResponse(
            this.t(emailSent ? 'STAFF_INVITE_RESENT' : 'STAFF_CREATED_EMAIL_FAILED', lang, { email: staff.email }),
            { email_sent: emailSent }
        );
    }

    async remove(id: string, actor: any, lang: string) {
        const staff: any = await this.findStaff(id, lang);

        if (staff._id.equals(actor._id)) this.fail('CANNOT_MODIFY_SELF', lang, HttpStatus.FORBIDDEN);
        this.assertCanManage(actor, staff.user_type, lang);

        await this.models.SessionModel.deleteMany({ user_id: staff._id });
        await this.models.UserModel.deleteOne({ _id: staff._id });

        return this.common.successResponse(this.t('STAFF_DELETED', lang));
    }
}
