import type Stripe from 'stripe'
import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
import { ToolingError } from '../../../../types'
import type { ExecutionContext } from '../../../../../base/types'
import type {
  StripeCredentials,
  CreatePaymentLinkParams,
  CreatePaymentLinkResult,
  GetPaymentLinkParams,
  GetPaymentLinkResult,
  UpdatePaymentLinkParams,
  UpdatePaymentLinkResult,
  ListPaymentLinksParams,
  ListPaymentLinksResult,
  CreateCheckoutSessionParams,
  CreateCheckoutSessionResult,
  CreateAutoPaymentLinkParams,
  CreateAutoPaymentLinkResult
} from './fetch/utils/types'

/**
 * Stripe API version
 * Using latest stable version with Payment Links support
 */
const STRIPE_API_VERSION = '2026-01-28.clover' as const

// Lazy-loaded Stripe client to avoid eager loading issues in Docker
let stripeModule: typeof import('stripe') | null = null
async function getStripeModule(): Promise<typeof import('stripe')> {
  if (!stripeModule) {
    stripeModule = await import('stripe')
  }
  return stripeModule
}

/**
 * Stripe integration adapter
 *
 * Provides access to Stripe Payment Links API for creating and managing
 * payment links programmatically.
 *
 * @example
 * ```typescript
 * const adapter = new StripeAdapter()
 * const result = await adapter.call(
 *   'createPaymentLink',
 *   {
 *     priceId: 'price_xxx',
 *     quantity: 1,
 *     metadata: { dealId: '123' }
 *   },
 *   { apiKey: 'sk_xxx' },
 *   context
 * )
 * ```
 */
export class StripeAdapter implements BaseIntegrationAdapter {
  readonly name = 'stripe'

  /**
   * Create Stripe client with secret key (lazy-loaded)
   */
  private async createClient(secretKey: string): Promise<Stripe> {
    const { default: StripeClient } = await getStripeModule()
    return new StripeClient(secretKey, {
      apiVersion: STRIPE_API_VERSION,
      typescript: true
    })
  }

  /**
   * Call Stripe API method
   */
  async call(
    method: string,
    params: unknown,
    credentials: Record<string, unknown>,
    context?: ExecutionContext
  ): Promise<unknown> {
    // Validate credentials first
    if (!this.validateCredentials(credentials)) {
      throw new ToolingError('credentials_invalid', 'Invalid Stripe credentials', {
        integration: this.name,
        organizationId: context?.organizationId
      })
    }

    // Safe to cast after validation confirms apiKey exists and is valid
    const creds = credentials as unknown as StripeCredentials
    const stripe = await this.createClient(creds.apiKey)

    // Route to method handler
    switch (method) {
      case 'createPaymentLink':
        return this.createPaymentLink(stripe, params as CreatePaymentLinkParams, context)
      case 'getPaymentLink':
        return this.getPaymentLink(stripe, params as GetPaymentLinkParams, context)
      case 'updatePaymentLink':
        return this.updatePaymentLink(stripe, params as UpdatePaymentLinkParams, context)
      case 'listPaymentLinks':
        return this.listPaymentLinks(stripe, params as ListPaymentLinksParams, context)
      case 'createCheckoutSession':
        return this.createCheckoutSession(stripe, params as CreateCheckoutSessionParams, context)
      case 'createAutoPaymentLink':
        return this.createAutoPaymentLink(stripe, params as CreateAutoPaymentLinkParams, context)
      default:
        throw new ToolingError('method_not_found', `Unknown Stripe method: ${method}`, {
          integration: this.name,
          method,
          organizationId: context?.organizationId
        })
    }
  }

  /**
   * Validate Stripe credentials
   */
  validateCredentials(credentials: Record<string, unknown>): boolean {
    const creds = credentials as unknown as StripeCredentials
    // Require API key (should start with 'sk_')
    return !!creds.apiKey && typeof creds.apiKey === 'string' && creds.apiKey.startsWith('sk_')
  }

