import {
  ComponentType,
  ControllerType,
  Engine,
  hasComponents,
  hasNext,
  hasRepeater,
  isPaymentPage,
  type Link,
  type Page
} from '@defra/forms-model'
import Boom from '@hapi/boom'
import { type RouteOptions } from '@hapi/hapi'
import { type ValidationErrorItem } from 'joi'

import {
  COMPONENT_STATE_ERROR,
  EXTERNAL_STATE_APPENDAGE,
  EXTERNAL_STATE_PAYLOAD,
  PAYMENT_EXPIRED_NOTIFICATION
} from '../../../constants.js'
import { ComponentCollection } from '../components/ComponentCollection.js'
import { PaymentField } from '../components/PaymentField.js'
import { optionalText } from '../components/constants.js'
import { type BackLink } from '../components/types.js'
import {
  checkFormStatus,
  getCacheService,
  getErrors,
  getSaveAndExitHelpers,
  normalisePath,
  proceed
} from '../helpers.js'
import { type FormModel } from '../models/index.js'
import { PageController } from './PageController.js'
import { prefillStateFromQueryParameters } from './helpers/state.js'
import {
  type AnyFormRequest,
  type FormContext,
  type FormContextRequest,
  type FormPageViewModel,
  type FormPayload,
  type FormPayloadParams,
  type FormState,
  type FormStateValue,
  type FormSubmissionState
} from '../types.js'
import { getComponentsByType } from '../validationHelpers.js'
import { formatCurrency } from '../../payment/helper.js'
import {
  FormAction,
  FormStatus,
  type FormRequest,
  type FormRequestPayload,
  type FormRequestPayloadRefs,
  type FormRequestRefs,
  type FormResponseToolkit
} from '../../../routes/types.js'
import {
  actionSchema,
  crumbSchema,
  paramsSchema
} from '../../../schemas/index.js'
import { merge } from '../../../services/cacheService.js'

export class QuestionPageController extends PageController {
  collection: ComponentCollection
  errorSummaryTitle = 'There is a problem'
  allowSaveAndExit = true

  constructor(model: FormModel, pageDef: Page) {
    super(model, pageDef)

    // Components collection
    this.collection = new ComponentCollection(
      hasComponents(pageDef) ? pageDef.components : [],
      { model, page: this }
    )

    this.collection.formSchema = this.collection.formSchema.keys({
      crumb: crumbSchema,
      action: actionSchema
    })
  }

  get next(): Link[] {
    const { def, pageDef } = this

    if (!hasNext(pageDef)) {
      return []
    }

    // Remove stale links
    return pageDef.next.filter(({ path }) => {
      const linkPath = normalisePath(path)

      return def.pages.some((page) => {
        const pagePath = normalisePath(page.path)
        return pagePath === linkPath
      })
    })
  }

  get allowContinue(): boolean {
    if (this.model.engine === Engine.V2) {
      return this.pageDef.controller !== ControllerType.Terminal
    }

    return this.next.length > 0
  }

  getItemId(request?: FormContextRequest) {
    const { itemId } = this.getFormParams(request)
    return itemId ?? request?.params.itemId
  }

