import { randomUUID } from 'node:crypto'

import {
  type FormMetadata,
  type PaymentFieldComponent
} from '@defra/forms-model'
import { StatusCodes } from 'http-status-codes'
import joi, { type ObjectSchema } from 'joi'

import { logger } from '../../../common/helpers/logging/logger.js'
import { COMPONENT_STATE_ERROR } from '../../../constants.js'
import { FormComponent } from './FormComponent.js'
import { type PaymentState } from './PaymentField.types.js'
import {
  createError,
  getPluginOptions
} from '../helpers.js'
import { type FormModel } from '../models/index.js'
import {
  PaymentErrorTypes,
  PaymentPreAuthError,
  PaymentSubmissionError
} from '../pageControllers/errors.js'
import {
  type FormContext,
  type FormRequestPayload,
  type FormResponseToolkit
} from '../types/index.js'
import {
  type ErrorMessageTemplateList,
  type FormPayload,
  type FormState,
  type FormStateValue,
  type FormSubmissionError,
  type FormSubmissionState,
  type PaymentExternalArgs
} from '../types.js'
import {
  createPaymentService,
  formatCurrency
} from '../../payment/helper.js'

export class PaymentField extends FormComponent {
  declare options: PaymentFieldComponent['options']
  declare formSchema: ObjectSchema
  declare stateSchema: ObjectSchema
  isAppendageStateSingleObject = true

  constructor(
    def: PaymentFieldComponent,
    props: ConstructorParameters<typeof FormComponent>[1]
  ) {
    super(def, props)

    this.options = def.options

    const paymentStateSchema = joi
      .object({
        paymentId: joi.string().required(),
        reference: joi.string().required(),
        amount: joi.number().required(),
        description: joi.string().required(),
        uuid: joi.string().uuid().required(),
        formId: joi.string().required(),
        isLivePayment: joi.boolean().required(),
        preAuth: joi
          .object({
            status: joi
              .string()
              .valid('success', 'failed', 'started')
              .required(),
            createdAt: joi.string().isoDate().required()
          })
          .required()
      })
      .unknown(true)
      .label(this.label)

    this.formSchema = paymentStateSchema
    // 'required()' forces the payment page to be invalid until we have valid payment state
    // i.e. the user will automatically be directed back to the payment page
    // if they attempt to access future pages when no payment entered yet
    this.stateSchema = paymentStateSchema.required()
  }

  /**
   * Gets the PaymentState from form submission state
   */
  getPaymentStateFromState(
    state: FormSubmissionState
  ): PaymentState | undefined {
    const value = state[this.name]
    return this.isPaymentState(value) ? value : undefined
  }

  getDisplayStringFromState(state: FormSubmissionState): string {
    const value = this.getPaymentStateFromState(state)

    if (!value) {
      return ''
    }

    return `${formatCurrency(value.amount)} - ${value.description}`
  }

  getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
    const viewModel = super.getViewModel(payload, errors)

    // Payload is pre-populated from state if a payment has already been made
    const paymentState = this.isPaymentState(payload[this.name] as unknown)
      ? (payload[this.name] as unknown as PaymentState)
      : undefined

    // Use payment state amount if pre-authorized, otherwise use default.
    // The page controller overrides this with the resolved conditional amount
    // using the full form state (which getViewModel doesn't have access to).
    const amount = paymentState?.amount ?? this.options.amount

