import {
    BadRequestException,
    forwardRef,
    Inject,
    Injectable,
    InternalServerErrorException,
    Logger,
    NotFoundException,
} from "@nestjs/common";
import { DataSource } from "typeorm";
import { PaymentEntity } from "./entities/payment.entity";
import { PaymentRepository } from "./payment.repository";
import { PaymentClientService } from "./payment-client.service";

import * as fs from "fs";
import * as path from "path";
import * as Handlebars from "handlebars";
import { Booking } from "../bookings/entities/booking.entity";

import {
    ClientAction,
    ProviderPaymentStatus,
} from "@edr/payment-providers";
import {
    PaymentService as PaymentServiceEnum,
    PaymentReferenceType,
    PaymentIntentSnapshot,
    ProviderMethod,
} from "@edr/types";
import {
    InitiatePaymentDto,
    InitiateResponseDto,
    IntentStatusDto,
    RefundDto,
} from "./payments.dto";
import { BookingBatchService } from "../train-scheduling/booking-batch.service";
import { DropdownSettingsService } from "../dropdown-settings/dropdown-settings.service";

/** Setting code holding the global ordering window (months) for general contracts. */
const CONTRACT_PERIOD_SETTING_CODE = "general_contract_period";
const DEFAULT_CONTRACT_PERIOD_MONTHS = 3;

