import { PlanDao } from "../dao/plan.dao";
import { StripeService } from "./stripe.service";
import { LanguageService } from "../services/language.service";
import { sequelize } from "../models";
import { Common } from "../../utils/common";
import { AppError } from "../../utils/errors";
import { PLAN } from "../config/constants";
import Axios from "axios";

export class PlanService {
    private accountId: number | null;
    private userId: number | null;
    private language: string;
    private scope: string[] | null;
    private config: userConfig | null;
    private planDao: PlanDao;
    private languageService: LanguageService;
    private stripeService: StripeService;
    constructor(
        private options: {
            language: string;
            scope: string[] | null;
            accountId: number | null;
            userId: number | null;
            config: userConfig | null;
        } = {
            language: process.env.DEFAULT_LANGUAGE_CODE!,
            scope: null,
            config: null,
            userId: null,
            accountId: null,
        }
    ) {
        this.language = options.language;
        this.scope = options.scope ?? [];
        this.config = options.config ?? null;
        this.userId = options.userId ?? null;
        this.accountId = options.accountId ?? null;
        this.planDao = new PlanDao({
            userId: this.userId,
            accountId: this.accountId,
            language: this.language,
            scope: this.scope,
            config: this.config,
        });

        this.languageService = new LanguageService({
            userId: this.userId,
            accountId: this.accountId,
            language: this.language,
            scope: this.scope,
            config: this.config,
        });
        this.stripeService = new StripeService();
    }

    private getRazorpayConfig = () => {
        const keyId = process.env.RAZORPAY_KEY_ID;
        const keySecret = process.env.RAZORPAY_KEY_SECRET;

        if (!keyId || !keySecret) {
            throw new AppError(500, "RAZORPAY_CONFIGURATION_MISSING", {});
        }

        return { keyId, keySecret };
    }

    private razorpayRequest = async (method: "get" | "post" | "patch", path: string, data?: any) => {
        const { keyId, keySecret } = this.getRazorpayConfig();
        const client = Axios.create({
            baseURL: "https://api.razorpay.com/v1",
            auth: { username: keyId, password: keySecret },
            headers: { "Content-Type": "application/json" }
        });

        try {
            const response = await client.request({
                method,
                url: path,
                data
            });

            return response.data;
        } catch (err: any) {
            const razorpayError = err?.response?.data || {
                message: err?.message || "Razorpay request failed"
            };
            throw new AppError(400, "RAZORPAY_REQUEST_FAILED", razorpayError);
        }
    }

    private resolvePlanType = (prices: PriceInterface[]): number => {
        const hasSubscriptionPricing = prices.some((price) => (price.autoRenew ?? 1) === 1);
        const hasOneTimePricing = prices.some((price) => (price.autoRenew ?? 1) === 0);

        if (hasSubscriptionPricing && hasOneTimePricing) {
            throw new AppError(400, "MIXED_PLAN_PRICE_TYPES_NOT_ALLOWED", {});
        }

        return hasSubscriptionPricing ? PLAN.PLAN_TYPE.SUBSCRIPTION : PLAN.PLAN_TYPE.ONE_TIME;
    }

    private getRazorpayPeriod = (frequency: PriceInterface["frequency"]) => {
        switch (frequency) {
            case "days":
                return { period: "daily", intervalCount: 1 };
            case "weeks":
                return { period: "weekly", intervalCount: 1 };
            case "months":
                return { period: "monthly", intervalCount: 1 };
            case "years":
                return { period: "yearly", intervalCount: 1 };
            default:
                throw new AppError(400, "INVALID_PLAN_INTERVAL", {});
        }
    }

    private getStripeInterval = (frequency: PriceInterface["frequency"]) => {
        switch (frequency) {
            case "days":
                return "day" as const;
            case "weeks":
                return "week" as const;
            case "months":
                return "month" as const;
            case "years":
                return "year" as const;
            default:
                throw new AppError(400, "INVALID_PLAN_INTERVAL", {});
        }
    }

