import { Injectable, Logger } from "@nestjs/common"
import Stripe from "stripe"
import { StripeSettingsService } from "../stripe-settings/stripe-settings.service"
import { SubscriptionRepository } from "../subscriptions/repository/subscription.repository"
import { TransactionRepository } from "../transactions/repository/transaction.repository"
import { SubscriptionPlanRepository } from "../subscription-plans/repository/subscription-plan.repository"
import { WebhookLogRepository } from "../webhook-logs/repository/webhook-log.repository"
import { SubscriptionHistoryRepository } from "../subscription-history/repository/subscription-history.repository"
import { AuthService } from "../auth/auth.service"
import { successResponse, failureResponse } from "src/common/response/response"
import { code } from "src/common/response/response.code"
import {
  successMessage,
  errorMessage,
  validationMessage,
  isEmpty,
} from "src/utils/helpers"
import { messageKey } from "src/constants/message-keys"
import { EV } from "src/utils/env.values"
import { Subscription } from "../subscriptions/entities/subscription.entity"
import { Transaction } from "../transactions/entities/transaction.entity"
import { WebhookLog } from "../webhook-logs/entities/webhook-log.entity"
import { SubscriptionHistory } from "../subscription-history/entities/subscription-history.entity"

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

  constructor(
    private readonly stripeSettingsService: StripeSettingsService,
    private readonly subscriptionRepository: SubscriptionRepository,
    private readonly transactionRepository: TransactionRepository,
    private readonly subscriptionPlanRepository: SubscriptionPlanRepository,
    private readonly webhookLogRepository: WebhookLogRepository,
    private readonly subscriptionHistoryRepository: SubscriptionHistoryRepository,
    private readonly authService: AuthService,
  ) {}

  private async getStripeClient(): Promise<Stripe | null> {
    const settings = await this.stripeSettingsService.getActiveStripeKeys()
    if (!settings) return null
    return new Stripe(settings.stripe_secret_key, {
      apiVersion: "2024-06-20",
    })
  }

  async createCheckoutSession(
    token: string,
    planId: string,
    successUrl?: string,
    cancelUrl?: string,
  ) {
    try {
      const stripe = await this.getStripeClient()
      if (!stripe) {
        return failureResponse(
          code.STRIPE_NOT_CONNECTED,
          errorMessage(messageKey.stripe_not_configured),
        )
      }

      const loggedInUser: any = await this.authService.getUserByToken(token)
      if (isEmpty(loggedInUser)) {
        return failureResponse(
          code.VALIDATION,
          validationMessage(messageKey.user_not_found),
        )
      }

      const plan: any = await this.subscriptionPlanRepository.getByParams({
        where: { id: parseInt(planId) },
        findOne: true,
      })

      if (!plan) {
        return failureResponse(
          code.SUCCESS,
          errorMessage(messageKey.data_not_found, { ":data": "Plan" }),
        )
      }

      // Get or find existing subscription to reuse stripe_customer_id
      const existingSub: any = await this.subscriptionRepository.getByParams({
        where: { user_id: loggedInUser.user_id },
        orderBy: { created_at: "DESC" },
        findOne: true,
      })

      let customerId = existingSub?.stripe_customer_id

      // Create or retrieve Stripe customer
      if (!customerId) {
        const userDetails: any = await this.authService.getMe(token)
        const userData = userDetails?.data

        const customer = await stripe.customers.create({
          email: userData?.email,
          metadata: { user_id: String(loggedInUser.user_id) },
        })
        customerId = customer.id
      }

      // Create Stripe Price on-the-fly if no stripe_price_id stored
      let priceId = plan.stripe_price_id
      if (!priceId) {
        let productId = plan.stripe_product_id
        if (!productId) {
          const product = await stripe.products.create({
            name: plan.name,
            description: plan.description || undefined,
          })
          productId = product.id
          await this.subscriptionPlanRepository.save(
            { stripe_product_id: productId },
            { id: plan.id },
          )
        }

        const price = await stripe.prices.create({
          product: productId,
          unit_amount: Math.round(plan.price * 100),
          currency: plan.currency || "usd",
          recurring: {
            interval: plan.interval || "month",
            interval_count: plan.interval_count || 1,
          },
        })
        priceId = price.id
        await this.subscriptionPlanRepository.save(
          { stripe_price_id: priceId },
          { id: plan.id },
        )
      }

      // Determine if this is a plan switch (user already has an active paid sub)
      const isPlanSwitch =
        existingSub?.stripe_subscription_id && existingSub?.status === "active"

      const frontUrl = EV["USER_FRONT_URL"] || "http://localhost:3000"

      // Always open Stripe checkout so the user pays first.
      // If switching plans, we pass metadata so the webhook handler knows
      // to schedule the new plan after the current one ends.
      const session = await stripe.checkout.sessions.create({
        customer: customerId,
        payment_method_types: ["card"],
        line_items: [{ price: priceId, quantity: 1 }],
        mode: "subscription",
        success_url:
          successUrl || `${frontUrl}success?session_id={CHECKOUT_SESSION_ID}`,
        cancel_url: cancelUrl || `${frontUrl}cancel`,
        metadata: {
          user_id: String(loggedInUser.user_id),
          plan_id: String(plan.id),
          is_plan_switch: isPlanSwitch ? "true" : "false",
          current_subscription_id: existingSub?.id
            ? String(existingSub.id)
            : "",
          current_stripe_subscription_id:
            existingSub?.stripe_subscription_id || "",
        },
      })

      return successResponse(
        code.SUCCESS,
        successMessage(messageKey.checkout_created),
        {
          session_id: session.id,
          url: session.url,
        } as any,
      )
    } catch (err) {
      this.logger.error("Checkout session error:", err)
      return failureResponse(
        code.ERROR,
        errorMessage(messageKey.exception, { ":data": "Checkout" }),
      )
    }
  }

  async handleWebhook(rawBody: Buffer, signature: string) {
    let webhookLog: WebhookLog | null = null

    try {
      if (!rawBody) {
        this.logger.error("Webhook received empty body")
        return { received: false, error: "Empty request body" }
      }

      const bodyBuffer = Buffer.isBuffer(rawBody)
        ? rawBody
        : Buffer.from(rawBody)

      const settings = await this.stripeSettingsService.getActiveStripeKeys()
      if (!settings) {
        this.logger.error("Stripe settings not configured for webhook")
        return { received: false }
      }

      const stripe = new Stripe(settings.stripe_secret_key, {
        apiVersion: "2024-06-20",
      })

      const webhookSecret = EV["STRIPE_WEBHOOK_SECRET"]
      let event: Stripe.Event

      if (webhookSecret) {
        event = stripe.webhooks.constructEvent(
          bodyBuffer,
          signature,
          webhookSecret,
        )
      } else {
        event = JSON.parse(bodyBuffer.toString("utf8")) as Stripe.Event
      }

      // Log the webhook event
      webhookLog = new WebhookLog()
      webhookLog.event_id = event.id
      webhookLog.type = event.type
      webhookLog.payload = JSON.stringify(event.data.object)
      webhookLog.status = "processing"
      await this.webhookLogRepository.save(webhookLog)

      switch (event.type) {
        case "checkout.session.completed":
          await this.handleCheckoutCompleted(event.data.object as any)
          break
        case "invoice.paid":
          await this.handleInvoicePaid(event.data.object as any)
          break
        case "invoice.payment_failed":
          await this.handleInvoicePaymentFailed(event.data.object as any)
          break
        case "customer.subscription.updated":
          await this.handleSubscriptionUpdated(event.data.object as any)
          break
        case "customer.subscription.deleted":
          await this.handleSubscriptionDeleted(event.data.object as any)
          break
        default:
          this.logger.log(`Unhandled event type: ${event.type}`)
      }

      // Mark as processed
      if (webhookLog?.id) {
        await this.webhookLogRepository.save(
          { status: "processed" },
          { id: webhookLog.id },
        )
      }

      return { received: true }
    } catch (err) {
      this.logger.error("Webhook error:", err)

      // Log the failure
      if (webhookLog?.id) {
        await this.webhookLogRepository.save(
          { status: "failed", error_message: err.message },
          { id: webhookLog.id },
        )
      }

      return { received: false, error: err.message }
    }
  }

  private async handleCheckoutCompleted(session: Stripe.Checkout.Session) {
    const userId = parseInt(session.metadata?.user_id)
    const planId = parseInt(session.metadata?.plan_id)
    if (!userId) return

    const newStripeSubscriptionId = session.subscription as string
    const stripeCustomerId = session.customer as string
    const isPlanSwitch = session.metadata?.is_plan_switch === "true"
    const currentStripeSubId =
      session.metadata?.current_stripe_subscription_id || null

    const existingSub: any = await this.subscriptionRepository.getByParams({
      where: { user_id: userId },
      orderBy: { created_at: "DESC" },
      findOne: true,
    })

    // PLAN SWITCH: user already has an active subscription and paid for a new plan.
    // The new plan should start after the current plan ends.
    if (isPlanSwitch && existingSub && currentStripeSubId) {
      // Pause the newly created subscription immediately so it doesn't charge
      // again right away. We'll resume it when the current plan ends.
      const stripe = await this.getStripeClient()
      if (stripe) {
        try {
          await stripe.subscriptions.update(newStripeSubscriptionId, {
            pause_collection: { behavior: "void" },
          })
        } catch (err) {
          this.logger.error("Failed to pause new subscription:", err)
        }
      }

      // Store next plan info on the existing subscription
      existingSub.next_plan_id = planId
      existingSub.next_plan_start_date = existingSub.current_period_end
      existingSub.next_stripe_subscription_id = newStripeSubscriptionId
      existingSub.stripe_customer_id = stripeCustomerId
      await this.subscriptionRepository.save(existingSub, {
        id: existingSub.id,
      })

      await this.logSubscriptionHistory(
        userId,
        existingSub.plan_id,
        planId,
        "plan_change_scheduled",
      )
      return
    }

    // NORMAL FLOW: first subscription or trial-to-paid
    const oldPlanId = existingSub?.plan_id || null
    const oldStatus = existingSub?.status

    if (existingSub) {
      existingSub.status = "active"
      existingSub.plan_id = planId || existingSub.plan_id
      existingSub.stripe_subscription_id = newStripeSubscriptionId
      existingSub.stripe_customer_id = stripeCustomerId
      existingSub.cancel_at_period_end = 0
      existingSub.cancelled_at = null
      existingSub.next_plan_id = null
      existingSub.next_plan_start_date = null
      existingSub.next_stripe_subscription_id = null
      await this.subscriptionRepository.save(existingSub, {
        id: existingSub.id,
      })
    } else {
      const subscription = new Subscription()
      subscription.user_id = userId
      subscription.plan_id = planId
      subscription.status = "active"
      subscription.stripe_subscription_id = newStripeSubscriptionId
      subscription.stripe_customer_id = stripeCustomerId
      await this.subscriptionRepository.save(subscription)
    }

    const action =
      oldStatus === "trialing"
        ? "trial_to_paid"
        : oldPlanId && oldPlanId !== planId
          ? "plan_changed"
          : "plan_activated"

    await this.logSubscriptionHistory(userId, oldPlanId, planId, action)
  }

  private async handleInvoicePaid(invoice: Stripe.Invoice) {
    const customerId = invoice.customer as string

    const sub: any = await this.subscriptionRepository.getByParams({
      where: { stripe_customer_id: customerId },
      orderBy: { created_at: "DESC" },
      relations: ["plan"],
      findOne: true,
    })

    if (sub) {
      sub.status = "active"
      sub.current_period_start = new Date(
        (invoice as any).lines?.data?.[0]?.period?.start * 1000 || Date.now(),
      )
      sub.current_period_end = new Date(
        (invoice as any).lines?.data?.[0]?.period?.end * 1000 || Date.now(),
      )
      await this.subscriptionRepository.save(sub, { id: sub.id })

      const transaction = new Transaction()
      transaction.user_id = sub.user_id
      transaction.subscription_id = sub.id
      transaction.plan_id = sub.plan_id
      transaction.plan_name = sub.plan?.name || ""
      transaction.amount = (invoice.amount_paid || 0) / 100
      transaction.currency = invoice.currency || "usd"
      transaction.status = "succeeded"
      transaction.stripe_payment_intent_id = invoice.payment_intent as string
      transaction.stripe_invoice_id = invoice.id
      await this.transactionRepository.save(transaction)
    }
  }

  private async handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
    const customerId = invoice.customer as string

    const sub: any = await this.subscriptionRepository.getByParams({
      where: { stripe_customer_id: customerId },
      orderBy: { created_at: "DESC" },
      relations: ["plan"],
      findOne: true,
    })

    if (sub) {
      const transaction = new Transaction()
      transaction.user_id = sub.user_id
      transaction.subscription_id = sub.id
      transaction.plan_id = sub.plan_id
      transaction.plan_name = sub.plan?.name || ""
      transaction.amount = (invoice.amount_due || 0) / 100
      transaction.currency = invoice.currency || "usd"
      transaction.status = "failed"
      transaction.stripe_payment_intent_id = invoice.payment_intent as string
      transaction.stripe_invoice_id = invoice.id
      await this.transactionRepository.save(transaction)
    }
  }

  private async handleSubscriptionUpdated(
    stripeSubscription: Stripe.Subscription,
  ) {
    const customerId = stripeSubscription.customer as string

    const sub: any = await this.subscriptionRepository.getByParams({
      where: { stripe_customer_id: customerId },
      orderBy: { created_at: "DESC" },
      findOne: true,
    })

    if (sub) {
      const oldStatus = sub.status

      sub.current_period_start = new Date(
        stripeSubscription.current_period_start * 1000,
      )
      sub.current_period_end = new Date(
        stripeSubscription.current_period_end * 1000,
      )
      sub.cancel_at_period_end = stripeSubscription.cancel_at_period_end ? 1 : 0

      if (stripeSubscription.status === "active") {
        sub.status = "active"
      } else if (stripeSubscription.status === "canceled") {
        sub.status = "cancelled"
      }

      await this.subscriptionRepository.save(sub, { id: sub.id })

      if (oldStatus !== sub.status) {
        const action =
          sub.status === "cancelled"
            ? "cancelled"
            : sub.status === "active" && oldStatus === "cancelled"
              ? "reactivated"
              : "renewed"
        await this.logSubscriptionHistory(
          sub.user_id,
          sub.plan_id,
          sub.plan_id,
          action,
        )
      }
    }
  }

  private async handleSubscriptionDeleted(
    stripeSubscription: Stripe.Subscription,
  ) {
    const customerId = stripeSubscription.customer as string

    const sub: any = await this.subscriptionRepository.getByParams({
      where: { stripe_customer_id: customerId },
      orderBy: { created_at: "DESC" },
      findOne: true,
    })

    if (!sub) return

    const oldPlanId = sub.plan_id

    // If there's a next plan scheduled, activate it now
    if (sub.next_plan_id && sub.next_stripe_subscription_id) {
      const stripe = await this.getStripeClient()
      if (stripe) {
        try {
          await stripe.subscriptions.update(sub.next_stripe_subscription_id, {
            pause_collection: "",
            billing_cycle_anchor: "now",
            proration_behavior: "none",
          })
        } catch (err) {
          this.logger.error("Failed to resume next subscription:", err)
        }
      }

      // Activate the next plan
      sub.plan_id = sub.next_plan_id
      sub.stripe_subscription_id = sub.next_stripe_subscription_id
      sub.status = "active"
      sub.next_plan_id = null
      sub.next_plan_start_date = null
      sub.next_stripe_subscription_id = null
      sub.cancel_at_period_end = 0
      sub.cancelled_at = null
      await this.subscriptionRepository.save(sub, { id: sub.id })

      await this.logSubscriptionHistory(
        sub.user_id,
        oldPlanId,
        sub.plan_id,
        "plan_changed",
      )
    } else {
      sub.status = "expired"
      await this.subscriptionRepository.save(sub, { id: sub.id })

      await this.logSubscriptionHistory(sub.user_id, oldPlanId, null, "expired")
    }
  }

  /**
   * Cancel a Stripe subscription at the end of the current billing period.
   * Called by SubscriptionService when user cancels via POST /subscription/cancel.
   */
  async cancelStripeSubscription(stripeSubscriptionId: string): Promise<void> {
    const stripe = await this.getStripeClient()
    if (!stripe) {
      throw new Error("Stripe is not configured")
    }

    await stripe.subscriptions.update(stripeSubscriptionId, {
      cancel_at_period_end: true,
    })
  }

  private async logSubscriptionHistory(
    userId: number,
    oldPlanId: number | null,
    newPlanId: number | null,
    action: string,
  ) {
    try {
      const history = new SubscriptionHistory()
      history.user_id = userId
      history.old_plan_id = oldPlanId
      history.new_plan_id = newPlanId
      history.action = action
      await this.subscriptionHistoryRepository.save(history)
    } catch (err) {
      this.logger.error("Failed to log subscription history:", err)
    }
  }
}
