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 { NotificationService } from 'src/notification/notification.service';
import { NotificationType } from 'src/notification/enums/notification-type.enum';
import { SmsService } from 'src/common/services/sms.service';
import { AgentDocumentStatus, AgentDocumentType, AgentStatus, UserType } from 'src/user/schema/users.schema';
import { OTPType } from 'src/user/schema/otp.schema';
import { AgentListDto, AgentStatusFilter, InviteAgentDto, RejectAgentDto, SendOtpDto, SubmitKycDto, UpdateAgentDto, VerifyOtpDto } from './dto/agent.dto';

const FIRST_AGENT_NO = 8041;
const PENDING_STATUSES = [AgentStatus.INVITED, AgentStatus.ACCEPTED, AgentStatus.SUBMITTED];
const OTP_EXPIRY_MS = 5 * 60 * 1000;
const MOCK_OTP = '1234';
const REQUIRED_DOCUMENTS = Object.values(AgentDocumentType);
const PAN_FORMAT = /^[A-Z]{5}[0-9]{4}[A-Z]$/;

// What the app should show after login: the submission form, the "under review" screen (which also shows a rejection note and the resubmit option), or the dashboard.
export enum AgentScreen {
    DOCUMENT_SUBMISSION = 'DOCUMENT_SUBMISSION',
    DOCUMENT_REVIEW = 'DOCUMENT_REVIEW',
    DASHBOARD = 'DASHBOARD'
}