    private syncSubscriptionPricesWithRazorpay = async (planName: string, planDescription: string | null | undefined, prices: PriceInterface[]): Promise<PriceInterface[]> => {
        return await Promise.all(prices.map(async (price) => {
            if (!price.duration || !price.frequency) {
                throw new AppError(400, "PLAN_DURATION_FREQUENCY_REQUIRED_FOR_SUBSCRIPTION", {});
            }

            const { period, intervalCount } = this.getRazorpayPeriod(price.frequency);
            const response = await this.razorpayRequest("post", "/plans", {
                period,
                interval: price.duration ?? intervalCount,
                item: {
                    name: planName,
                    amount: Math.round(Number(price.amount) * 100),
                    currency: String(price.currency || "").toUpperCase(),
                    description: planDescription ? String(planDescription) : planName
                }
            });

            return {
                ...price,
                externalPriceId: response?.id ?? null
            };
        }));
    }

    // create a new category type
    createPlan = async (data: PlanCreateServiceInput): Promise<PlanObjectInteface> => {
        const transaction = await sequelize.transaction();
        try {
            const { name, description, status, prices } = data;
            const languages: LanguageInfo = await this.languageService.getRequestedAndDefaultLanguage();
            const planType = this.resolvePlanType(prices);
            let planObj: PlanObject = { code: "-", status: status, type: planType };
            planObj.status = planObj.status ?? PLAN.STATUS.ACTIVE;
            let planContentObj: PlanContentObject = { name, description };
            const code = Common.slugify(planContentObj.name);
            planContentObj.descriptionText = (planContentObj.description
                ? await Common.convertHtmlToText(planContentObj.description.toString())
                : null) as unknown as Text;
            const doesPlanExistInput: PlanDoExistsByCodeDaoInput = { code };
            const exists = await this.planDao.doExistsByCode(doesPlanExistInput, { transaction });
            if (!exists) {
                planObj.code = code;
                let normalizedPrices = prices;
                if (planObj.type === PLAN.PLAN_TYPE.SUBSCRIPTION) {
                    switch (process.env.PAYMENT_GATEWAY) {
                        case "stripe": {
                            const stripeProduct = await this.stripeService.createProduct(planContentObj.name);
                            if (stripeProduct) {
                                for (let item of prices) {
                                    await this.stripeService.createPrice(
                                        stripeProduct,
                                        item.amount,
                                        item.currency,
                                        this.getStripeInterval(item.frequency),
                                        item.duration
                                    );
                                }
                            }
                            break;
                        }
                        case "razorpay": {
                            normalizedPrices = await this.syncSubscriptionPricesWithRazorpay(planContentObj.name, planContentObj.description as any, prices);
                            break;
                        }
                        default:
                            break;
                    }
                }

                const createPlanInput: PlanCreateDaoInput = { planObj, planContentObj, prices: normalizedPrices, languages };
                let planIdentifier: number = await this.planDao.create(createPlanInput, { transaction });
                const getPlanByIdInput: PlanGetByIdDaoInput = { id: planIdentifier };
                let plan: PlanObjectInteface = await this.planDao.getPlanById(getPlanByIdInput, { transaction });
                await transaction.commit();
                return plan;
            }
            throw new AppError(400, "PLAN_ALREADY_EXISTS", { name: "PLAN_ALREADY_EXISTS" });
        } catch (err) {
            await transaction.rollback();
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // update the existing category type
    updatePlan = async (data: PlanUpdateServiceInput): Promise<PlanObjectInteface> => {
        const transaction = await sequelize.transaction();
        try {
            const { id, name, description, status, prices } = data;
            const languages: LanguageInfo = await this.languageService.getRequestedLanguage();
            const planType = this.resolvePlanType(prices);
            let planObj: PlanObject = { code: "-", status: status, type: planType };
            planObj.status = planObj.status ?? PLAN.STATUS.ACTIVE;
            let planContentObj: PlanContentObject = { name, description };
            const code = languages.requested.code == process.env.DEFAULT_LANGUAGE_CODE ? (planObj.code = Common.slugify(planContentObj.name)) : null;
            planContentObj.descriptionText = (planContentObj.description
                ? await Common.convertHtmlToText(planContentObj.description.toString())
                : null) as unknown as Text;
            const doesPlanExistInput: PlanDoExistsByIdDaoInput = { id };
            const exists = await this.planDao.doExistsById(doesPlanExistInput, { transaction });
            if (exists) {
                if (code) {
                    const codeInUseInput: PlanDoExistsByCodeDaoInput = { code, excludeId: id };
                    let codeInUse = await this.planDao.doExistsByCode(codeInUseInput, { transaction });
                    if (codeInUse) {
                        throw new AppError(400, "PLAN_ALREADY_EXISTS", { name: "PLAN_ALREADY_EXISTS" });
                    }
                }

                let normalizedPrices = prices;
                if (planObj.type === PLAN.PLAN_TYPE.SUBSCRIPTION) {
                    switch (process.env.PAYMENT_GATEWAY) {
                        case "stripe": {
                            const stripeProduct = await this.stripeService.createProduct(planContentObj.name);
                            if (stripeProduct) {
                                for (let item of prices) {
                                    await this.stripeService.createPrice(
                                        stripeProduct,
                                        item.amount,
                                        item.currency,
                                        this.getStripeInterval(item.frequency),
                                        item.duration
                                    );
                                }
                            }
                            break;
                        }
                        case "razorpay": {
                            normalizedPrices = await this.syncSubscriptionPricesWithRazorpay(planContentObj.name, planContentObj.description as any, prices);
                            break;
                        }
                        default:
                            break;
                    }
                }

                const updatePlanInput: PlanUpdateDaoInput = { id, planObj, planContentObj, prices: normalizedPrices, languages };
                await this.planDao.update(updatePlanInput, { transaction });
                const getPlanByIdInput: PlanGetByIdDaoInput = { id };
                let plan: PlanObjectInteface = await this.planDao.getPlanById(getPlanByIdInput, { transaction });
                await transaction.commit();
                return plan;
            }
            throw new AppError(404, "PLAN_NOT_FOUND", { name: "PLAN_NOT_FOUND" });
        } catch (err) {
            await transaction.rollback();
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // delete the existing category type
    deletePlan = async (data: PlanDeleteServiceInput): Promise<PlanObjectInteface> => {
        const transaction = await sequelize.transaction();
        try {
            const deletePlanInput: PlanDeleteDaoInput = { id: data.id };
            await this.planDao.delete(deletePlanInput, { transaction });
            const getPlanByIdInput: PlanGetByIdDaoInput = { id: data.id, expanded: true, paranoid: false };
            let plan: PlanObjectInteface = await this.planDao.getPlanById(getPlanByIdInput, { transaction });
            await transaction.commit();
            return plan;
        } catch (err) {
            await transaction.rollback();
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // get category Type by id
    getById = async (data: PlanGetByIdServiceInput): Promise<PlanObjectInteface> => {
        try {
            const getPlanByIdInput: PlanGetByIdDaoInput = { id: data.id, expanded: data.expanded ?? true };
            let plan: PlanObjectInteface = await this.planDao.getPlanById(getPlanByIdInput);
            return plan;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // get category Type by code
    getByCode = async (data: PlanGetByCodeServiceInput): Promise<PlanObjectInteface> => {
        try {
            const getPlanByCodeInput: PlanGetByCodeDaoInput = { code: data.code, expanded: data.expanded ?? true };
            let plan: PlanObjectInteface = await this.planDao.getPlanByCode(getPlanByCodeInput);
            return plan;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // list category types
    getPlans = async (data: PlanGetPlansServiceInput): Promise<PlanPaginatedList> => {
        try {
            const { listRequest } = data;
            const { page, perPage } = listRequest;
            const getPlanListInput: PlanGetPlanListDaoInput = { listRequest };
            let plans: PlanPaginatedData = await this.planDao.getPlanList(getPlanListInput);
            let totalPages = Common.getTotalPages(plans.count, perPage);
            return {
                data: plans.rows,
                page: page,
                perPage: perPage,
                totalRecords: plans.count,
                totalPages: totalPages,
            } as unknown as PlanPaginatedList;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // list all category types
    getAllPlans = async (data: PlanGetAllPlansServiceInput): Promise<PlanObjectSummaryInteface[]> => {
        try {
            const getAllPlansInput: PlanGetAllPlansDaoInput = { listRequest: data.listRequest };
            let plans: PlanObjectSummaryInteface[] = await this.planDao.getAllPlans(getAllPlansInput);
            return plans;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // list category types revisions
    getPlansRevisions = async (data: PlanGetPlansRevisionsServiceInput): Promise<PlanPaginatedList> => {
        try {
            const { id, listRequest, language = process.env.DEFAULT_LANGUAGE_CODE! } = data;
            const { page, perPage } = listRequest;
            const getPlanRevisionListInput: PlanGetPlanRevisionListDaoInput = { id, listRequest, language };
            let plans: PlanPaginatedData = await this.planDao.getPlanRevisionList(getPlanRevisionListInput);
            let totalPages = Common.getTotalPages(plans.count, perPage);
            return {
                data: plans.rows,
                page: page,
                perPage: perPage,
                totalRecords: plans.count,
                totalPages: totalPages,
            } as unknown as PlanPaginatedList;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // Restore category type revision
    restoreRevision = async (data: PlanRestoreRevisionServiceInput): Promise<PlanObjectInteface> => {
        const transaction = await sequelize.transaction();
        try {
            const restoreRevisionInput: PlanRestoreRevisionDaoInput = { id: data.id };
            let restoredEntiryId: number = await this.planDao.restoreRevision(restoreRevisionInput, { transaction });
            const getPlanByIdInput: PlanGetByIdDaoInput = { id: restoredEntiryId };
            let plan = await this.planDao.getPlanById(getPlanByIdInput, { transaction });
            await transaction.commit();
            return plan;
        } catch (err) {
            await transaction.rollback();
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // get category type id from code
    getPlanId = async (data: PlanGetPlanIdServiceInput): Promise<number> => {
        try {
            const getIdFromCodeInput: PlanGetIdFromCodeDaoInput = { code: data.code };
            let categoryId = await this.planDao.getIdFromCode(getIdFromCodeInput);
            return categoryId;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // update category type sort order
    setSortOrder = async (data: PlanSetSortOrderServiceInput): Promise<boolean> => {
        const transaction = await sequelize.transaction();
        try {
            const setSortOrderInput: PlanSetSortOrderDaoInput = { id: data.id, before: data.before, after: data.after };
            let sortOrder: boolean = await this.planDao.setSortOrder(setSortOrderInput, { transaction });
            await transaction.commit();
            return sortOrder;
        } catch (err) {
            await transaction.rollback();
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // update questionnaire status
    updateStatus = async (data: PlanUpdateStatusServiceInput): Promise<PlanObjectInteface> => {
        try {
            const getPlanByIdInput: PlanGetByIdDaoInput = { id: data.id };
            let plan = await this.planDao.getPlanById(getPlanByIdInput);
            if (plan) {
                const updateStatusInput: PlanUpdateStatusDaoInput = { id: data.id, status: data.status };
                await this.planDao.updateStatus(updateStatusInput);
                let planObject = await this.planDao.getPlanById(getPlanByIdInput);
                return planObject;
            } else {
                throw new AppError(404, "PLAN_NOT_FOUND", { id: "PLAN_NOT_FOUND" });
            }
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // update plan price status
    updatePriceStatus = async (data: PlanUpdatePriceStatusServiceInput): Promise<PlanObjectInteface> => {
        try {
            const getExternalPriceByIdInput: PlanGetExternalPriceByIdDaoInput = { id: data.id };
            let price = await this.planDao.getExternalPriceById(getExternalPriceByIdInput);
            if (price) {
                const updatePriceStatusInput: PlanUpdatePriceStatusDaoInput = { id: data.id, status: data.status };
                await this.planDao.updatePriceStatus(updatePriceStatusInput);
                const getPlanByIdInput: PlanGetByIdDaoInput = { id: data.planId };
                let planObject = await this.planDao.getPlanById(getPlanByIdInput);
                return planObject;
            } else {
                throw new AppError(404, "PRICE_NOT_FOUND", { id: "PRICE_NOT_FOUND" });
            }
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };

    // add prices to the stripe plan
    createPrice = async (data: PlanCreatePriceServiceInput): Promise<PlanObjectInteface> => {
        try {
            const getPlanByIdInput: PlanGetByIdDaoInput = { id: data.planId };
            const planInfo = await this.planDao.getPlanById(getPlanByIdInput);
            let price: PriceInterface = {
                amount: data.amount,
                duration: data.duration,
                frequency: data.frequency,
                autoRenew: data.autoRenew ?? 1,
                currency: data.currency,
                planId: data.planId,
            };



            const transaction = await sequelize.transaction();
            try {
                const createPriceInput: PlanCreatePriceDaoInput = { price };
                await this.planDao.createPrice(createPriceInput, { transaction });
                const updatedPlan = await this.planDao.getPlanById(getPlanByIdInput, { transaction });
                await transaction.commit();
                return updatedPlan;
            } catch (err) {
                await transaction.rollback();
                throw err;
            }
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };
}