    return {
      ...viewModel,
      amount: formatCurrency(amount),
      description: this.options.description,
      paymentState
    }
  }

  /**
   * Type guard to check if value is PaymentState
   */
  isPaymentState(value: unknown): value is PaymentState {
    return PaymentField.isPaymentState(value)
  }

  /**
   * Static type guard to check if value is PaymentState
   */
  static isPaymentState(value: unknown): value is PaymentState {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
      return false
    }

    const state = value as PaymentState
    return (
      typeof state.paymentId === 'string' &&
      typeof state.amount === 'number' &&
      typeof state.description === 'string'
    )
  }

  /**
   * Override base isState to validate PaymentState
   */
  isState(value?: FormStateValue | FormState): value is FormState {
    return this.isPaymentState(value)
  }

  getFormValue(value?: FormStateValue | FormState) {
    return this.isPaymentState(value)
      ? (value as unknown as NonNullable<FormStateValue>)
      : undefined
  }

  getContextValueFromState(state: FormSubmissionState) {
    return this.isPaymentState(state)
      ? `Reference: ${state.reference}\nAmount: ${formatCurrency(state.amount)}`
      : ''
  }

  /**
   * For error preview page that shows all possible errors on a component
   */
  getAllPossibleErrors(): ErrorMessageTemplateList {
    return PaymentField.getAllPossibleErrors()
  }

  /**
   * Static version of getAllPossibleErrors that doesn't require a component instance.
   */
  static getAllPossibleErrors(): ErrorMessageTemplateList {
    return {
      baseErrors: [
        {
          type: 'paymentRequired',
          template: 'Complete the payment to continue'
        }
      ],
      advancedSettingsErrors: []
    }
  }

  /**
   * Resolves the payment amount from conditional amounts configuration.
   * Evaluates conditions in order; first true condition wins.
   * Falls back to the default options.amount.
   */
  static resolveAmount(
    options: PaymentFieldComponent['options'],
    model: FormModel,
    state: FormState
  ): number {
    const { conditionalAmounts } = options

    if (!conditionalAmounts?.length) {
      return options.amount
    }

    for (const { condition, amount } of conditionalAmounts) {
      if (!model.conditions[condition]) {
        logger.warn(
          `[payment] Condition '${condition}' not found in form conditions. Skipping.`
        )
        continue
      }
      if (model.conditions[condition].fn(state)) {
        return amount
      }
    }

    return options.amount
  }

  /**
   * Dispatcher for external redirect to GOV.UK Pay
   */
  static async dispatcher(
    request: FormRequestPayload,
    h: FormResponseToolkit,
    args: PaymentExternalArgs
  ): Promise<unknown> {
    const { options, name: componentName } = args.component
    const { model } = args.controller

    const state = await args.controller.getState(request)
    const { baseUrl } = getPluginOptions(request.server)
    const summaryUrl = `${baseUrl}/${model.basePath}/summary`

    const existingPaymentState = state[componentName]
    if (
      PaymentField.isPaymentState(existingPaymentState) &&
      existingPaymentState.preAuth?.status === 'success'
    ) {
      return h.redirect(summaryUrl).code(StatusCodes.SEE_OTHER)
    }

    const isLivePayment = args.isLive && !args.isPreview
    const formId = args.controller.model.formId
    const formsService = model.services.formsService
    const paymentService = await createPaymentService(
      isLivePayment,
      formId,
      formsService
    )

    const uuid = randomUUID()

    const reference = state.$$__referenceNumber as string
    const resolvedAmount = PaymentField.resolveAmount(options, model, state)

    // Zero-amount safety net (page skip should prevent this, but defensive)
    if (resolvedAmount === 0) {
      return h.redirect(summaryUrl).code(StatusCodes.SEE_OTHER)
    }

    const description = options.description

    const slug = `/${model.basePath}`

    const payCallbackUrl = `${baseUrl}/payment-callback?uuid=${uuid}`
    const paymentPageUrl = args.sourceUrl

    let prefilledEmail: string | undefined
    const userConfirmationEmail = state.userConfirmationEmailAddress
    if (typeof userConfirmationEmail === 'string' && userConfirmationEmail) {
      prefilledEmail = userConfirmationEmail
    }

    const amountInPence = Math.round(resolvedAmount * 100)
    const payment = await paymentService.createPayment(
      amountInPence,
      description,
      payCallbackUrl,
      reference,
      isLivePayment,
      { formId, slug },
      prefilledEmail
    )

    if (!payment) {
      const message = isLivePayment
        ? 'There is a problem and we cannot take a payment. Contact us (details in the footer of this form) or save your progress and return to the form later.'
        : 'Add a valid test API key before you can preview the payment journey.'
      const govukError = createError(componentName, message)
      request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)
      return h
        .redirect(`${request.url.pathname}${request.url.search}`)
        .code(StatusCodes.SEE_OTHER)
    }

    const sessionData: PaymentSessionData = {
      uuid,
      formId,
      reference,
      amount: resolvedAmount,
      description,
      paymentId: payment.paymentId,
      componentName,
      returnUrl: summaryUrl,
      failureUrl: paymentPageUrl,
      isLivePayment
    }

    request.yar.set(`payment-${uuid}`, sessionData)

    return h.redirect(payment.paymentUrl).code(StatusCodes.SEE_OTHER)
  }

  /**
   * Called on form submission to capture the payment
   * @see https://docs.payments.service.gov.uk/delayed_capture/#delay-taking-a-payment
   */
  async onSubmit(
    request: FormRequestPayload,
    _metadata: FormMetadata,
    context: FormContext
  ): Promise<void> {
    // Zero-amount bypass — no capture needed
    const resolvedAmount = PaymentField.resolveAmount(
      this.options,
      this.model,
      context.state
    )
    if (resolvedAmount === 0) {
      return
    }

    const paymentState = this.getPaymentStateFromState(context.state)

    if (!paymentState) {
      throw new PaymentPreAuthError(
        this,
        'Complete the payment to continue',
        true,
        PaymentErrorTypes.PaymentIncomplete
      )
    }

    if (paymentState.capture?.status === 'success') {
      return
    }

    const { paymentId, isLivePayment, formId } = paymentState
    const formsService = this.model.services.formsService
    const paymentService = await createPaymentService(
      isLivePayment,
      formId,
      formsService
    )

    /**
     * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle
     */
    const status = await paymentService.getPaymentStatus(
      paymentId,
      isLivePayment
    )

    PaymentSubmissionError.checkPaymentAmount(
      status.amount,
      resolvedAmount,
      this
    )

    if (status.state.status === 'success') {
      await this.markPaymentCaptured(request, paymentState)
      return
    }

    if (status.state.status !== 'capturable') {
      throw new PaymentPreAuthError(
        this,
        'Your payment authorisation has expired. Please add your payment details again.',
        true,
        PaymentErrorTypes.PaymentExpired
      )
    }

    const captured = await paymentService.capturePayment(
      paymentId,
      status.amount
    )

    if (!captured) {
      throw new PaymentPreAuthError(
        this,
        'There was a problem and your form was not submitted. Try submitting the form again.',
        false
      )
    }

    await this.markPaymentCaptured(request, paymentState)
  }

  /**
   * Updates payment state to mark capture as successful
   * This ensures we don't try to re-capture on submission retry
   */
  private async markPaymentCaptured(
    request: FormRequestPayload,
    paymentState: PaymentState
  ): Promise<void> {
    const updatedState: PaymentState = {
      ...paymentState,
      capture: {
        status: 'success',
        createdAt: new Date().toISOString()
      }
    }

    if (this.page) {
      const currentState = await this.page.getState(request)
      await this.page.mergeState(request, currentState, {
        [this.name]: updatedState
      })
    }
  }
}

/**
 * Session data stored when dispatching to GOV.UK Pay
 */
export interface PaymentSessionData {
  uuid: string
  formId: string
  reference: string
  amount: number
  description: string
  paymentId: string
  componentName: string
  returnUrl: string
  failureUrl: string
  isLivePayment: boolean
}