  /**
   * Used for mapping form payloads and errors to govuk-frontend's template api, so a page can be rendered
   * @param request - the hapi request
   * @param context - the form context
   */
  getViewModel(
    request: FormContextRequest,
    context: FormContext
  ): FormPageViewModel {
    const { collection, viewModel } = this
    const { query } = request
    const { payload, errors } = context

    let { pageTitle, showTitle } = viewModel

    const components = collection.getViewModel(payload, errors, query)
    const formComponents = components.filter(
      ({ isFormComponent }) => isFormComponent
    )

    // Single form component? Hide title and customise label or legend instead
    if (formComponents.length === 1) {
      const { model } = formComponents[0]
      const { fieldset, label } = model

      // Set as page heading when not following other content
      const isPageHeading = formComponents[0] === components[0]

      // Check for legend or label
      const labelOrLegend = fieldset?.legend ?? label

      // Use legend or label as page heading
      if (labelOrLegend) {
        const size = isPageHeading ? 'l' : 'm'

        labelOrLegend.classes =
          labelOrLegend === label
            ? `govuk-label--${size}`
            : `govuk-fieldset__legend--${size}`

        if (isPageHeading) {
          labelOrLegend.isPageHeading = isPageHeading

          // Check for optional in label
          const isOptional =
            this.collection.fields.at(0)?.options.required === false

          if (pageTitle) {
            labelOrLegend.text = isOptional
              ? `${pageTitle}${optionalText}`
              : pageTitle
          }

          pageTitle = pageTitle || labelOrLegend.text
        }
      }

      showTitle = !isPageHeading
    } else if (formComponents.length > 1) {
      // When there is more than one form component,
      // adjust the label/legends to give equal prominence
      for (const { model } of formComponents) {
        if (model.fieldset?.legend) {
          model.fieldset.legend.classes = 'govuk-fieldset__legend--m'
        }
        if (model.label) {
          model.label.classes = 'govuk-label--m'
        }
      }
    }

    // Override payment amount display with resolved conditional amount
    // getViewModel() only has the current page's payload, not full form state.
    // The full state is available via context.evaluationState.
    for (const comp of components) {
      if ('amount' in comp.model && 'paymentState' in comp.model) {
        const paymentField = this.collection.fields.find(
          (f): f is PaymentField => f instanceof PaymentField
        )
        if (paymentField) {
          const resolvedAmount = PaymentField.resolveAmount(
            paymentField.options,
            this.model,
            context.evaluationState
          )
          comp.model.amount = formatCurrency(resolvedAmount)
        }
      }
    }

    const hasIncompletePayment = components.some(({ model }) => {
      if ('paymentState' in model) {
        const paymentState = model.paymentState as
          | { preAuth?: { status?: string } }
          | undefined
        return !paymentState?.preAuth?.status
      }
      return false
    })

    return {
      ...viewModel,
      backLink: this.getBackLink(request, context),
      context,
      showTitle,
      components,
      errors,
      allowSaveAndExit:
        this.shouldShowSaveAndExit(request.server) &&
        !isPaymentPage(this.pageDef),
      showSubmitButton: !hasIncompletePayment
    }
  }

  getRelevantPath(request: AnyFormRequest, context: FormContext) {
    const { paths } = context

    const startPath = this.getStartPath()
    const relevantPath = paths.at(-1) ?? startPath

    return !paths.length
      ? startPath // First possible path
      : relevantPath // Last possible path
  }

  /**
   * Apply conditions to evaluation state to determine next page path
   */
  getNextPath(context: FormContext) {
    const { model, next, path } = this
    const { evaluationState } = context

    const summaryPath = this.getSummaryPath()
    const statusPath = this.getStatusPath()

    // Walk from summary page (no next links) to status page
    let defaultPath = path === summaryPath ? statusPath : undefined

    if (model.engine === Engine.V2) {
      if (this.pageDef.controller !== ControllerType.Terminal) {
        const { pages } = this.model
        const pageIndex = pages.indexOf(this)

        // The "next" page is the first found after the current which is
        // either unconditional or has a condition that evaluates to "true"
        const nextPage = pages.slice(pageIndex + 1).find((page) => {
          const { condition } = page

          if (condition) {
            const conditionResult = condition.fn(evaluationState)

            if (!conditionResult) {
              return false
            }
          }

          // Skip payment pages in the normal page walk.
          // Users reach the payment page via "Pay and submit" on CYA,
          // not by navigating forward through the form.
          if (isPaymentPage(page.pageDef)) {
            return false
          }

          return true
        })

        return nextPage?.path ?? defaultPath
      } else {
        return defaultPath
      }
    }

    const nextLink = next.find((link) => {
      const { condition } = link

      if (condition) {
        return model.conditions[condition]?.fn(evaluationState) ?? false
      }

      defaultPath = link.path
      return false
    })

    return nextLink?.path ?? defaultPath
  }

  /**
   * Gets the form payload (from state) for this page only
   */
  getFormDataFromState(
    request: FormContextRequest | undefined,
    state: FormSubmissionState
  ): FormPayload {
    const { collection } = this

    // Form params from request
    const params = this.getFormParams(request)

    // Form payload from state
    const payload = collection.getFormDataFromState(state)

    return {
      ...params,
      ...payload
    }
  }

  /**
   * Gets form params (from payload) for this page only
   */
  getFormParams(request?: FormContextRequest): FormPayloadParams {
    const { payload } = request ?? {}

    const result = paramsSchema.validate(payload, {
      abortEarly: false,
      stripUnknown: true
    })

    return result.value as FormPayloadParams
  }

  getStateFromValidForm(
    request: FormContextRequest,
    state: FormSubmissionState,
    payload: FormPayload
  ): FormState {
    return this.collection.getStateFromValidForm(payload)
  }

  getErrors(details?: ValidationErrorItem[]) {
    return getErrors(details)
  }

  async getState(request: AnyFormRequest) {
    const { query } = request

    // Skip get for preview URL direct access
    if ('force' in query) {
      return {}
    }

    const cacheService = getCacheService(request.server)

    return cacheService.getState(request)
  }

