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 { ProductService } from 'src/product/product.service';
import { AgentStatus, UserType } from 'src/user/schema/users.schema';
import { LeadHistoryType, LeadStage } from 'src/lead/schema/lead.schema';
import { CommissionStatus, PolicyDisplayStatus, PolicyStatus } from './schema/policy.schema';
import { formatPolicy, POLICY_POPULATE } from './policy.format';
import { CancelPolicyDto, ConvertLeadToPolicyDto, CreatePolicyDto, IssuedPeriod, PolicyListDto, UpdatePolicyDto } from './dto/policy.dto';

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

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

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

    private t(key: string, lang: string) {
        return this.translationService.translate(key, lang);
    }

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

    /** Policy Staff only see policies they handled. */
    private scope(actor: any): Record<string, any> {
        return actor.user_type === UserType.POLICY_STAFF ? { assigned_to: actor._id } : {};
    }

    /** Commission is for agents; Policy Staff are salaried and never see it. */
    private showCommission(actor: any) {
        return actor.user_type !== UserType.POLICY_STAFF;
    }

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

    private async detail(id: Types.ObjectId | string, actor: any) {
        const policy = await this.models.PolicyModel.findById(id).populate(POLICY_POPULATE).lean();
        return formatPolicy(policy, this.showCommission(actor));
    }

    private assertDates(issue: number, expiry: number, lang: string) {
        if (expiry <= issue) this.fail('INVALID_POLICY_DATES', lang);
    }

    /** Agents must be approved and authorised for the product's category; Policy Staff can handle anything. */
    private async resolveAssignee(assignedTo: string, categoryId: Types.ObjectId, 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: categoryId }
            ]
        }).lean();
        if (!assignee) this.fail('LEAD_INVALID_ASSIGNEE', lang);
        return assignee!;
    }

    private async listFilter(query: PolicyListDto, actor: any) {
        const now = +new Date();
        const filter: any = {};
        if (query.category_id) filter.category_id = new Types.ObjectId(query.category_id);
        if (query.product_id) filter.product_id = new Types.ObjectId(query.product_id);
        if (query.insurer_id) filter.insurer_id = new Types.ObjectId(query.insurer_id);
        if (query.assigned_to) filter.assigned_to = new Types.ObjectId(query.assigned_to);
        if (query.customer_id) filter.customer_id = new Types.ObjectId(query.customer_id);

        if (query.status === PolicyDisplayStatus.ACTIVE) Object.assign(filter, { status: PolicyStatus.ACTIVE, expiry_date: { $gte: now } });
        if (query.status === PolicyDisplayStatus.EXPIRED) Object.assign(filter, { status: PolicyStatus.ACTIVE, expiry_date: { $lt: now } });
        if (query.status === PolicyDisplayStatus.CANCELLED) filter.status = PolicyStatus.CANCELLED;
        if (query.expiring_in_days) {
            Object.assign(filter, { status: PolicyStatus.ACTIVE, expiry_date: { $gte: now, $lte: now + query.expiring_in_days * DAY_MS } });
        }

        if (query.issued === IssuedPeriod.LAST_30_DAYS) filter.issue_date = { $gte: now - 30 * DAY_MS };
        if (query.issued === IssuedPeriod.LAST_90_DAYS) filter.issue_date = { $gte: now - 90 * DAY_MS };
        if (query.issued === IssuedPeriod.THIS_QUARTER) {
            const d = new Date();
            filter.issue_date = { $gte: new Date(d.getFullYear(), Math.floor(d.getMonth() / 3) * 3, 1).getTime() };
        }

        if (query.search) {
            const regex = { $regex: escapeRegex(query.search), $options: 'i' };
            const [customerIds, insurerIds] = await Promise.all([
                this.models.CustomerModel.find({ $or: [{ name: regex }, { phone_no: regex }] }).distinct('_id'),
                this.models.InsurerModel.find({ name: regex }).distinct('_id')
            ]);
            filter.$or = [{ policy_number: regex }, { customer_id: { $in: customerIds } }, { insurer_id: { $in: insurerIds } }];
        }

        return { ...filter, ...this.scope(actor) };
    }

    // ---------- create / convert ----------

    private async record(dto: CreatePolicyDto, actor: any, lang: string, lead?: any) {
        if (!(await this.models.CustomerModel.exists({ _id: new Types.ObjectId(dto.customer_id) }))) {
            this.fail('CUSTOMER_NOT_FOUND', lang, HttpStatus.NOT_FOUND);
        }

        const product = await this.models.ProductModel.findOne({ _id: new Types.ObjectId(dto.product_id), is_active: true }).lean();
        if (!product) this.fail('INVALID_PRODUCT', lang);

        if (actor.user_type === UserType.POLICY_STAFF && dto.assigned_to !== actor._id.toString()) {
            this.fail('LEAD_ASSIGN_SELF_ONLY', lang, HttpStatus.FORBIDDEN);
        }
        const assignee: any = await this.resolveAssignee(dto.assigned_to, product!.category_id, lang);

        this.assertDates(dto.issue_date, dto.expiry_date, lang);
        if (lead && lead.category_id.toString() !== product!.category_id.toString()) this.fail('LEAD_PRODUCT_CATEGORY_MISMATCH', lang);

        if (await this.models.PolicyModel.findOne({ policy_number: dto.policy_number }).collation({ locale: 'en', strength: 2 }).lean()) {
            this.fail('POLICY_NUMBER_EXISTS', lang);
        }

        const calc = this.productService.calculatePremium(product!, dto.gross_premium);
        const forAgent = assignee.user_type === UserType.AGENT;

        let policy: any;
        try {
            policy = await this.models.PolicyModel.create({
                policy_number: dto.policy_number,
                customer_id: new Types.ObjectId(dto.customer_id),
                product_id: product!._id,
                product_name: product!.name,
                category_id: product!.category_id,
                insurer_id: product!.insurer_id,
                assigned_to: assignee._id,
                lead_id: lead?._id ?? null,
                gross_premium: calc.gross_premium,
                gst_rate: product!.gst_rate,
                net_premium: calc.net_premium,
                gst_amount: calc.gst_amount,
                commission_type: product!.commission_type,
                commission_value: product!.commission_value,
                commission_amount: forAgent ? calc.commission : null,
                commission_status: forAgent ? CommissionStatus.PENDING : CommissionStatus.NOT_APPLICABLE,
                premium_frequency: dto.premium_frequency,
                sum_insured: dto.sum_insured ?? null,
                sum_insured_note: dto.sum_insured_note || null,
                insured_members: dto.insured_members || [],
                vehicle_registration: dto.vehicle_registration || null,
                issue_date: dto.issue_date,
                expiry_date: dto.expiry_date,
                schedule_file: dto.schedule_file || null,
                created_by: actor._id
            });
        } catch (error) {
            if (error?.code === 11000) this.fail('POLICY_NUMBER_EXISTS', lang);
            throw error;
        }

        if (lead) {
            const now = +new Date();
            const converted = await this.models.LeadModel.updateOne(
                { _id: lead._id, stage: { $ne: LeadStage.CONVERTED } },
                {
                    $set: { stage: LeadStage.CONVERTED, converted_at: now, next_follow_up_at: null, lost_reason: null, policy_id: policy._id },
                    $push: {
                        history: {
                            $each: [{
                                type: LeadHistoryType.EVENT, action: 'CONVERTED_TO_POLICY', text: null,
                                meta: { policy_number: policy.policy_number },
                                actor_id: actor._id, actor_name: actor.name ?? null, created_at: now
                            }],
                            $slice: -HISTORY_LIMIT
                        }
                    }
                }
            );
            if (!converted.modifiedCount) {
                await this.models.PolicyModel.deleteOne({ _id: policy._id });
                this.fail('LEAD_CONVERTED_LOCKED', lang);
            }
        }

        return policy;
    }

    async create(dto: CreatePolicyDto, actor: any, lang: string) {
        const policy = await this.record(dto, actor, lang);
        return this.common.successResponse(this.t('POLICY_CREATED', lang), await this.detail(policy._id, actor));
    }

    async convertLead(leadId: string, dto: ConvertLeadToPolicyDto, actor: any, lang: string) {
        const lead: any = Types.ObjectId.isValid(leadId)
            ? await this.models.LeadModel.findOne({ _id: new Types.ObjectId(leadId), ...this.scope(actor) }).lean()
            : null;
        if (!lead) this.fail('LEAD_NOT_FOUND', lang, HttpStatus.NOT_FOUND);
        if (lead.stage === LeadStage.CONVERTED) this.fail('LEAD_CONVERTED_LOCKED', lang);

        const policy = await this.record(
            { ...dto, customer_id: lead.customer_id.toString(), assigned_to: dto.assigned_to ?? lead.assigned_to.toString() },
            actor, lang, lead
        );
        return this.common.successResponse(this.t('LEAD_CONVERTED_TO_POLICY', lang), await this.detail(policy._id, actor));
    }

    // ---------- read ----------

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

        const [total, policies] = await Promise.all([
            this.models.PolicyModel.countDocuments(filter),
            this.models.PolicyModel.find(filter)
                .populate(POLICY_POPULATE)
                .sort(query.expiring_in_days ? { expiry_date: 1 } : { created_at: -1 })
                .skip((page - 1) * limit)
                .limit(limit)
                .lean()
        ]);

        return this.common.paginatedResponse(
            this.t('POLICIES_FETCHED', lang),
            policies.map(p => formatPolicy(p, this.showCommission(actor))),
            total, page, limit
        );
    }

    async getById(id: string, actor: any, lang: string) {
        const policy = await this.findPolicy(id, actor, lang);
        return this.common.successResponse(this.t('POLICY_FETCHED', lang), await this.detail(policy._id, actor));
    }

    // ---------- change ----------

    async update(id: string, dto: UpdatePolicyDto, actor: any, lang: string) {
        const policy = await this.findPolicy(id, actor, lang);
        if (policy.status === PolicyStatus.CANCELLED) this.fail('POLICY_CANCELLED_LOCKED', lang);

        this.assertDates(dto.issue_date ?? policy.issue_date, dto.expiry_date ?? policy.expiry_date, lang);

        const set: any = {};
        if (dto.gross_premium !== undefined && dto.gross_premium !== policy.gross_premium) {
            if (policy.commission_status === CommissionStatus.PAID) this.fail('COMMISSION_ALREADY_PAID', lang);
            const calc = this.productService.calculatePremium(policy, dto.gross_premium);
            Object.assign(set, {
                gross_premium: calc.gross_premium, net_premium: calc.net_premium, gst_amount: calc.gst_amount,
                commission_amount: policy.commission_status === CommissionStatus.PENDING ? calc.commission : null
            });
        }
        for (const key of ['premium_frequency', 'issue_date', 'expiry_date', 'sum_insured', 'sum_insured_note', 'insured_members', 'vehicle_registration', 'schedule_file'] as const) {
            if (dto[key] !== undefined) set[key] = dto[key];
        }

        if (Object.keys(set).length) await this.models.PolicyModel.updateOne({ _id: policy._id }, { $set: set });

        return this.common.successResponse(this.t('POLICY_UPDATED', lang), await this.detail(policy._id, actor));
    }

    async cancel(id: string, dto: CancelPolicyDto, actor: any, lang: string) {
        const policy = await this.findPolicy(id, actor, lang);
        if (policy.status === PolicyStatus.CANCELLED) this.fail('POLICY_ALREADY_CANCELLED', lang);

        await this.models.PolicyModel.updateOne({ _id: policy._id }, {
            $set: {
                status: PolicyStatus.CANCELLED,
                cancel_reason: dto.reason,
                cancelled_at: +new Date(),
                ...(policy.commission_status === CommissionStatus.PENDING ? { commission_status: CommissionStatus.CANCELLED } : {})
            }
        });

        return this.common.successResponse(this.t('POLICY_CANCELLED', lang), await this.detail(policy._id, actor));
    }
}
