import { SessionStore } from '@backend/auth'
import { BillingProfile } from '@backend/domain'
import { BillingProfileRepository } from '@backend/repository'
import { StageId } from '@core/types'
import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common'
import { IncomingHttpHeaders } from 'node:http'
import stripe from 'stripe'
import { Stage, STRIPE_OPTIONS } from '../../../config'
import { BillingProfileMapper } from '../mapper'
import { BillingProfileResponse, CreateBillingProfileRequest, CreateBillingProfileResponse } from '../types'
import { StripeService } from './stripe.service'

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

	constructor(
		private readonly _stripeService: StripeService,
		private readonly _billingProfileRepository: BillingProfileRepository,
	) {}

	async findBillingProfile(): Promise<BillingProfileResponse> {
		const profile = await this._billingProfileRepository.findOne({})
		if (!profile) throw new NotFoundException('Tenant has not setup the billing profile')

		return BillingProfileMapper.billingProfileToResponse(profile)
	}

	async createBillingProfile(request: CreateBillingProfileRequest): Promise<CreateBillingProfileResponse> {
		const tenantId = SessionStore.tenantId
		if (!tenantId) throw new ForbiddenException()

		const account = await this._stripeService.createConnectedAccount(tenantId)

		const profile: Partial<BillingProfile> = { stripeAccountId: account.id }

		await this._billingProfileRepository.save(profile)

		this.logger.log('Created billing profile for tenant', { tenantId, accountId: account.id })

		const onBoardingUrl = await this._stripeService.getAccountOnBoardingUrl(
			account.id,
			request.refreshUrl,
			request.redirectUrl,
		)

		return { redirectUrl: onBoardingUrl, accountId: account.id }
	}

	async getOnBoardingUrl(request: CreateBillingProfileRequest): Promise<CreateBillingProfileResponse> {
		const profile = await this._billingProfileRepository.findOne({ select: { stripeAccountId: true } })
		if (!profile) throw new NotFoundException('Tenant has not setup the billing profile')

		const onBoardingUrl = await this._stripeService.getAccountOnBoardingUrl(
			profile.stripeAccountId,
			request.refreshUrl,
			request.redirectUrl,
		)

		return { redirectUrl: onBoardingUrl, accountId: profile.stripeAccountId }
	}

	async handleStripeWebhook(rawBody: Buffer, headers: IncomingHttpHeaders): Promise<void> {
		const event = this.getEventFromRequest(rawBody, headers['stripe-signature']!, STRIPE_OPTIONS.WEBHOOK_SECRET!)

		const { data, livemode, type } = event

		// Ignore test events in production
		if (Stage === StageId.PRODUCTION && livemode === false) return

		this.logger.log('Stripe webhook received', { event })

		switch (type) {
			case 'account.updated': {
				return this.onAccountUpdated(data)
			}
			default: {
				return
			}
		}
	}

	private getEventFromRequest(body: Buffer, signature: string | string[], secret: string): stripe.Event {
		try {
			return stripe.webhooks.constructEvent(body, signature, secret)
		} catch (error) {
			throw new BadRequestException(error)
		}
	}

	private async onAccountUpdated(data: stripe.AccountUpdatedEvent.Data): Promise<void> {
		this.logger.log({ message: 'Handling as account updated event', data })

		const { object: account } = data

		if (!account.metadata?.tenantId) {
			this.logger.error({ message: 'Stripe webhook did not provide tenantId in metadata', metadata: account.metadata })
			return
		}

		const tenantId = account.metadata.tenantId
		await this._billingProfileRepository.update({ tenantId }, { profile: account as never })

		this.logger.log({ message: 'Billing info updated for tenant via Stripe webhook', tenantId })
	}
}
