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 { CommissionType } from './schema/product.schema';
import { CalculatePremiumDto, CreateProductDto, ProductListDto, UpdateProductDto } from './dto/product.dto';

const round2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;

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

    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 are salaried, so agent commission terms are hidden from them. */
    private canSeeCommission(actor: any) {
        return actor.user_type !== UserType.POLICY_STAFF;
    }

    private toDto(product: any, actor: any) {
        const cat = product.category_id, ins = product.insurer_id;
        return {
            _id: product._id,
            name: product.name,
            category: cat?._id ? { _id: cat._id, name: cat.name, is_active: cat.is_active } : null,
            insurer: ins?._id ? { _id: ins._id, name: ins.name, is_active: ins.is_active } : null,
            gst_rate: product.gst_rate,
            ...(this.canSeeCommission(actor) ? { commission_type: product.commission_type, commission_value: product.commission_value } : {}),
            is_active: product.is_active,
            created_at: product.created_at,
            updated_at: product.updated_at
        };
    }

    private populated(query: any) {
        return query.populate('category_id', 'name is_active').populate('insurer_id', 'name is_active');
    }

    private async findProduct(id: string, lang: string) {
        const product = Types.ObjectId.isValid(id) ? await this.models.ProductModel.findById(id) : null;
        if (!product) this.fail('PRODUCT_NOT_FOUND', lang, HttpStatus.NOT_FOUND);
        return product as any;
    }

    private assertCommission(type: CommissionType, value: number, lang: string) {
        if (type === CommissionType.PERCENT_OF_NET && value > 100) this.fail('INVALID_COMMISSION_RATE', lang);
    }

    private async assertCategoryAndInsurer(categoryId: Types.ObjectId, insurerId: Types.ObjectId, lang: string) {
        const [category, insurer] = await Promise.all([
            this.models.CategoryModel.findOne({ _id: categoryId, is_active: true }).lean(),
            this.models.InsurerModel.findOne({ _id: insurerId, is_active: true }).lean()
        ]);
        if (!category) this.fail('INVALID_CATEGORIES', lang);
        if (!insurer) this.fail('INVALID_INSURER', lang);
        const underwrites = (insurer!.category_ids || []).map(c => c.toString());
        if (underwrites.length && !underwrites.includes(categoryId.toString())) this.fail('INSURER_CATEGORY_MISMATCH', lang);
    }

    /** Net premium excludes GST; commission comes from the product's rule. Reused by policy entry. */
    calculatePremium(product: { gst_rate: number; commission_type: CommissionType; commission_value: number }, grossPremium: number) {
        const net = grossPremium / (1 + product.gst_rate / 100);
        const commission = product.commission_type === CommissionType.FLAT
            ? product.commission_value
            : net * product.commission_value / 100;
        return {
            gross_premium: round2(grossPremium),
            net_premium: round2(net),
            gst_amount: round2(grossPremium - net),
            commission: round2(commission)
        };
    }

    async create(dto: CreateProductDto, actor: any, lang: string) {
        this.assertCommission(dto.commission_type, dto.commission_value, lang);
        const categoryId = new Types.ObjectId(dto.category_id), insurerId = new Types.ObjectId(dto.insurer_id);
        await this.assertCategoryAndInsurer(categoryId, insurerId, lang);

        if (await this.models.ProductModel.findOne({ insurer_id: insurerId, name: dto.name }).collation({ locale: 'en', strength: 2 }).lean()) {
            this.fail('PRODUCT_ALREADY_EXISTS', lang);
        }

        const product = await this.models.ProductModel.create({
            name: dto.name, category_id: categoryId, insurer_id: insurerId,
            gst_rate: dto.gst_rate, commission_type: dto.commission_type, commission_value: dto.commission_value,
            created_by: actor._id
        });

        const saved = await this.populated(this.models.ProductModel.findById(product._id)).lean();
        return this.common.successResponse(this.t('PRODUCT_CREATED', lang), this.toDto(saved, actor));
    }

    async list(query: ProductListDto, actor: any, lang: string) {
        const { page, limit, search, category_id, insurer_id, is_active, assignee_id } = query;
        const filter: any = {};
        if (search) filter.name = { $regex: search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' };
        if (category_id) filter.category_id = new Types.ObjectId(category_id);
        if (insurer_id) filter.insurer_id = new Types.ObjectId(insurer_id);
        if (is_active !== undefined) filter.is_active = is_active;

        if (assignee_id) {
            const assignee: any = await this.models.UserModel.findOne({ _id: new Types.ObjectId(assignee_id), is_active: true }).select('user_type category_ids agent_status').lean();
            if (!assignee) this.fail('LEAD_INVALID_ASSIGNEE', lang);
            if (assignee.user_type === UserType.AGENT) {
                const allowed: Types.ObjectId[] = assignee.agent_status === AgentStatus.APPROVED ? assignee.category_ids || [] : [];
                const requested = filter.category_id ? [filter.category_id.toString()] : null;
                filter.category_id = { $in: requested ? allowed.filter(c => requested.includes(c.toString())) : allowed };
            }
        }

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

        return this.common.paginatedResponse(this.t('PRODUCTS_FETCHED', lang), products.map(p => this.toDto(p, actor)), total, page, limit);
    }

    async getById(id: string, actor: any, lang: string) {
        const found = await this.findProduct(id, lang);
        const product = await this.populated(this.models.ProductModel.findById(found._id)).lean();
        return this.common.successResponse(this.t('PRODUCT_FETCHED', lang), this.toDto(product, actor));
    }

    async update(id: string, dto: UpdateProductDto, actor: any, lang: string) {
        const product = await this.findProduct(id, lang);

        const type = dto.commission_type ?? product.commission_type;
        const value = dto.commission_value ?? product.commission_value;
        this.assertCommission(type, value, lang);

        const categoryId = dto.category_id ? new Types.ObjectId(dto.category_id) : product.category_id;
        const insurerId = dto.insurer_id ? new Types.ObjectId(dto.insurer_id) : product.insurer_id;
        const placementChanged = categoryId.toString() !== product.category_id.toString() || insurerId.toString() !== product.insurer_id.toString();
        if (placementChanged) await this.assertCategoryAndInsurer(categoryId, insurerId, lang);

        const name = dto.name ?? product.name;
        if ((placementChanged || name.toLowerCase() !== product.name.toLowerCase()) &&
            await this.models.ProductModel.findOne({ insurer_id: insurerId, name, _id: { $ne: product._id } }).collation({ locale: 'en', strength: 2 }).lean()) {
            this.fail('PRODUCT_ALREADY_EXISTS', lang);
        }

        Object.assign(product, {
            name, category_id: categoryId, insurer_id: insurerId, commission_type: type, commission_value: value,
            ...(dto.gst_rate !== undefined ? { gst_rate: dto.gst_rate } : {}),
            ...(dto.is_active !== undefined ? { is_active: dto.is_active } : {})
        });
        await product.save();

        const saved = await this.populated(this.models.ProductModel.findById(product._id)).lean();
        return this.common.successResponse(this.t('PRODUCT_UPDATED', lang), this.toDto(saved, actor));
    }

    async calculate(id: string, query: CalculatePremiumDto, actor: any, lang: string) {
        const product = await this.findProduct(id, lang);
        const { commission, ...amounts } = this.calculatePremium(product, query.gross_premium);
        return this.common.successResponse(this.t('PREMIUM_CALCULATED', lang), {
            ...amounts,
            gst_rate: product.gst_rate,
            ...(this.canSeeCommission(actor) ? { commission, commission_type: product.commission_type, commission_value: product.commission_value } : {})
        });
    }
}
