import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
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 { AgentStatus, UserType } from 'src/user/schema/users.schema';
import { LeadHistoryType, LeadStage, OPEN_STAGES } from './schema/lead.schema';
import { AddLeadNoteDto, AssigneesDto, CreateLeadDto, LeadBoardDto, LeadListDto, MoveLeadStageDto, UpdateLeadDto } from './dto/lead.dto';

const HISTORY_LIMIT = 500;
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

@Injectable()
export class LeadService {
    constructor(
        private readonly models: ModelsService,
        private readonly common: CommonService,
        private readonly translationService: TranslationService
    ) { }

    // ---------- 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);
    }

    /** Policy Staff only work leads assigned to them. */
    private scope(actor: any): Record<string, any> {
        return actor.user_type === UserType.POLICY_STAFF ? { assigned_to: actor._id } : {};
    }

    private populated(query: any) {
        return query
            .populate('customer_id', 'name phone_no country_code email')
            .populate('category_id', 'name')
            .populate('assigned_to', 'name user_type agent_code');
    }

    private entry(type: LeadHistoryType, actor: any, extra: { action?: string; text?: string; meta?: any } = {}) {
        return {
            type,
            action: extra.action ?? null,
            text: extra.text ?? null,
            meta: extra.meta ?? null,
            actor_id: actor._id,
            actor_name: actor.name ?? null,
            created_at: +new Date()
        };
    }

    private push(entries: any[]) {
        return { $each: entries, $slice: -HISTORY_LIMIT };
    }

    private toDto(lead: any) {
        const c = lead.customer_id, cat = lead.category_id, a = lead.assigned_to;
        return {
            _id: lead._id,
            stage: lead.stage,
            customer: c?._id ? { _id: c._id, name: c.name, phone_no: c.phone_no, country_code: c.country_code, email: c.email } : null,
            category: cat?._id ? { _id: cat._id, name: cat.name } : null,
            assigned_to: a?._id ? { _id: a._id, name: a.name, user_type: a.user_type, agent_code: a.agent_code ?? null } : null,
            estimated_premium: lead.estimated_premium,
            notes: lead.notes,
            lost_reason: lead.lost_reason,
            last_call_at: lead.last_call_at,
            next_follow_up_at: lead.next_follow_up_at,
            converted_at: lead.converted_at,
            policy_id: lead.policy_id ?? null,
            created_at: lead.created_at,
            updated_at: lead.updated_at
        };
    }

    private async findLead(id: string, actor: any, lang: string) {
        const lead = Types.ObjectId.isValid(id)
            ? await this.models.LeadModel.findOne({ _id: new Types.ObjectId(id), ...this.scope(actor) })
            : null;
        if (!lead) this.fail('LEAD_NOT_FOUND', lang, HttpStatus.NOT_FOUND);
        return lead as any;
    }

    private async buildDetail(id: Types.ObjectId | string, lang: string) {
        const lead: any = await this.populated(this.models.LeadModel.findById(id)).lean();

        const history = [...(lead.history || [])]
            .sort((a, b) => b.created_at - a.created_at)
            .map(h => ({
                type: h.type,
                message: h.type === LeadHistoryType.EVENT
                    ? this.t(`LEAD_ACTIVITY_${h.action}`, lang, {
                        actor: h.actor_name || '',
                        stage: h.meta?.stage ? this.t(`STAGE_${h.meta.stage}`, lang) : '',
                        assignee: h.meta?.assignee_name || '',
                        reason: h.meta?.reason || '',
                        policy: h.meta?.policy_number || ''
                    })
                    : h.text,
                actor_id: h.actor_id,
                actor_name: h.actor_name,
                created_at: h.created_at
            }));

        return { ...this.toDto(lead), history };
    }

    /** Assignee must be an active Policy Staff, or an approved agent authorised for the category. */
    private async resolveAssignee(assignedTo: string, categoryId: Types.ObjectId | string, lang: string) {
        const assignee = await this.models.UserModel.findOne({
            _id: new Types.ObjectId(assignedTo),
            is_active: true,
            $or: [
                { user_type: UserType.POLICY_STAFF, is_email_verified: true },
                { user_type: UserType.AGENT, agent_status: AgentStatus.APPROVED, category_ids: new Types.ObjectId(categoryId as any) }
            ]
        }).lean();
        if (!assignee) this.fail('LEAD_INVALID_ASSIGNEE', lang);
        return assignee!;
    }

    private async activeCategory(categoryId: string, lang: string) {
        const category = await this.models.CategoryModel.findOne({ _id: new Types.ObjectId(categoryId), is_active: true }).lean();
        if (!category) this.fail('INVALID_CATEGORIES', lang);
        return category!;
    }

    private async listFilter(query: { search?: string; assigned_to?: string; category_id?: string; customer_id?: string; stage?: LeadStage; follow_up_due?: string }, actor: any) {
        const filter: any = {};
        if (query.stage) filter.stage = query.stage;
        if (query.assigned_to) filter.assigned_to = new Types.ObjectId(query.assigned_to);
        if (query.category_id) filter.category_id = new Types.ObjectId(query.category_id);
        if (query.customer_id) filter.customer_id = new Types.ObjectId(query.customer_id);
        if (query.follow_up_due === 'true') {
            filter.stage = { $in: OPEN_STAGES };
            filter.next_follow_up_at = { $ne: null, $lte: new Date().setHours(23, 59, 59, 999) };
        }
        if (query.search) {
            const regex = { $regex: escapeRegex(query.search), $options: 'i' };
            const customerIds = await this.models.CustomerModel.find({ $or: [{ name: regex }, { phone_no: regex }] }).distinct('_id');
            filter.$or = [{ customer_id: { $in: customerIds } }, { notes: regex }];
        }
        return { ...filter, ...this.scope(actor) };
    }

    // ---------- endpoints ----------

    async create(dto: CreateLeadDto, actor: any, lang: string) {
        let customer: any;
        if (dto.customer_id) {
            customer = await this.models.CustomerModel.findById(dto.customer_id);
            if (!customer) this.fail('CUSTOMER_NOT_FOUND', lang, HttpStatus.NOT_FOUND);
        } else {
            if (!dto.customer_name || !dto.phone_no) this.fail('LEAD_CUSTOMER_REQUIRED', lang);
            const country_code = dto.country_code || '+91';
            customer = await this.models.CustomerModel.findOne({ country_code, phone_no: dto.phone_no });
            if (!customer) {
                customer = await this.models.CustomerModel.create({
                    name: dto.customer_name, phone_no: dto.phone_no, country_code, email: dto.email || null, created_by: actor._id
                });
            }
        }

        const category = await this.activeCategory(dto.category_id, lang);
        if (actor.user_type === UserType.POLICY_STAFF && dto.assigned_to !== actor._id.toString()) {
            this.fail('LEAD_ASSIGN_SELF_ONLY', lang, HttpStatus.FORBIDDEN);
        }
        await this.resolveAssignee(dto.assigned_to, category._id as Types.ObjectId, lang);

        const lead = await this.models.LeadModel.create({
            customer_id: customer._id,
            category_id: category._id,
            assigned_to: new Types.ObjectId(dto.assigned_to),
            stage: LeadStage.NEW,
            estimated_premium: dto.estimated_premium ?? null,
            notes: dto.notes || null,
            created_by: actor._id,
            history: [this.entry(LeadHistoryType.EVENT, actor, { action: 'CREATED' })]
        });

        return this.common.successResponse(this.t('LEAD_CREATED', lang), await this.buildDetail(lead._id as Types.ObjectId, lang));
    }

    async list(query: LeadListDto, actor: any, lang: string) {
        const { page, limit } = query;
        const filter = await this.listFilter(query, actor);

        const [total, leads] = await Promise.all([
            this.models.LeadModel.countDocuments(filter),
            this.populated(this.models.LeadModel.find(filter))
                .select('-history')
                .sort({ created_at: -1 })
                .skip((page - 1) * limit)
                .limit(limit)
                .lean()
        ]);

        return this.common.paginatedResponse(this.t('LEADS_FETCHED', lang), leads.map((l: any) => this.toDto(l)), total, page, limit);
    }

    /** Kanban data: one column per stage, newest first. */
    async board(query: LeadBoardDto, actor: any, lang: string) {
        const base = await this.listFilter(query, actor);

        const stages = await Promise.all(Object.values(LeadStage).map(async stage => {
            const filter = { ...base, stage };
            const [count, leads] = await Promise.all([
                this.models.LeadModel.countDocuments(filter),
                this.populated(this.models.LeadModel.find(filter))
                    .select('-history')
                    .sort({ updated_at: -1 })
                    .limit(query.limit_per_stage)
                    .lean()
            ]);
            return { stage, count, leads: leads.map((l: any) => this.toDto(l)) };
        }));

        return this.common.successResponse(this.t('LEAD_BOARD_FETCHED', lang), { stages });
    }

    /** People a lead can be assigned to: approved agents (for the category) and active Policy Staff. */
    async assignees(query: AssigneesDto, actor: any, lang: string) {
        const agentFilter: any = { user_type: UserType.AGENT, is_active: true, agent_status: AgentStatus.APPROVED };
        if (query.category_id) agentFilter.category_ids = new Types.ObjectId(query.category_id);
        const staffFilter = { user_type: UserType.POLICY_STAFF, is_active: true, is_email_verified: true };

        const filter: any = actor.user_type === UserType.POLICY_STAFF ? { _id: actor._id } : { $or: [agentFilter, staffFilter] };
        const users: any[] = await this.models.UserModel.find(filter).select('name user_type agent_code').sort({ name: 1 }).lean();

        return this.common.successResponse(
            this.t('ASSIGNEES_FETCHED', lang),
            users.map(u => ({ _id: u._id, name: u.name, user_type: u.user_type, agent_code: u.agent_code ?? null }))
        );
    }

    async getById(id: string, actor: any, lang: string) {
        const lead = await this.findLead(id, actor, lang);
        return this.common.successResponse(this.t('LEAD_FETCHED', lang), await this.buildDetail(lead._id, lang));
    }

    async update(id: string, dto: UpdateLeadDto, actor: any, lang: string) {
        const lead = await this.findLead(id, actor, lang);

        const categoryChanged = dto.category_id !== undefined && dto.category_id !== lead.category_id.toString();
        const assigneeChanged = dto.assigned_to !== undefined && dto.assigned_to !== lead.assigned_to.toString();
        if (lead.stage === LeadStage.CONVERTED && (categoryChanged || assigneeChanged)) this.fail('LEAD_CONVERTED_LOCKED', lang);
        if (assigneeChanged && actor.user_type === UserType.POLICY_STAFF) this.fail('LEAD_ASSIGN_SELF_ONLY', lang, HttpStatus.FORBIDDEN);

        const set: any = {};
        const events: any[] = [];

        if (categoryChanged || assigneeChanged) {
            const categoryId = categoryChanged ? (await this.activeCategory(dto.category_id!, lang))._id : lead.category_id;
            const assignee: any = await this.resolveAssignee(dto.assigned_to ?? lead.assigned_to.toString(), categoryId, lang);
            if (categoryChanged) set.category_id = categoryId;
            if (assigneeChanged) {
                set.assigned_to = assignee._id;
                events.push(this.entry(LeadHistoryType.EVENT, actor, { action: 'ASSIGNED', meta: { assignee_name: assignee.name } }));
            }
        }
        if (dto.estimated_premium !== undefined && dto.estimated_premium !== lead.estimated_premium) set.estimated_premium = dto.estimated_premium;
        if (dto.notes !== undefined && dto.notes !== lead.notes) set.notes = dto.notes;
        if (categoryChanged || set.estimated_premium !== undefined || set.notes !== undefined) {
            events.push(this.entry(LeadHistoryType.EVENT, actor, { action: 'UPDATED' }));
        }

        if (events.length) {
            await this.models.LeadModel.updateOne({ _id: lead._id }, { $set: set, $push: { history: this.push(events) } });
        }

        return this.common.successResponse(this.t('LEAD_UPDATED', lang), await this.buildDetail(lead._id, lang));
    }

    async moveStage(id: string, dto: MoveLeadStageDto, actor: any, lang: string) {
        const lead = await this.findLead(id, actor, lang);
        if (lead.stage === LeadStage.CONVERTED) this.fail('LEAD_CONVERTED_LOCKED', lang);

        if (dto.stage === LeadStage.CONVERTED) this.fail('LEAD_USE_CONVERT', lang);

        if (dto.stage !== lead.stage) {
            const closing = dto.stage === LeadStage.LOST;
            const reason = dto.stage === LeadStage.LOST ? dto.lost_reason : undefined;

            await this.models.LeadModel.updateOne({ _id: lead._id }, {
                $set: {
                    stage: dto.stage,
                    lost_reason: reason ?? null,
                    ...(closing ? { next_follow_up_at: null } : {})
                },
                $push: {
                    history: this.push([this.entry(LeadHistoryType.EVENT, actor, {
                        action: reason ? 'STAGE_CHANGED_REASON' : 'STAGE_CHANGED',
                        meta: { stage: dto.stage, reason }
                    })])
                }
            });
        }

        return this.common.successResponse(this.t('LEAD_STAGE_UPDATED', lang), await this.buildDetail(lead._id, lang));
    }

    async addNote(id: string, dto: AddLeadNoteDto, actor: any, lang: string) {
        const lead = await this.findLead(id, actor, lang);
        if (dto.next_follow_up_at !== undefined && dto.next_follow_up_at <= +new Date()) this.fail('LEAD_FOLLOW_UP_PAST', lang);

        const set: any = {};
        if (dto.is_call) set.last_call_at = +new Date();
        if (dto.next_follow_up_at !== undefined) set.next_follow_up_at = dto.next_follow_up_at;

        await this.models.LeadModel.updateOne({ _id: lead._id }, {
            $set: set,
            $push: { history: this.push([this.entry(dto.is_call ? LeadHistoryType.CALL : LeadHistoryType.NOTE, actor, { text: dto.remark })]) }
        });

        return this.common.successResponse(this.t('LEAD_NOTE_ADDED', lang), await this.buildDetail(lead._id, lang));
    }
}