  /**
   * Create a payment link
   */
  private async createPaymentLink(
    stripe: Stripe,
    params: CreatePaymentLinkParams,
    context?: ExecutionContext
  ): Promise<CreatePaymentLinkResult> {
    try {
      // Build line items
      const lineItems: Stripe.PaymentLinkCreateParams.LineItem[] = [
        {
          price: params.priceId,
          quantity: params.quantity ?? 1
        }
      ]

      // Build after_completion config
      let afterCompletion: Stripe.PaymentLinkCreateParams.AfterCompletion | undefined
      if (params.afterCompletionType === 'redirect' && params.redirectUrl) {
        afterCompletion = {
          type: 'redirect',
          redirect: { url: params.redirectUrl }
        }
      } else if (params.afterCompletionType === 'hosted_confirmation') {
        afterCompletion = {
          type: 'hosted_confirmation'
        }
      }

      // Build create params
      const createParams: Stripe.PaymentLinkCreateParams = {
        line_items: lineItems,
        ...(params.metadata && { metadata: params.metadata }),
        ...(afterCompletion && { after_completion: afterCompletion }),
        ...(params.allowPromotionCodes !== undefined && {
          allow_promotion_codes: params.allowPromotionCodes
        }),
        ...(params.automaticTax !== undefined && {
          automatic_tax: { enabled: params.automaticTax }
        }),
        ...(params.billingAddressCollection && {
          billing_address_collection: params.billingAddressCollection
        }),
        ...(params.invoiceCreation !== undefined && {
          invoice_creation: { enabled: params.invoiceCreation }
        })
      }

      const paymentLink = await stripe.paymentLinks.create(createParams)

      // Log success
      if (context?.logger) {
        context.logger.info(
          `[StripeAdapter] Payment link created: organizationId=${context.organizationId} executionId=${context.executionId} paymentLinkId=${paymentLink.id}`
        )
      }

      return {
        id: paymentLink.id,
        url: paymentLink.url,
        active: paymentLink.active,
        livemode: paymentLink.livemode
      }
    } catch (error) {
      throw this.handleStripeError(error, 'createPaymentLink', context)
    }
  }

  /**
   * Get a payment link by ID
   */
  private async getPaymentLink(
    stripe: Stripe,
    params: GetPaymentLinkParams,
    context?: ExecutionContext
  ): Promise<GetPaymentLinkResult> {
    try {
      const paymentLink = await stripe.paymentLinks.retrieve(params.paymentLinkId)

      // Log success
      if (context?.logger) {
        context.logger.info(
          `[StripeAdapter] Payment link retrieved: organizationId=${context.organizationId} executionId=${context.executionId} paymentLinkId=${paymentLink.id}`
        )
      }

      return {
        id: paymentLink.id,
        url: paymentLink.url,
        active: paymentLink.active,
        livemode: paymentLink.livemode,
        metadata: (paymentLink.metadata as Record<string, string>) ?? {}
      }
    } catch (error) {
      throw this.handleStripeError(error, 'getPaymentLink', context)
    }
  }

  /**
   * Update a payment link
   */
  private async updatePaymentLink(
    stripe: Stripe,
    params: UpdatePaymentLinkParams,
    context?: ExecutionContext
  ): Promise<UpdatePaymentLinkResult> {
    try {
      const updateParams: Stripe.PaymentLinkUpdateParams = {
        ...(params.active !== undefined && { active: params.active }),
        ...(params.metadata && { metadata: params.metadata })
      }

      const paymentLink = await stripe.paymentLinks.update(params.paymentLinkId, updateParams)

      // Log success
      if (context?.logger) {
        context.logger.info(
          `[StripeAdapter] Payment link updated: organizationId=${context.organizationId} executionId=${context.executionId} paymentLinkId=${paymentLink.id}`
        )
      }

      return {
        id: paymentLink.id,
        active: paymentLink.active,
        url: paymentLink.url
      }
    } catch (error) {
      throw this.handleStripeError(error, 'updatePaymentLink', context)
    }
  }

