import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../database/prisma.service';
import { EasebuzzService } from './easebuzz.service';
import { Prisma, WalletTxnStatus, WalletTxnType } from '@prisma/client';
import { ConfigService } from '@nestjs/config';

type PlanPurchaseMeta = {
  shopId: string;
  planCode: string;
};

const PLAN_PERIOD_DAYS = 30;
function addDays(d: Date, days: number) {
  const ms = Math.max(0, Math.floor(Number(days))) * 24 * 60 * 60 * 1000;
  return new Date(d.getTime() + ms);
}

@Injectable()
export class WalletService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly easebuzz: EasebuzzService,
    private readonly config: ConfigService,
  ) {}

  private publicUrl() {
    const raw =
      this.config.get<string>('payments.publicUrl') ?? 'http://localhost:4000';
    return raw.replace(/\/+$/, '');
  }

  private normalizeOrigin(raw: string | null | undefined) {
    const v = (raw ?? '').toString().trim();
    if (!v) return null;
    try {
      return new URL(v).origin.replace(/\/+$/, '');
    } catch {
      return null;
    }
  }

  private easebuzzReturnUrl(portalPublicUrl?: string | null) {
    const portalOrigin = this.normalizeOrigin(portalPublicUrl);
    const cfgOrigin = this.normalizeOrigin(this.publicUrl());
    const chosen = portalOrigin || cfgOrigin || null;
    if (!chosen) {
      throw new BadRequestException(
        'Invalid payment callback URL configuration.',
      );
    }
    return `${chosen}/api/easebuzz/return`;
  }

  private easebuzzEmailFromUser(raw: string | null | undefined) {
    const email = (raw ?? '').toString().trim().toLowerCase();
    return email || 'support@cashi.com';
  }

  private portalOrigin(raw: string | null | undefined) {
    return this.normalizeOrigin(raw) ?? '';
  }

  private easebuzzPhoneFromUser(raw: string | null | undefined) {
    const digits = (raw ?? '').toString().replace(/\D/g, '');
    // Easebuzz expects a local 10-digit number (no country code).
    const local10 = digits.length >= 10 ? digits.slice(-10) : '';
    return local10 || '9999999999';
  }

  async getWalletSummaryForShop(args: { shopId: string; take: number }) {
    const shop = await this.prisma.shop.findUnique({
      where: { id: args.shopId },
      select: { id: true, walletBalance: true, updatedAt: true },
    });
    if (!shop) throw new NotFoundException('Shop not found');

    const txns = await this.prisma.walletTransaction.findMany({
      where: { shopId: shop.id },
      orderBy: { createdAt: 'desc' },
      take: Math.min(Math.max(Math.floor(args.take ?? 10), 1), 50),
      select: {
        id: true,
        type: true,
        status: true,
        amount: true,
        currency: true,
        purpose: true,
        referenceId: true,
        meta: true,
        createdAt: true,
      },
    });

    return {
      shopId: shop.id,
      balance: shop.walletBalance,
      currency: 'INR',
      updatedAt: shop.updatedAt,
      recentTransactions: txns,
    };
  }

  async listWalletTransactionsForShop(args: {
    shopId: string;
    page: number;
    limit: number;
  }) {
    const page = Math.max(1, Math.floor(args.page || 1));
    const limit = Math.min(100, Math.max(1, Math.floor(args.limit || 20)));
    const skip = (page - 1) * limit;

    const [total, items] = await Promise.all([
      this.prisma.walletTransaction.count({ where: { shopId: args.shopId } }),
      this.prisma.walletTransaction.findMany({
        where: { shopId: args.shopId },
        orderBy: { createdAt: 'desc' },
        skip,
        take: limit,
        select: {
          id: true,
          type: true,
          status: true,
          amount: true,
          currency: true,
          purpose: true,
          referenceId: true,
          meta: true,
          createdAt: true,
        },
      }),
    ]);

    return { page, limit, total, items };
  }

  async createWalletTopupCheckout(args: {
    userId: string;
    shopId: string;
    amountToTopupInr: number;
    portalPublicUrl?: string | null;
  }) {
    const requestedAmount = Math.max(
      0,
      Math.floor(Number(args.amountToTopupInr ?? 0)),
    );
    if (requestedAmount <= 0)
      throw new BadRequestException('Invalid topup amount');
    const amountToTopupInr = requestedAmount;

    const user = await this.prisma.user.findUnique({
      where: { id: args.userId },
      select: { id: true, email: true, phone: true },
    });
    if (!user) throw new NotFoundException('User not found');

    // Easebuzz/PGs often require txnid to be <=25 chars and alphanumeric only.
    const txnid = (() => {
      const a = Date.now().toString(36);
      const b = Math.random().toString(36).slice(2, 10);
      const raw = `tx${a}${b}`.replace(/[^a-z0-9]/gi, '');
      return raw.slice(0, 25);
    })();
    const returnUrl = this.easebuzzReturnUrl(args.portalPublicUrl);
    const portal = this.portalOrigin(args.portalPublicUrl);
    const email = this.easebuzzEmailFromUser(user.email);
    const firstname = email.split('@')[0] || 'Cashi';
    const phone = this.easebuzzPhoneFromUser(user.phone);
    const gatewayRequest = {
      txnid,
      amount: amountToTopupInr.toFixed(2),
      productinfo: 'Cashi Wallet Topup',
      firstname,
      email,
      phone,
      surl: returnUrl,
      furl: returnUrl,
      udf: {
        udf2: args.shopId,
        udf3: '',
        udf4: user.id,
        udf5: 'WALLET_TOPUP',
        udf6: '',
      },
    };

    const attempt = await this.prisma.paymentAttempt.create({
      data: {
        txnid,
        shopId: args.shopId,
        userId: user.id,
        amount: amountToTopupInr,
        currency: 'INR',
        purpose: 'WALLET_TOPUP',
        rawRequest: {
          amountToTopupInr,
          portalPublicUrl: args.portalPublicUrl ?? null,
          gatewayRequest,
        } as any,
        status: WalletTxnStatus.PENDING,
      },
      select: { id: true, txnid: true },
    });

    try {
      const { checkoutUrl, raw } = await this.easebuzz.initiatePaymentLink({
        txnid,
        amountInr: amountToTopupInr,
        productinfo: gatewayRequest.productinfo,
        firstname,
        email,
        phone,
        surl: returnUrl,
        furl: returnUrl,
        udf: {
          udf1: attempt.id, // attemptId
          ...gatewayRequest.udf,
        },
      });

      await this.prisma.paymentAttempt.update({
        where: { id: attempt.id },
        data: { rawResponse: raw },
        select: { id: true },
      });

      return { checkoutUrl, attemptId: attempt.id, txnid };
    } catch (e: any) {
      const exceptionResponse =
        typeof e?.getResponse === 'function' ? e.getResponse() : null;
      await this.prisma.paymentAttempt.update({
        where: { id: attempt.id },
        data: {
          status: WalletTxnStatus.FAILED,
          rawResponse: {
            error: e?.message ?? 'Easebuzz initiate failed',
            exceptionResponse,
          } as any,
        },
        select: { id: true },
      });
      throw e;
    }
  }

  async createPlanPurchaseCheckout(args: {
    adminUserId: string;
    shopId: string;
    planCode: string;
    amountToTopupInr: number;
    portalPublicUrl?: string | null;
  }) {
    const requestedAmount = Math.max(
      0,
      Math.floor(Number(args.amountToTopupInr ?? 0)),
    );
    if (requestedAmount <= 0)
      throw new BadRequestException('Invalid topup amount');
    const amountToTopupInr = requestedAmount;

    const user = await this.prisma.user.findUnique({
      where: { id: args.adminUserId },
      select: { id: true, email: true, phone: true },
    });
    if (!user) throw new NotFoundException('User not found');

    // Easebuzz/PGs often require txnid to be <=25 chars and alphanumeric only.
    const txnid = (() => {
      const a = Date.now().toString(36);
      const b = Math.random().toString(36).slice(2, 10);
      const raw = `tx${a}${b}`.replace(/[^a-z0-9]/gi, '');
      return raw.slice(0, 25);
    })();
    const returnUrl = this.easebuzzReturnUrl(args.portalPublicUrl);
    const portal = this.portalOrigin(args.portalPublicUrl);
    const email = this.easebuzzEmailFromUser(user.email);
    const firstname = email.split('@')[0] || 'Cashi';
    const phone = this.easebuzzPhoneFromUser(user.phone);
    const gatewayRequest = {
      txnid,
      amount: amountToTopupInr.toFixed(2),
      productinfo: `Cashi Plan ${String(args.planCode).toUpperCase()}`,
      firstname,
      email,
      phone,
      surl: returnUrl,
      furl: returnUrl,
      udf: {
        udf2: args.shopId,
        udf3: args.planCode,
        udf4: user.id,
        udf5: 'PLAN_PURCHASE',
        udf6: '',
      },
    };
    const attempt = await this.prisma.paymentAttempt.create({
      data: {
        txnid,
        shopId: args.shopId,
        userId: user.id,
        amount: amountToTopupInr,
        currency: 'INR',
        purpose: 'PLAN_PURCHASE',
        planCode: args.planCode,
        rawRequest: {
          planCode: args.planCode,
          amountToTopupInr,
          portalPublicUrl: args.portalPublicUrl ?? null,
          gatewayRequest,
        } as any,
        status: WalletTxnStatus.PENDING,
      },
      select: { id: true, txnid: true },
    });

    try {
      const { checkoutUrl, raw } = await this.easebuzz.initiatePaymentLink({
        txnid,
        amountInr: amountToTopupInr,
        productinfo: gatewayRequest.productinfo,
        firstname,
        email,
        phone,
        surl: returnUrl,
        furl: returnUrl,
        udf: {
          udf1: attempt.id, // attemptId
          ...gatewayRequest.udf,
        },
      });

      await this.prisma.paymentAttempt.update({
        where: { id: attempt.id },
        data: { rawResponse: raw },
        select: { id: true },
      });

      return { checkoutUrl, attemptId: attempt.id, txnid };
    } catch (e: any) {
      const exceptionResponse =
        typeof e?.getResponse === 'function' ? e.getResponse() : null;
      await this.prisma.paymentAttempt.update({
        where: { id: attempt.id },
        data: {
          status: WalletTxnStatus.FAILED,
          rawResponse: {
            error: e?.message ?? 'Easebuzz initiate failed',
            exceptionResponse,
          } as any,
        },
        select: { id: true },
      });
      throw e;
    }
  }

  async applyWalletTopupOnSuccess(args: {
    attemptId: string;
    easebuzzPayload: Record<string, any>;
  }) {
    const attempt = await this.prisma.paymentAttempt.findUnique({
      where: { id: args.attemptId },
      select: {
        id: true,
        status: true,
        shopId: true,
        userId: true,
        amount: true,
        txnid: true,
        purpose: true,
      },
    });
    if (!attempt) throw new NotFoundException('Payment attempt not found');
    if (attempt.status === WalletTxnStatus.SUCCESS) {
      return { ok: true, alreadyProcessed: true };
    }
    if (attempt.purpose !== 'WALLET_TOPUP') {
      throw new BadRequestException('Invalid wallet topup attempt');
    }

    return this.prisma.$transaction(async (tx) => {
      await tx.paymentAttempt.update({
        where: { id: attempt.id },
        data: {
          status: WalletTxnStatus.SUCCESS,
          rawResponse: args.easebuzzPayload as any,
        },
        select: { id: true },
      });

      await tx.walletTransaction.create({
        data: {
          shopId: attempt.shopId,
          userId: attempt.userId,
          type: WalletTxnType.TOPUP,
          status: WalletTxnStatus.SUCCESS,
          amount: attempt.amount,
          currency: 'INR',
          purpose: 'WALLET_TOPUP',
          referenceId: attempt.id,
          meta: { gateway: 'EASEBUZZ', txnid: attempt.txnid } as any,
        },
        select: { id: true },
      });

      await tx.shop.update({
        where: { id: attempt.shopId },
        data: { walletBalance: { increment: attempt.amount } },
        select: { id: true },
      });

      return { ok: true };
    });
  }

  async applyPlanPurchaseOnSuccess(args: {
    attemptId: string;
    easebuzzPayload: Record<string, any>;
  }) {
    const attempt = await this.prisma.paymentAttempt.findUnique({
      where: { id: args.attemptId },
      select: {
        id: true,
        status: true,
        shopId: true,
        userId: true,
        amount: true,
        planCode: true,
        txnid: true,
      },
    });
    if (!attempt) throw new NotFoundException('Payment attempt not found');
    if (attempt.status === WalletTxnStatus.SUCCESS) {
      return { ok: true, alreadyProcessed: true };
    }
    if (!attempt.planCode) throw new BadRequestException('Missing plan code');

    const plan = await this.prisma.plan.findUnique({
      where: { code: attempt.planCode },
      select: { id: true, code: true, priceMonthly: true },
    });
    if (!plan) throw new NotFoundException('Plan not found');

    // Process atomically: mark attempt, credit wallet, debit wallet, update shared subscription.
    return this.prisma.$transaction(async (tx) => {
      const now = new Date();
      const updatedAttempt = await tx.paymentAttempt.update({
        where: { id: attempt.id },
        data: {
          status: WalletTxnStatus.SUCCESS,
          rawResponse: args.easebuzzPayload as any,
        },
        select: { id: true },
      });

      // Credit wallet (topup)
      await tx.walletTransaction.create({
        data: {
          shopId: attempt.shopId,
          userId: attempt.userId,
          type: WalletTxnType.TOPUP,
          status: WalletTxnStatus.SUCCESS,
          amount: attempt.amount,
          currency: 'INR',
          purpose: 'PLAN_TOPUP',
          referenceId: attempt.id,
          meta: { gateway: 'EASEBUZZ', txnid: attempt.txnid } as any,
        },
        select: { id: true },
      });

      const shop = await tx.shop.update({
        where: { id: attempt.shopId },
        data: { walletBalance: { increment: attempt.amount } },
        select: {
          id: true,
          adminId: true,
          walletBalance: true,
          admin: {
            select: {
              adminSubscription: {
                select: {
                  planId: true,
                  planExpiresAt: true,
                },
              },
            },
          },
        },
      });

      // Debit for plan purchase (full plan priceMonthly)
      const price = Math.max(0, Math.floor(Number(plan.priceMonthly ?? 0)));
      if (price > shop.walletBalance) {
        throw new BadRequestException('Wallet balance is still insufficient');
      }

      await tx.walletTransaction.create({
        data: {
          shopId: attempt.shopId,
          userId: attempt.userId,
          type: WalletTxnType.DEBIT,
          status: WalletTxnStatus.SUCCESS,
          amount: price,
          currency: 'INR',
          purpose: 'PLAN_PURCHASE',
          referenceId: attempt.id,
          meta: { planCode: plan.code } as any,
        },
        select: { id: true },
      });

      const base =
        shop.admin.adminSubscription?.planId === plan.id &&
        shop.admin.adminSubscription?.planExpiresAt &&
        shop.admin.adminSubscription.planExpiresAt > now
          ? shop.admin.adminSubscription.planExpiresAt
          : now;
      const nextExpiry = addDays(base, PLAN_PERIOD_DAYS);

      await tx.shop.update({
        where: { id: attempt.shopId },
        data: {
          walletBalance: { decrement: price },
        },
        select: { id: true },
      });

      await tx.adminSubscription.upsert({
        where: { adminId: shop.adminId },
        create: {
          adminId: shop.adminId,
          planId: plan.id,
          planExpiresAt: nextExpiry,
          scheduledPlanCode: null,
          scheduledPlanAt: null,
        },
        update: {
          planId: plan.id,
          planExpiresAt: nextExpiry,
          scheduledPlanCode: null,
          scheduledPlanAt: null,
        },
      });

      return { ok: true, attempt: updatedAttempt };
    });
  }

  async debitWalletForPlanIfEnough(args: {
    adminUserId: string;
    shopId: string;
    planCode: string;
  }) {
    const plan = await this.prisma.plan.findUnique({
      where: { code: args.planCode },
      select: { id: true, code: true, priceMonthly: true },
    });
    if (!plan) throw new NotFoundException('Plan not found');
    const price = Math.max(0, Math.floor(Number(plan.priceMonthly ?? 0)));

    return this.prisma.$transaction(async (tx) => {
      const now = new Date();
      const shop = await tx.shop.findUnique({
        where: { id: args.shopId },
        select: {
          id: true,
          adminId: true,
          walletBalance: true,
          admin: {
            select: {
              adminSubscription: {
                select: {
                  planId: true,
                  planExpiresAt: true,
                },
              },
            },
          },
        },
      });
      if (!shop) throw new NotFoundException('Shop not found');
      if (shop.walletBalance < price) {
        return { ok: false, needed: price - shop.walletBalance, price };
      }

      await tx.walletTransaction.create({
        data: {
          shopId: shop.id,
          userId: args.adminUserId,
          type: WalletTxnType.DEBIT,
          status: WalletTxnStatus.SUCCESS,
          amount: price,
          currency: 'INR',
          purpose: 'PLAN_PURCHASE',
          referenceId: null,
          meta: { planCode: plan.code } as any,
        },
        select: { id: true },
      });

      const nextExpiry = addDays(
        shop.admin.adminSubscription?.planId === plan.id &&
          shop.admin.adminSubscription?.planExpiresAt &&
          shop.admin.adminSubscription.planExpiresAt > now
          ? shop.admin.adminSubscription.planExpiresAt
          : now,
        PLAN_PERIOD_DAYS,
      );

      await tx.shop.update({
        where: { id: shop.id },
        data: {
          walletBalance: { decrement: price },
        },
        select: { id: true },
      });

      await tx.adminSubscription.upsert({
        where: { adminId: shop.adminId },
        create: {
          adminId: shop.adminId,
          planId: plan.id,
          planExpiresAt: nextExpiry,
          scheduledPlanCode: null,
          scheduledPlanAt: null,
        },
        update: {
          planId: plan.id,
          planExpiresAt: nextExpiry,
          scheduledPlanCode: null,
          scheduledPlanAt: null,
        },
      });

      return { ok: true, needed: 0, price };
    });
  }

  async getPaymentAttemptPortalOrigin(attemptId: string) {
    const attempt = await this.prisma.paymentAttempt.findUnique({
      where: { id: attemptId },
      select: { rawRequest: true },
    });
    const rawRequest =
      attempt?.rawRequest && typeof attempt.rawRequest === 'object'
        ? (attempt.rawRequest as Prisma.JsonObject)
        : null;
    const portalPublicUrl = rawRequest?.portalPublicUrl;
    return this.portalOrigin(
      typeof portalPublicUrl === 'string' ? portalPublicUrl : null,
    );
  }
}