  async setState(request: AnyFormRequest, state: FormSubmissionState) {
    const { query } = request

    // Skip set for preview URL direct access
    if ('force' in query) {
      return state
    }

    const cacheService = getCacheService(request.server)

    return cacheService.setState(request, state)
  }

  async mergeState(
    request: AnyFormRequest,
    state: FormSubmissionState,
    update: object
  ) {
    const { query } = request

    // Merge state before set
    const updated = merge(state, update)

    // Skip set for preview URL direct access
    if ('force' in query) {
      return updated
    }

    const cacheService = getCacheService(request.server)

    return cacheService.setState(request, updated)
  }

  filterConditionalComponents(
    viewModel: FormPageViewModel,
    model: FormModel,
    evaluationState: Partial<Record<string, FormStateValue>>
  ) {
    // Filter our components based on their conditions using our evaluated state
    let filtered = viewModel.components.filter((component) => {
      if (
        (!!component.model.content ||
          component.type === ComponentType.Details) &&
        component.model.condition
      ) {
        const condition = model.conditions[component.model.condition]
        return condition?.fn(evaluationState)
      }
      return true
    })

    /**
     * For conditional reveal components (which we no longer support until GDS resolves the related accessibility issues {@link https://github.com/alphagov/govuk-frontend/issues/1991}
     */
    filtered = filtered.map((component) => {
      const evaluatedComponent = component
      const content = evaluatedComponent.model.content
      if (Array.isArray(content)) {
        evaluatedComponent.model.content = content.filter((item) =>
          item.condition
            ? model.conditions[item.condition]?.fn(evaluationState)
            : true
        )
      }
      // apply condition to items for radios, checkboxes etc
      const items = evaluatedComponent.model.items

      if (Array.isArray(items)) {
        evaluatedComponent.model.items = items.filter((item) =>
          item.condition
            ? model.conditions[item.condition]?.fn(evaluationState)
            : true
        )
      }

      return evaluatedComponent
    })

    return filtered
  }

  makeGetRouteHandler() {
    return async (
      request: FormRequest,
      context: FormContext,
      h: FormResponseToolkit
    ) => {
      const { collection, model, viewName } = this
      const { evaluationState } = context

      // Copy any URL params into the form state (if not already done so)
      if (await prefillStateFromQueryParameters(request, this)) {
        // Forward to same page without query string
        return h.redirect(request.url.pathname)
      }

      const viewModel = this.getViewModel(request, context)
      viewModel.errors = collection.getViewErrors(viewModel.errors)

      const flashedError = request.yar.flash(COMPONENT_STATE_ERROR)
      const flashedErrors = !Array.isArray(flashedError) ? [flashedError] : []

      viewModel.errors = (viewModel.errors ?? []).concat(flashedErrors)

      const paymentExpiredFlash = request.yar.flash(
        PAYMENT_EXPIRED_NOTIFICATION
      )
      viewModel.showPaymentExpiredNotification =
        !Array.isArray(paymentExpiredFlash)

      /**
       * Content components can be hidden based on a condition. If the condition evaluates to true, it is safe to be kept, otherwise discard it
       */

      // Filter our components based on their conditions using our evaluated state
      viewModel.components = this.filterConditionalComponents(
        viewModel,
        model,
        evaluationState
      )

      viewModel.hasMissingNotificationEmail =
        await this.hasMissingNotificationEmail(request, context)

      return h.view(viewName, viewModel)
    }
  }

  async hasMissingNotificationEmail(
    request: FormRequest,
    context: FormContext
  ) {
    const { path } = this
    const { params } = request
    const { isForceAccess } = context

    const startPath = this.getStartPath()
    const summaryPath = this.getSummaryPath()
    const { formsService } = this.model.services
    const { getFormMetadata } = formsService

    // Warn the user if the form has no notification email set only on start page and summary page
    if ([startPath, summaryPath].includes(path) && !isForceAccess) {
      const { notificationEmail } = await getFormMetadata(params.slug)
      return !notificationEmail
    }

    return false
  }

  /**
   * Get the back link for a given progress.
   */
  protected getBackLink(
    request: FormContextRequest,
    context: FormContext
  ): BackLink | undefined {
    const { pageDef } = this
    const { path, query } = request
    const { returnUrl } = query
    const { paths } = context

    const itemId = this.getItemId(request)

    // Check answers back link
    if (returnUrl) {
      return {
        text:
          hasRepeater(pageDef) && itemId
            ? 'Go back to add another'
            : 'Go back to check answers',
        href: returnUrl
      }
    }

    // Item delete pages etc
    const backPath =
      itemId && !path.endsWith(itemId)
        ? paths.at(-1) // Back to main page
        : paths.at(-2) // Back to previous page

    // No back link
    if (!backPath) {
      return
    }

    // Default back link
    return {
      text: 'Back',
      href: this.getHref(backPath)
    }
  }