const STATUS_MAP: Record<string, ProviderPaymentStatus> = {
    "action-required": ProviderPaymentStatus.REQUIRES_ACTION,
    "processing": ProviderPaymentStatus.PROCESSING,
    "success": ProviderPaymentStatus.SUCCEEDED,
    "failed": ProviderPaymentStatus.FAILED,
    "canceled": ProviderPaymentStatus.CANCELLED,
    "refunded": ProviderPaymentStatus.CANCELLED,
};

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

    constructor(
        private readonly datasource: DataSource,
        private readonly paymentRepo: PaymentRepository,
        private readonly paymentClient: PaymentClientService,
        @Inject(forwardRef(() => BookingBatchService))
        private readonly bookingBatchService: BookingBatchService,
        private readonly dropdownSettings: DropdownSettingsService,
    ) { }

    /** Configured general-contract ordering window in months (defaults to 3). */
    private async contractPeriodMonths(): Promise<number> {
        try {
            const setting = await this.dropdownSettings.getByCode(
                CONTRACT_PERIOD_SETTING_CODE,
            );
            const months = Number(setting.children?.[0]?.value);
            if (Number.isFinite(months) && months > 0) return months;
        } catch {
            // Setting not seeded — fall back to the default.
        }
        return DEFAULT_CONTRACT_PERIOD_MONTHS;
    }

    async getAll(filters: {
        search?: string;
        status?: string;
        method?: string;
        page?: number;
        pageSize?: number;
    }) {
        const { search, status, method, page = 1, pageSize = 10 } = filters;
        const skip = (page - 1) * pageSize;

        const qb = this.paymentRepo.createQueryBuilder("payment");

        if (search) {
            qb.andWhere(
                "(payment.merchantOrderId ILIKE :search OR payment.refId ILIKE :search OR payment.transactionId ILIKE :search)",
                { search: `%${search}%` },
            );
        }
        if (status) {
            qb.andWhere("payment.status = :status", { status });
        }
        if (method) {
            qb.andWhere("payment.method = :method", { method });
        }

        const [items, total] = await qb
            .orderBy("payment.createdAt", "DESC")
            .skip(skip)
            .take(pageSize)
            .getManyAndCount();

        return {
            items: items.map((p) => ({
                id: p.id,
                bookingId: p.refId,
                amount: p.amount,
                currency: p.currency,
                method: p.method,
                status: p.status,
                merchantOrderId: p.merchantOrderId,
                paidAt: p.paidAt,
                createdAt: p.createdAt,
            })),
            total,
            page,
            pageSize,
        };
    }

    /** Aggregate counts across ALL payments for the dashboard summary cards. */
    async getSummary() {
        const rows = await this.paymentRepo
            .createQueryBuilder("payment")
            .select("payment.status", "status")
            .addSelect("COUNT(*)::int", "count")
            .groupBy("payment.status")
            .getRawMany<{ status: string; count: number }>();

        const byStatus: Record<string, number> = {};
        let total = 0;
        for (const row of rows) {
            byStatus[row.status] = row.count;
            total += row.count;
        }

        // Sum of successfully collected amounts.
        const paidAgg = await this.paymentRepo
            .createQueryBuilder("payment")
            .select("COALESCE(SUM(payment.amount), 0)", "sum")
            .where("payment.status = :status", { status: "success" })
            .getRawOne<{ sum: string }>();

        return {
            total,
            success: byStatus["success"] ?? 0,
            processing:
                (byStatus["processing"] ?? 0) + (byStatus["action-required"] ?? 0),
            failed: (byStatus["failed"] ?? 0) + (byStatus["canceled"] ?? 0),
            refunded: byStatus["refunded"] ?? 0,
            paidAmount: Number(paidAgg?.sum ?? 0),
        };
    }

    async initiatePayment(dto: InitiatePaymentDto): Promise<InitiateResponseDto> {
        const booking = await this.datasource
            .getRepository(Booking)
            .findOneBy({ id: dto.bookingId });
        if (!booking) throw new NotFoundException("Booking not found");

        const amountMinor = Math.round(Number(booking.totalAmount));

        const snapshot = await this.paymentClient.initiate({
            service: PaymentServiceEnum.FREIGHT,
            referenceType: PaymentReferenceType.SHIPMENT,
            referenceId: booking.id,
            orderRef: booking.reference,
            amountMinor,
            currency: booking.paymentCurrency,
            provider: dto.method as unknown as ProviderMethod,
            platform: dto.platform,
            payerAccount: dto.payerAccount,
            returnUrl:'https://edrfreight.triaplc.com/payment/success',
            failureUrl: 'https://edrfreight.triaplc.com/payment/failure',
        });

        const intent = await this.syncIntentProjection(booking.id, booking, snapshot);

        if (snapshot.status === ProviderPaymentStatus.SUCCEEDED) {
            await this.finalizePaymentSuccess({
                intentId: intent.id,
                bookingId: booking.id,
                providerTxnId: snapshot.providerTxnId,
                paidAt: snapshot.paidAt ? new Date(snapshot.paidAt) : undefined,
            });
        }

        return this.formatIntentResponse(intent);
    }

    private async syncIntentProjection(
        bookingId: string,
        booking: Booking,
        snapshot: PaymentIntentSnapshot,
    ): Promise<PaymentEntity> {
        const existing = await this.paymentRepo.findOneBy({ refId: bookingId, type: "booking" });

        const PROVIDER_TO_METHOD: Record<string, PaymentEntity["method"]> = {
            TELEBIRR: "telebirr",
            CBE_BIRR: "cbe-birr",
            EBIRR: "ebirr",
            WAAFI: "waafi",
            CARD: "card",
            DMONEY: "dmoney",
            CAC_BANK: "cac-bank",
        };
        const method: PaymentEntity["method"] =
            PROVIDER_TO_METHOD[snapshot.provider ?? ""] ?? "telebirr";
        const status = snapshot.status === ProviderPaymentStatus.SUCCEEDED
            ? "processing"
            : this.toLocalStatus(snapshot.status);

        const clientAction = (snapshot.clientAction ?? undefined) as Record<string, unknown> | undefined;
        const data = {
            status,
            method,
            merchantOrderId: snapshot.merchantOrderId ?? existing?.merchantOrderId ?? "",
            transactionId: snapshot.providerTxnId ?? existing?.transactionId,
            expiresAt: snapshot.expiresAt ? new Date(snapshot.expiresAt) : existing?.expiresAt,
            failerCode: snapshot.failureCode ?? undefined,
            failureMessage: snapshot.failureMessage ?? undefined,
        };

        if (existing) {
            await this.paymentRepo.update({ id: existing.id }, { ...data, clientAction } as any);
            return { ...existing, ...data, clientAction } as PaymentEntity;
        }

        return this.paymentRepo.create({
            refId: bookingId,
            type: "booking",
            amount: booking.totalAmount,
            currency: booking.paymentCurrency,
            reason: `Payment for booking ${booking.reference}`,
            rawInitiation: snapshot as unknown as Record<string, unknown>,
            clientAction: clientAction ?? {},
            ...data,
        } as any);
    }

    async getIntentByBookingId(bookingId: string): Promise<IntentStatusDto> {
        const local = await this.paymentRepo.findOneBy({ refId: bookingId, type: "booking" });

        let snapshot: PaymentIntentSnapshot | null = null;
        try {
            snapshot = await this.paymentClient.getIntentByReference(
                PaymentReferenceType.SHIPMENT,
                bookingId,
            );
        } catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            this.logger.warn(
                `payment service lookup failed for booking ${bookingId}: ${message}; using local intent`,
            );
        }

        if (!snapshot) {
            if (!local) throw new NotFoundException("PaymentIntent not found");
            return this.formatIntentStatus(local);
        }

        const booking = await this.datasource
            .getRepository(Booking)
            .findOneBy({ id: bookingId });

        if (!booking) throw new NotFoundException("Booking not found");

        const intent = await this.syncIntentProjection(bookingId, booking, snapshot);

        if (snapshot.status === ProviderPaymentStatus.SUCCEEDED) {
            await this.finalizePaymentSuccess({
                intentId: intent.id,
                bookingId: booking.id,
                providerTxnId: snapshot.providerTxnId,
                paidAt: snapshot.paidAt ? new Date(snapshot.paidAt) : undefined,
            });
        }

        const refreshed = await this.paymentRepo.findOneBy({ id: intent.id });
        return this.formatIntentStatus(refreshed ?? intent);
    }

    async refund(dto: RefundDto) {
        const intent = await this.paymentRepo.findOneBy({ refId: dto.bookingId, type: "booking" });
        if (!intent || intent.status !== "success") {
            throw new BadRequestException("No successful payment to refund");
        }

        await this.datasource.transaction(async (mg) => {
            await mg.update(PaymentEntity, { id: intent.id }, { status: "refunded", refundedAt: new Date() });
            await mg.update(Booking, { id: dto.bookingId }, { paymentStatus: "FAILED", status: "CANCELLED" });
        });

        return { refunded: true, bookingId: dto.bookingId };
    }

    async finalizePaymentSuccess(input: {
        intentId: string;
        bookingId: string;
        providerTxnId?: string;
        paidAt?: Date;
    }): Promise<{ alreadyFinalized: boolean }> {
        const intent = await this.paymentRepo.findOneBy({ id: input.intentId });
        if (!intent) throw new NotFoundException("PaymentIntent not found");
        if (intent.status === "success") return { alreadyFinalized: true };

        const paidAt = input.paidAt ?? new Date();

        // A general contract is paid once, up front; it does NOT enter the train
        // queue (nothing has been ordered yet). Instead it becomes ACTIVE and
        // opens its ordering window. Orders placed later spawn their own paid
        // child bookings that go through the normal pipeline.
        const booking = await this.datasource
            .getRepository(Booking)
            .findOne({ where: { id: input.bookingId } });
        const isGeneralContract = booking?.bookingType === "GENERAL_CONTRACT";

        let contractExpiresAt: Date | null = null;
        if (isGeneralContract) {
            const months = await this.contractPeriodMonths();
            contractExpiresAt = new Date(paidAt);
            contractExpiresAt.setMonth(contractExpiresAt.getMonth() + months);
        }

        await this.datasource.transaction(async (mg) => {
            await mg.update(
                PaymentEntity,
                { id: intent.id },
                { status: "success", paidAt, transactionId: input.providerTxnId ?? intent.transactionId },
            );
            await mg.update(
                Booking,
                { id: input.bookingId },
                isGeneralContract
                    ? { paymentStatus: "PAID", status: "CONTRACT_ACTIVE", expiresAt: contractExpiresAt }
                    : { paymentStatus: "PAID", status: "PAID" },
            );
        });

        if (isGeneralContract) {
            this.logger.log(
                `General contract ${booking?.reference ?? input.bookingId} ACTIVE — ordering open until ${contractExpiresAt?.toISOString()}`,
            );
            return { alreadyFinalized: false };
        }

        try {
            await this.bookingBatchService.ensurePaidBookingAllocated(input.bookingId);
        } catch (err) {
            this.logger.error(
                `Error allocating booking after payment: ${err instanceof Error ? err.message : String(err)}`,
            );
        }

        return { alreadyFinalized: false };
    }

    async markPaymentFailed(input: {
        intentId: string;
        failureCode?: string;
        failureMessage?: string;
    }): Promise<void> {
        const intent = await this.paymentRepo.findOneBy({ id: input.intentId });
        if (!intent) throw new NotFoundException("PaymentIntent not found");
        if (intent.status === "success" || intent.status === "canceled") return;

        await this.paymentRepo.update(
            { id: intent.id },
            { status: "failed", failerCode: input.failureCode, failureMessage: input.failureMessage },
        );
    }

    async getActivePaymentByOrderIdAndMethod(orderId: string, method: PaymentEntity["method"]): Promise<PaymentEntity | null> {
        return this.paymentRepo.getActivePaymentByOrderIdAndMethod(orderId, method);
    }

    async genReceiptHtml(orderId: string) {
        const payment = await this.paymentRepo.findOneBy({ merchantOrderId: orderId, status: "success" });
        if (!payment) throw new BadRequestException("No successful payment found for this order");

        const filePath = path.join(__dirname, "templates", "receipt.hbs");
        if (!fs.existsSync(filePath)) throw new InternalServerErrorException();

        const source = fs.readFileSync(filePath, "utf8");
        const template = Handlebars.compile(source);
        return template({
            vendorName: "Ethio Djibouti Railway Freight Booking",
            vendorAddress: "Addis Ababa",
            receiptDate: payment.paidAt,
            paymentMethod: payment.method,
            subtotal: payment.amount.toString(),
            total: payment.amount.toString(),
            currency: payment.currency,
            reason: payment.reason,
        });
    }

    findBookingById(id: string) {
        return this.paymentRepo.findOneBy({ refId: id, type: "booking" });
    }

    formatIntentResponse(intent: PaymentEntity): InitiateResponseDto {
        const clientAction =
            intent.clientAction && typeof intent.clientAction === "object"
                ? (intent.clientAction as unknown as ClientAction)
                : undefined;
        return {
            intentId: intent.id,
            status: STATUS_MAP[intent.status] ?? ProviderPaymentStatus.PROCESSING,
            clientAction,
            merchantOrderId: intent.merchantOrderId ?? undefined,
        };
    }

    private formatIntentStatus(intent: PaymentEntity): IntentStatusDto {
        return {
            ...this.formatIntentResponse(intent),
            paidAt: intent.paidAt?.toISOString(),
            failureCode: intent.failerCode ?? undefined,
            failureMessage: intent.failureMessage ?? undefined,
        };
    }

    async handlePaymentEvent(event: {
        eventType: string;
        eventId: string;
        referenceId: string;
        intentId: string;
        providerTxnId?: string;
        paidAt?: string;
        failureCode?: string;
        failureMessage?: string;
    }): Promise<{ processed: boolean; alreadyFinalized?: boolean; reason?: string }> {
        if (event.eventType === "payment.succeeded") {
            const intent = await this.paymentRepo.findOneBy({ refId: event.referenceId, type: "booking" });
            if (!intent) {
                return { processed: false, reason: `No local intent for booking ${event.referenceId}` };
            }
            const { alreadyFinalized } = await this.finalizePaymentSuccess({
                intentId: intent.id,
                bookingId: event.referenceId,
                providerTxnId: event.providerTxnId,
                paidAt: event.paidAt ? new Date(event.paidAt) : undefined,
            });
            return { processed: true, alreadyFinalized };
        }

        if (event.eventType === "payment.failed") {
            const intent = await this.paymentRepo.findOneBy({ refId: event.referenceId, type: "booking" });
            if (!intent) {
                return { processed: false, reason: `No local intent for booking ${event.referenceId}` };
            }
            await this.markPaymentFailed({
                intentId: intent.id,
                failureCode: event.failureCode,
                failureMessage: event.failureMessage,
            });
            return { processed: true };
        }

        return { processed: false, reason: `Unknown event type: ${event.eventType}` };
    }

    private toLocalStatus(status: ProviderPaymentStatus): PaymentEntity["status"] {
        switch (status) {
            case ProviderPaymentStatus.SUCCEEDED: return "success";
            case ProviderPaymentStatus.FAILED: return "failed";
            case ProviderPaymentStatus.CANCELLED: return "canceled";
            case ProviderPaymentStatus.PROCESSING: return "processing";
            default: return "action-required";
        }
    }

    async findByCompanyId(companyId: string) {
        return this.paymentRepo.findByCompanyId(companyId);
    }
}