export enum AgentReviewStatus {
    DOCUMENTS_REQUIRED = 'DOCUMENTS_REQUIRED',
    UNDER_REVIEW = 'UNDER_REVIEW',
    REJECTED = 'REJECTED',
    APPROVED = 'APPROVED'
}

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

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

    // ---------- helpers ----------

    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 = HttpStatus.BAD_REQUEST): never {
        throw new HttpException({ message: this.t(key, lang) }, status);
    }

    private agentFilter(extra: Record<string, any> = {}) {
        return { user_type: UserType.AGENT, ...extra };
    }

    private async findAgent(id: string, lang: string, projection?: string) {
        const agent = Types.ObjectId.isValid(id)
            ? await this.models.UserModel.findOne(this.agentFilter({ _id: new Types.ObjectId(id) }), projection)
            : null;
        if (!agent) this.fail('AGENT_NOT_FOUND', lang, HttpStatus.NOT_FOUND);
        return agent as any;
    }

    private async assertContactFree(phone_no: string, country_code: string, email: string | undefined, lang: string, excludeId?: Types.ObjectId) {
        const notSelf = excludeId ? { _id: { $ne: excludeId } } : {};
        if (await this.models.UserModel.exists(this.agentFilter({ phone_no, country_code, ...notSelf }))) {
            this.fail('AGENT_PHONE_EXISTS', lang);
        }
        if (email && await this.models.UserModel.exists({ email, ...notSelf })) {
            this.fail('EMAIL_ALREADY_EXISTS', lang);
        }
    }

    /** Every id must be an existing category; ids not already assigned must also be active. */
    private async resolveCategoryIds(ids: string[], lang: string, current: Types.ObjectId[] = []) {
        const objectIds = ids.map(id => new Types.ObjectId(id));
        const currentSet = new Set(current.map(id => id.toString()));
        const categories = await this.models.CategoryModel.find({ _id: { $in: objectIds } }).lean();
        const valid = categories.length === objectIds.length &&
            categories.every(c => c.is_active || currentSet.has(c._id.toString()));
        if (!valid) this.fail('INVALID_CATEGORIES', lang);
        return objectIds;
    }

    private async activeRegionId(id: string, lang: string) {
        const region = await this.models.RegionModel.findOne({ _id: new Types.ObjectId(id), is_active: true }).select('_id').lean();
        if (!region) this.fail('INVALID_REGION', lang);
        return region!._id as Types.ObjectId;
    }

    private async nextAgentNo(): Promise<number> {
        const last: any = await this.models.UserModel
            .findOne({ 'agent_no': { $exists: true } })
            .sort({ 'agent_no': -1 })
            .select('agent_no')
            .lean();
        return (last?.agent_no ?? FIRST_AGENT_NO - 1) + 1;
    }

    private categoriesOf(agent: any) {
        return (agent.category_ids || [])
            .filter((c: any) => c?.name)
            .map((c: any) => ({ _id: c._id, name: c.name, is_active: c.is_active }));
    }

    private toListDto(user: any) {
        const a = user;
        return {
            _id: user._id,
            agent_code: a.agent_code,
            name: user.name,
            phone_no: user.phone_no,
            country_code: user.country_code,
            email: user.email,
            region: a.region_id?.name ? { _id: a.region_id._id, name: a.region_id.name, is_active: a.region_id.is_active } : null,
            categories: this.categoriesOf(user),
            status: a.agent_status,
            is_active: user.is_active,
            invited_at: a.invited_at,
            submitted_at: a.kyc_submitted_at,
            created_at: user.created_at
        };
    }

    private appBaseUrl() {
        return (this.configService.get<string>('AGENT_APP_URL') || '').replace(/\/$/, '');
    }

    /** Emails the login link. There is no SMS invite. The agent then logs in with their mobile number and an OTP. */
    private async sendInvite(user: any, actor: any, lang: string) {
        const url = `${this.appBaseUrl()}/login?phone=${encodeURIComponent(`${user.country_code || '+91'}${user.phone_no}`)}`;

        console.log('url', url)
        // let emailSent = false;
        // try {
        //     emailSent = await this.emailService.sendBrokerInviteEmail(
        //         user.email, user.name,
        //         { inviteUrl: url, inviterName: actor.name || 'Admin', expiryHours: 24 * 365 },
        //         user.language || lang
        //     );
        // } catch (error) {
        //     this.logger.error(`Failed to email agent invite to ${user.email}`, error.stack);
        // }

        // return { invite_url: url, email_sent: emailSent };
        return { invite_url: url, email_sent: true };
    }

    // ---------- admin ----------

    async invite(dto: InviteAgentDto, actor: any, lang: string) {
        const country_code = dto.country_code || '+91';
        await this.assertContactFree(dto.phone_no, country_code, dto.email, lang);
        const category_ids = await this.resolveCategoryIds(dto.category_ids || [], lang);
        const region_id = await this.activeRegionId(dto.region_id, lang);

        let agent: any;
        for (let attempt = 0; attempt < 3 && !agent; attempt++) {
            const agent_no = await this.nextAgentNo();
            const now = +new Date();
            try {
                agent = await this.models.UserModel.create({
                    name: dto.name,
                    phone_no: dto.phone_no,
                    country_code,
                    email: dto.email || null,
                    user_type: UserType.AGENT,
                    is_active: true,
                    agent_no,
                    agent_code: `AG-${agent_no}`,
                    region_id,
                    category_ids,
                    agent_status: AgentStatus.INVITED,
                    invited_by: actor._id,
                    invited_at: now,
                    created_at: now,
                    updated_at: now
                });
            } catch (error) {
                const raceOnAgentNo = error?.code === 11000 && error?.keyPattern?.['agent_no'];
                if (!raceOnAgentNo || attempt === 2) throw error;
            }
        }

        await this.activity.log(agent._id, 'AGENT_INVITED', actor);
        const delivery = await this.sendInvite(agent, actor, lang);
        const populated = await this.models.UserModel.findById(agent._id).populate('category_ids', 'name is_active').populate('region_id', 'name is_active').lean();

        return this.common.successResponse(
            this.t(delivery.email_sent ? 'AGENT_INVITED' : 'AGENT_INVITED_EMAIL_FAILED', lang, { email: dto.email }),
            { ...this.toListDto(populated), ...delivery }
        );
    }

    async list(query: AgentListDto, lang: string) {
        const { page, limit, search, status, region_id, category_id } = query;
        const filter: any = this.agentFilter();

        if (status === AgentStatusFilter.PENDING) filter['agent_status'] = { $in: PENDING_STATUSES };
        else if (status) filter['agent_status'] = status;
        if (region_id) filter['region_id'] = new Types.ObjectId(region_id);
        if (category_id) filter['category_ids'] = new Types.ObjectId(category_id);
        if (search) {
            const regex = { $regex: search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' };
            const regionIds = await this.models.RegionModel.find({ name: regex }).distinct('_id');
            filter.$or = [{ name: regex }, { phone_no: regex }, { email: regex }, { 'agent_code': regex }, { 'region_id': { $in: regionIds } }];
        }

        const [total, agents] = await Promise.all([
            this.models.UserModel.countDocuments(filter),
            this.models.UserModel.find(filter)
                .populate('category_ids', 'name is_active').populate('region_id', 'name is_active')
                .sort({ created_at: -1 })
                .skip((page - 1) * limit)
                .limit(limit)
                .lean()
        ]);

        return this.common.paginatedResponse(this.t('AGENTS_FETCHED', lang), agents.map(a => this.toListDto(a)), total, page, limit);
    }

    /** Counts behind the filter chips: All, Pending KYC, Approved, Rejected. */
    async summary(lang: string) {
        const count = (extra: Record<string, any> = {}) => this.models.UserModel.countDocuments(this.agentFilter(extra));
        const [all, pending, approved, rejected] = await Promise.all([
            count(),
            count({ 'agent_status': { $in: PENDING_STATUSES } }),
            count({ 'agent_status': AgentStatus.APPROVED }),
            count({ 'agent_status': AgentStatus.REJECTED })
        ]);
        return this.common.successResponse(this.t('AGENTS_FETCHED', lang), { all, pending, approved, rejected });
    }

    async getById(id: string, lang: string) {
        const agent: any = await this.findAgent(id, lang);
        await agent.populate([{ path: 'category_ids', select: 'name is_active' }, { path: 'region_id', select: 'name is_active' }]);
        const a = agent;

        const activity = await this.activity.timeline(agent._id, (key, params) => this.t(key, lang, params));

        return this.common.successResponse(this.t('AGENT_FETCHED', lang), {
            ...this.toListDto(agent),
            kyc: {
                irdai_number: a.irdai_number,
                training_hours: a.training_hours,
                documents: (a.kyc_documents || []).map((d: any) => ({
                    type: d.type, number: d.number, file: d.file, status: d.status, uploaded_at: d.uploaded_at
                })),
                submitted_at: a.kyc_submitted_at,
                reviewed_at: a.kyc_reviewed_at,
                reject_reason: a.kyc_reject_reason
            },
            activity
        });
    }

    async update(id: string, dto: UpdateAgentDto, actor: any, lang: string) {
        const agent = await this.findAgent(id, lang);
        const set: any = {};
        const entries: { action: string; meta?: Record<string, any> }[] = [];

        if (dto.phone_no !== undefined || dto.country_code !== undefined || dto.email !== undefined) {
            await this.assertContactFree(
                dto.phone_no ?? agent.phone_no, dto.country_code ?? agent.country_code,
                dto.email && dto.email !== agent.email ? dto.email : undefined, lang, agent._id
            );
        }
        for (const key of ['name', 'phone_no', 'country_code', 'email'] as const) {
            if (dto[key] !== undefined && dto[key] !== agent[key]) set[key] = dto[key];
        }
        if (dto.region_id !== undefined && dto.region_id !== agent.region_id?.toString()) {
            set['region_id'] = await this.activeRegionId(dto.region_id, lang);
        }
        if (dto.category_ids !== undefined) {
            set['category_ids'] = await this.resolveCategoryIds(dto.category_ids, lang, agent.category_ids);
        }
        if (Object.keys(set).length) entries.push({ action: 'AGENT_UPDATED' });

        if (dto.is_active !== undefined && dto.is_active !== agent.is_active) {
            set.is_active = dto.is_active;
            entries.push({ action: dto.is_active ? 'AGENT_ACTIVATED' : 'AGENT_DEACTIVATED' });
        }

        if (entries.length) {
            set.updated_at = +new Date();
            await this.models.UserModel.updateOne({ _id: agent._id }, { $set: set });
            await this.activity.logMany(agent._id, entries, actor);
            if (dto.is_active === false) await this.models.SessionModel.deleteMany({ user_id: agent._id });
        }

        return this.getById(id, lang).then(res => ({ ...res, message: this.t('AGENT_UPDATED', lang) }));
    }

    async approve(id: string, actor: any, lang: string) {
        const agent = await this.findAgent(id, lang);
        if (agent.agent_status !== AgentStatus.SUBMITTED) this.fail('AGENT_NOT_AWAITING_REVIEW', lang);

        const now = +new Date();
        await this.models.UserModel.updateOne(
            { _id: agent._id },
            {
                $set: {
                    'agent_status': AgentStatus.APPROVED,
                    'kyc_documents.$[].status': AgentDocumentStatus.APPROVED,
                    'kyc_reviewed_by': actor._id,
                    'kyc_reviewed_at': now,
                    'kyc_reject_reason': null,
                    updated_at: now
                }
            }
        );
        await this.activity.logMany(agent._id, [{ action: 'AGENT_KYC_VERIFIED' }, { action: 'AGENT_APPROVED' }], actor);

        await this.notifyReview(agent, 'APPROVED', actor);
        return this.getById(id, lang).then(res => ({ ...res, message: this.t('AGENT_APPROVED', lang) }));
    }

    async reject(id: string, dto: RejectAgentDto, actor: any, lang: string) {
        const agent = await this.findAgent(id, lang);
        if (agent.agent_status !== AgentStatus.SUBMITTED) this.fail('AGENT_NOT_AWAITING_REVIEW', lang);

        const now = +new Date();
        await this.models.UserModel.updateOne(
            { _id: agent._id },
            {
                $set: {
                    'agent_status': AgentStatus.REJECTED,
                    'kyc_documents.$[].status': AgentDocumentStatus.REJECTED,
                    'kyc_reviewed_by': actor._id,
                    'kyc_reviewed_at': now,
                    'kyc_reject_reason': dto.reason,
                    updated_at: now
                }
            }
        );
        await this.activity.log(agent._id, 'AGENT_REJECTED', actor, { reason: dto.reason });

        await this.notifyReview(agent, 'REJECTED', actor, dto.reason);
        return this.getById(id, lang).then(res => ({ ...res, message: this.t('AGENT_REJECTED', lang) }));
    }

    /** Reconsider: puts a rejected agent's existing KYC back into review. */
    async reopen(id: string, actor: any, lang: string) {
        const agent = await this.findAgent(id, lang);
        if (agent.agent_status !== AgentStatus.REJECTED) this.fail('AGENT_NOT_REJECTED', lang);

        await this.models.UserModel.updateOne(
            { _id: agent._id },
            {
                $set: { 'agent_status': AgentStatus.SUBMITTED, 'kyc_documents.$[].status': AgentDocumentStatus.PENDING, 'kyc_reject_reason': null, updated_at: +new Date() }
            }
        );
        await this.activity.log(agent._id, 'AGENT_REOPENED', actor);

        return this.getById(id, lang).then(res => ({ ...res, message: this.t('AGENT_REOPENED', lang) }));
    }

    /** Tells the agent the outcome: in-app + push, and an email when they have one. Failures are logged, never thrown. */
    private async notifyReview(agent: any, outcome: 'APPROVED' | 'REJECTED', actor: any, reason?: string) {
        await this.notificationService.notifyUser({
            userId: agent._id,
            sentBy: actor._id,
            type: NotificationType.AGENT_KYC,
            titleKey: `NOTIFICATION_AGENT_${outcome}_TITLE`,
            messageKey: `NOTIFICATION_AGENT_${outcome}_MESSAGE`,
            meta: reason ? { reason } : undefined
        });

        if (!agent.email) return;
        try {
            const lang = agent.language || 'en';
            if (outcome === 'APPROVED') {
                await this.emailService.sendAgentApprovedEmail(agent.email, agent.name, `${this.appBaseUrl()}/dashboard`, lang);
            } else {
                await this.emailService.sendAgentRejectedEmail(agent.email, agent.name, reason || '', `${this.appBaseUrl()}/documents`, lang);
            }
        } catch (error) {
            this.logger.error(`Failed to email KYC ${outcome} to ${agent.email}`, error.stack);
        }
    }

    async resendInvite(id: string, actor: any, lang: string) {
        const agent = await this.findAgent(id, lang);
        if (![AgentStatus.INVITED, AgentStatus.ACCEPTED, AgentStatus.REJECTED].includes(agent.agent_status)) {
            this.fail('AGENT_INVITE_NOT_NEEDED', lang);
        }

        if (!agent.email) this.fail('AGENT_EMAIL_REQUIRED', lang);

        const delivery = await this.sendInvite(agent, actor, lang);
        await this.activity.log(agent._id, 'AGENT_INVITE_RESENT', actor);

        return this.common.successResponse(
            this.t(delivery.email_sent ? 'AGENT_INVITE_RESENT' : 'AGENT_INVITED_EMAIL_FAILED', lang, { email: agent.email }),
            delivery
        );
    }

    // ---------- agent app: phone + OTP login, own profile and documents (agent routes) ----------

    /** OTP is mocked as 1234 until a real SMS provider is connected. Set OTP_MOCK=false to send random OTPs. */
    private newOtp(): string {
        return this.configService.get<string>('OTP_MOCK') === 'false'
            ? String(Math.floor(1000 + Math.random() * 9000))
            : MOCK_OTP;
    }

    private async loadAgent(id: Types.ObjectId | string) {
        return this.models.UserModel.findOne({ _id: new Types.ObjectId(id as any), user_type: UserType.AGENT })
            .populate('category_ids', 'name is_active').populate('region_id', 'name is_active') as any;
    }

    private toAppView(agent: any) {
        const a = agent;
        const status: AgentStatus = a.agent_status;

        const review_status =
            status === AgentStatus.APPROVED ? AgentReviewStatus.APPROVED :
            status === AgentStatus.SUBMITTED ? AgentReviewStatus.UNDER_REVIEW :
            status === AgentStatus.REJECTED ? AgentReviewStatus.REJECTED :
            AgentReviewStatus.DOCUMENTS_REQUIRED;

        const next_screen =
            status === AgentStatus.APPROVED ? AgentScreen.DASHBOARD :
            status === AgentStatus.SUBMITTED || status === AgentStatus.REJECTED ? AgentScreen.DOCUMENT_REVIEW :
            AgentScreen.DOCUMENT_SUBMISSION;

        return {
            _id: agent._id,
            agent_code: a.agent_code,
            name: agent.name,
            phone_no: agent.phone_no,
            country_code: agent.country_code,
            email: agent.email,
            region: a.region_id?.name ? { _id: a.region_id._id, name: a.region_id.name } : null,
            categories: (a.category_ids || []).filter((c: any) => c?.name).map((c: any) => ({ _id: c._id, name: c.name })),
            review_status,
            next_screen,
            can_submit: status === AgentStatus.ACCEPTED || status === AgentStatus.INVITED || status === AgentStatus.REJECTED,
            can_resubmit: status === AgentStatus.REJECTED,
            reject_reason: status === AgentStatus.REJECTED ? a.kyc_reject_reason : null,
            kyc: {
                irdai_number: a.irdai_number,
                training_hours: a.training_hours,
                documents: (a.kyc_documents || []).map((d: any) => ({ type: d.type, number: d.number, file: d.file, status: d.status, uploaded_at: d.uploaded_at })),
                submitted_at: a.kyc_submitted_at,
                reviewed_at: a.kyc_reviewed_at
            }
        };
    }

    // ---------- login ----------

    async sendOtp(dto: SendOtpDto, lang: string) {
        const country_code = dto.country_code || '+91';
        const agent: any = await this.models.UserModel.findOne({ user_type: UserType.AGENT, phone_no: dto.phone_no, country_code }).select('is_active').lean();
        if (!agent) this.fail('AGENT_NOT_INVITED', lang, HttpStatus.NOT_FOUND);
        if (!agent.is_active) this.fail('ACCOUNT_DEACTIVATED', lang, HttpStatus.FORBIDDEN);

        const mobile = `${country_code}${dto.phone_no}`;
        const now = +new Date();
        const otp = this.newOtp();

        await this.models.OTPModel.updateMany({ mobile, type: OTPType.PHONE, is_used: false }, { is_used: true });
        await this.models.OTPModel.create({ mobile, type: OTPType.PHONE, otp, created_at: now, expires_at: now + OTP_EXPIRY_MS, is_used: false });

        try {
            await this.smsService.sendOTP(country_code, dto.phone_no, otp);
        } catch (error) {
            this.logger.error(`Failed to send OTP to ${mobile}`, error.stack);
        }

        return this.common.successResponse(this.t('AGENT_OTP_SENT', lang, { phone: `${country_code} ${dto.phone_no}` }), {
            expires_in_seconds: OTP_EXPIRY_MS / 1000
        });
    }

    async verifyOtp(dto: VerifyOtpDto, lang: string) {
        const country_code = dto.country_code || '+91';
        const mobile = `${country_code}${dto.phone_no}`;

        const record = await this.models.OTPModel.findOne({ mobile, type: OTPType.PHONE, otp: dto.otp, is_used: false }).sort({ created_at: -1 });
        if (!record) this.fail('INVALID_OTP', lang);
        if (this.common.isOTPExpired(record!.expires_at)) this.fail('OTP_EXPIRED', lang);

        const agent: any = await this.models.UserModel.findOne({ user_type: UserType.AGENT, phone_no: dto.phone_no, country_code });
        if (!agent) this.fail('AGENT_NOT_INVITED', lang, HttpStatus.NOT_FOUND);
        if (!agent.is_active) this.fail('ACCOUNT_DEACTIVATED', lang, HttpStatus.FORBIDDEN);

        await this.models.OTPModel.updateOne({ _id: record!._id }, { is_used: true });

        // First successful login accepts the invitation.
        const now = +new Date();
        const set: any = { is_phone_verified: true, updated_at: now };
        const accepting = agent.agent_status === AgentStatus.INVITED;
        if (accepting) {
            set['agent_status'] = AgentStatus.ACCEPTED;
            set['accepted_at'] = now;
        }
        await this.models.UserModel.updateOne({ _id: agent._id }, { $set: set });
        if (accepting) await this.activity.log(agent._id, 'AGENT_INVITE_ACCEPTED', agent);

        const tokenPayload = { _id: agent._id, scope: 'USER', user_type: UserType.AGENT, token_gen_at: now };
        const token = await this.common.generate_token(tokenPayload);
        await this.common.create_user_session(
            { device_type: dto.device_type, fcm_token: dto.fcm_token, voip_token: dto.voip_token },
            tokenPayload,
            token
        );

        return this.common.successResponse(this.t('LOGIN_SUCCESS', lang), { token, agent: this.toAppView(await this.loadAgent(agent._id)) });
    }

    async logout(agentId: string, lang: string) {
        await this.models.SessionModel.deleteMany({ user_id: new Types.ObjectId(agentId) });
        return this.common.successResponse(this.t('LOGOUT_SUCCESS', lang));
    }

    // ---------- own profile and documents ----------

    async me(agentId: string, lang: string) {
        return this.common.successResponse(this.t('PROFILE_RETRIEVED', lang), this.toAppView(await this.loadAgent(agentId)));
    }

    /** First submission, or resubmission after a rejection. Either way the agent goes (back) under review. */
    async submitKyc(agentId: string, dto: SubmitKycDto, lang: string) {
        const agent = await this.loadAgent(agentId);
        const previous: AgentStatus = agent.agent_status;

        if (previous === AgentStatus.SUBMITTED) this.fail('KYC_ALREADY_SUBMITTED', lang);
        if (previous === AgentStatus.APPROVED) this.fail('AGENT_ALREADY_APPROVED', lang);

        const types = dto.documents.map(d => d.type);
        if (new Set(types).size !== REQUIRED_DOCUMENTS.length || !REQUIRED_DOCUMENTS.every(t => types.includes(t))) {
            this.fail('KYC_DOCUMENTS_REQUIRED', lang);
        }
        const pan = dto.documents.find(d => d.type === AgentDocumentType.PAN)!;
        if (!pan.number || !PAN_FORMAT.test(pan.number)) this.fail('INVALID_PAN', lang);

        if (dto.email && dto.email !== agent.email && await this.models.UserModel.exists({ email: dto.email, _id: { $ne: agent._id } })) {
            this.fail('EMAIL_ALREADY_EXISTS', lang);
        }

        const now = +new Date();
        await this.models.UserModel.updateOne(
            { _id: agent._id },
            {
                $set: {
                    ...(dto.name ? { name: dto.name } : {}),
                    ...(dto.email ? { email: dto.email } : {}),
                    'agent_status': AgentStatus.SUBMITTED,
                    'irdai_number': dto.irdai_number,
                    'training_hours': dto.training_hours,
                    'kyc_documents': dto.documents.map(d => ({
                        type: d.type,
                        number: d.number || null,
                        file: d.file,
                        status: AgentDocumentStatus.PENDING,
                        uploaded_at: now
                    })),
                    'kyc_submitted_at': now,
                    'kyc_reject_reason': null,
                    updated_at: now
                }
            }
        );
        await this.activity.log(agent._id, previous === AgentStatus.REJECTED ? 'AGENT_KYC_RESUBMITTED' : 'AGENT_KYC_SUBMITTED', agent);

        return this.common.successResponse(this.t('KYC_SUBMITTED', lang), this.toAppView(await this.loadAgent(agentId)));
    }

    /** Placeholder for the dashboard; only reachable once approved (see AgentApprovedGuard). */
    async dashboard(agentId: string, lang: string) {
        return this.common.successResponse(this.t('AGENT_DASHBOARD_FETCHED', lang), this.toAppView(await this.loadAgent(agentId)));
    }
}