  /**
   * List payment links
   */
  private async listPaymentLinks(
    stripe: Stripe,
    params: ListPaymentLinksParams,
    context?: ExecutionContext
  ): Promise<ListPaymentLinksResult> {
    try {
      const listParams: Stripe.PaymentLinkListParams = {
        limit: params.limit ?? 10,
        ...(params.active !== undefined && { active: params.active }),
        ...(params.startingAfter && { starting_after: params.startingAfter }),
        ...(params.endingBefore && { ending_before: params.endingBefore })
      }

      const response = await stripe.paymentLinks.list(listParams)

      // Log success
      if (context?.logger) {
        context.logger.info(
          `[StripeAdapter] Payment links listed: organizationId=${context.organizationId} executionId=${context.executionId} count=${response.data.length}`
        )
      }

      return {
        data: response.data.map((link: Stripe.PaymentLink) => ({
          id: link.id,
          url: link.url,
          active: link.active,
          metadata: (link.metadata as Record<string, string>) ?? {}
        })),
        hasMore: response.has_more
      }
    } catch (error) {
      throw this.handleStripeError(error, 'listPaymentLinks', context)
    }
  }

  /**
   * Create a checkout session for custom/variable pricing
   *
   * Uses price_data instead of a pre-defined price ID, allowing any amount
   * to be specified. The session URL expires in 24 hours.
   */
  private async createCheckoutSession(
    stripe: Stripe,
    params: CreateCheckoutSessionParams,
    context?: ExecutionContext
  ): Promise<CreateCheckoutSessionResult> {
    try {
      const session = await stripe.checkout.sessions.create({
        mode: 'payment',
        line_items: [
          {
            price_data: {
              currency: params.currency ?? 'usd',
              unit_amount: params.amount,
              product_data: {
                name: params.productName ?? 'AI Automation Services',
                ...(params.productDescription && { description: params.productDescription })
              }
            },
            quantity: 1
          }
        ],
        ...(params.metadata && { metadata: params.metadata }),
        success_url: params.successUrl,
        cancel_url: params.cancelUrl,
        ...(params.customerEmail && { customer_email: params.customerEmail })
      })

      // Log success
      if (context?.logger) {
        context.logger.info(
          `[StripeAdapter] Checkout session created: organizationId=${context.organizationId} executionId=${context.executionId} sessionId=${session.id}`
        )
      }

      return {
        id: session.id,
        url: session.url!,
        expiresAt: session.expires_at
      }
    } catch (error) {
      throw this.handleStripeError(error, 'createCheckoutSession', context)
    }
  }