  makePostRouteHandler() {
    return async (
      request: FormRequestPayload,
      context: FormContext,
      h: FormResponseToolkit
    ) => {
      const { collection, viewName, model } = this
      const { isForceAccess, state, evaluationState } = context
      const action = request.payload.action

      if (action?.startsWith(FormAction.External)) {
        return await this.dispatchExternal(request, h, context)
      }

      /**
       * If there are any errors, render the page with the parsed errors
       * @todo Refactor to match POST REDIRECT GET pattern
       */
      if (context.errors || isForceAccess) {
        const viewModel = this.getViewModel(request, context)
        viewModel.errors = collection.getViewErrors(viewModel.errors)

        // Filter our components based on their conditions using our evaluated state
        viewModel.components = this.filterConditionalComponents(
          viewModel,
          model,
          evaluationState
        )

        return h.view(viewName, viewModel)
      }

      // Save state
      await this.setState(request, state)

      // Check if this is a save-and-exit action
      if (action === FormAction.SaveAndExit) {
        return this.handleSaveAndExit(request, context, h)
      }

      // Proceed to the next page
      return this.proceed(request, h, this.getNextPath(context))
    }
  }

  private async dispatchExternal(
    request: FormRequestPayload,
    h: FormResponseToolkit,
    context: FormContext
  ) {
    const { externalComponents } = getComponentsByType()
    const action = request.payload.action ?? ''

    // Find the external action and arguments
    // `external-{componentName}--{argname1}:{argvalue1}--{argname2}:{argvalue2}`
    // E.g. external-abcdef--amount:10--step:manual
    const externalActionsWithArgs = action
      .slice(`${FormAction.External}-`.length)
      .split('--')

    const externalActionArgs = externalActionsWithArgs
      .slice(1)
      .map((arg) => arg.split(':'))

    const args = Object.fromEntries(externalActionArgs) as Record<
      string,
      string
    >

    const componentName = externalActionsWithArgs[0]
    const component = this.model.componentDefMap.get(componentName)
    const componentType = component?.type

    if (!componentType) {
      throw Boom.internal(
        `External component of type ${componentType} not found`
      )
    }

    const selectedComponent = externalComponents.get(componentType)

    if (!selectedComponent) {
      throw Boom.internal(`External component ${componentName} not found`)
    }

    // Stash payload without crumb and action
    const stashedPayload = {
      ...context.payload,
      crumb: undefined,
      action: undefined
    }
    request.yar.flash(EXTERNAL_STATE_PAYLOAD, stashedPayload, true)

    // Clear any previous state appendage
    request.yar.clear(EXTERNAL_STATE_APPENDAGE)

    // Determine if this is a live form (not preview/draft)
    const { state, isPreview } = checkFormStatus(request.params)
    const isLive = state === FormStatus.Live

    return await selectedComponent.dispatcher(request, h, {
      component,
      controller: this,
      sourceUrl: `${request.url.pathname}${request.url.search}`,
      actionArgs: args,
      isLive,
      isPreview
    })
  }

  proceed(
    request: FormContextRequest,
    h: FormResponseToolkit,
    nextPath?: string
  ) {
    const nextUrl = nextPath
      ? this.getHref(nextPath) // Redirect to next page
      : this.href // Redirect to current page (refresh)

    return proceed(request, h, nextUrl)
  }

  /**
   * Handle save-and-exit action
   */
  handleSaveAndExit(
    request: FormRequestPayload,
    context: FormContext,
    h: FormResponseToolkit
  ) {
    const saveAndExit = getSaveAndExitHelpers(request.server)

    if (!saveAndExit) {
      throw Boom.internal('Server misconfigured for save and exit')
    }

    return saveAndExit(request, h, context)
  }

  /**
   * {@link https://hapi.dev/api/?v=20.1.2#route-options}
   */
  get getRouteOptions(): RouteOptions<FormRequestRefs> {
    return {
      ext: {
        onPostHandler: {
          method(_request, h) {
            return h.continue
          }
        }
      }
    }
  }

  /**
   * {@link https://hapi.dev/api/?v=20.1.2#route-options}
   */
  get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {
    return {
      payload: {
        parse: true,
        maxBytes: Number.MAX_SAFE_INTEGER,
        failAction: 'ignore'
      },
      ext: {
        onPostHandler: {
          method(_request, h) {
            return h.continue
          }
        }
      }
    }
  }
}