  /**
   * Create a payment link with inline price_data (dynamic pricing)
   *
   * Supports both one-time and recurring fees in a single payment link.
   * URLs never expire - same link can be used in all reminder emails.
   */
  private async createAutoPaymentLink(
    stripe: Stripe,
    params: CreateAutoPaymentLinkParams,
    context?: ExecutionContext
  ): Promise<CreateAutoPaymentLinkResult> {
    try {
      const { dealId, companyName, contactEmail, initialFee, monthlyFee } = params

      // Build line items with inline price_data
      const lineItems: Array<{
        price_data: {
          currency: string
          unit_amount: number
          recurring?: { interval: 'month' }
          product_data: { name: string; description?: string }
        }
        quantity: number
      }> = []

      // One-time setup fee (if > 0)
      if (initialFee > 0) {
        lineItems.push({
          price_data: {
            currency: 'usd',
            unit_amount: initialFee * 100, // Convert dollars to cents
            product_data: {
              name: 'Implementation Fee',
              description: `One-time setup for ${companyName}`
            }
          },
          quantity: 1
        })
      }

      // Recurring monthly fee (if > 0)
      if (monthlyFee > 0) {
        lineItems.push({
          price_data: {
            currency: 'usd',
            unit_amount: monthlyFee * 100, // Convert dollars to cents
            recurring: { interval: 'month' },
            product_data: {
              name: 'AI Automation Services',
              description: `Monthly subscription for ${companyName}`
            }
          },
          quantity: 1
        })
      }

      // Validate at least one fee exists
      if (lineItems.length === 0) {
        throw new ToolingError(
          'validation_error',
          'Cannot create payment link: no fees set (both initialFee and monthlyFee are 0)',
          {
            integration: this.name,
            method: 'createAutoPaymentLink',
            organizationId: context?.organizationId
          }
        )
      }

      const paymentLink = await stripe.paymentLinks.create({
        line_items: lineItems,
        metadata: {
          deal_id: dealId,
          company_name: companyName,
          contact_email: contactEmail
        }
      })

      // Log success
      if (context?.logger) {
        context.logger.info(
          `[StripeAdapter] Auto payment link created: organizationId=${context.organizationId} executionId=${context.executionId} paymentLinkId=${paymentLink.id} initialFee=${initialFee} monthlyFee=${monthlyFee}`
        )
      }

      return {
        paymentLinkId: paymentLink.id,
        url: paymentLink.url,
        hasInitialFee: initialFee > 0,
        hasRecurring: monthlyFee > 0
      }
    } catch (error) {
      throw this.handleStripeError(error, 'createAutoPaymentLink', context)
    }
  }

  /**
   * Handle Stripe SDK errors and map to ToolingError
   */
  private handleStripeError(error: unknown, method: string, context?: ExecutionContext): ToolingError {
    // Type guard for Stripe errors
    const isStripeError = (err: unknown): err is Stripe.errors.StripeError => {
      return err instanceof Error && 'type' in err && typeof (err as Stripe.errors.StripeError).type === 'string'
    }

    // Handle Stripe-specific errors by checking error type
    if (isStripeError(error)) {
      const stripeError = error

      // Map Stripe error types to ToolingError types
      switch (stripeError.type) {
        case 'StripeAuthenticationError':
          return new ToolingError('auth_error', `Stripe authentication failed: ${stripeError.message}`, {
            integration: this.name,
            method,
            organizationId: context?.organizationId,
            stripeCode: stripeError.code
          })

        case 'StripeRateLimitError':
          return new ToolingError('rate_limit_exceeded', `Stripe rate limit exceeded: ${stripeError.message}`, {
            integration: this.name,
            method,
            organizationId: context?.organizationId,
            stripeCode: stripeError.code
          })

        case 'StripeConnectionError':
          return new ToolingError('network_error', `Stripe connection error: ${stripeError.message}`, {
            integration: this.name,
            method,
            organizationId: context?.organizationId,
            stripeCode: stripeError.code
          })

        case 'StripeInvalidRequestError':
          return new ToolingError('validation_error', `Stripe invalid request: ${stripeError.message}`, {
            integration: this.name,
            method,
            organizationId: context?.organizationId,
            stripeCode: stripeError.code,
            stripeParam: (stripeError as Stripe.errors.StripeInvalidRequestError).param
          })

        case 'StripeAPIError':
          return new ToolingError('api_error', `Stripe API error: ${stripeError.message}`, {
            integration: this.name,
            method,
            organizationId: context?.organizationId,
            stripeCode: stripeError.code
          })

        default:
          // Handle other Stripe error types
          return new ToolingError('api_error', `Stripe error: ${stripeError.message}`, {
            integration: this.name,
            method,
            organizationId: context?.organizationId,
            stripeCode: stripeError.code,
            stripeType: stripeError.type
          })
      }
    }

    // Handle unknown errors
    const message = error instanceof Error ? error.message : 'Unknown error'
    return new ToolingError('unknown_error', `Stripe ${method} failed: ${message}`, {
      integration: this.name,
      method,
      organizationId: context?.organizationId
    })
  }
}
