import Boom from '@hapi/boom'
import { type Request, type Server } from '@hapi/hapi'
import { isEqual } from 'date-fns'

import { PREVIEW_PATH_PREFIX } from '../../../constants.js'
import { assertFormAvailable } from '../form-availability.js'
import {
  checkEmailAddressForLiveFormSubmission,
  getCacheService
} from '../helpers.js'
import { FormModel } from '../models/index.js'
import { type PageController } from '../pageControllers/PageController.js'
import { TerminalPageController } from '../pageControllers/index.js'
import * as defaultServices from '../services/index.js'
import {
  type AnyRequest,
  type FormContext,
  type FormContextRequest,
  type FormSubmissionError,
  type FormSubmissionState
} from '../types.js'
import { FormStatus } from '../../../routes/types.js'
import { type Services } from '../../../types.js'

type JourneyState = FormStatus | 'preview'

export interface FormModelOptions {
  services?: Services
  controllers?: Record<string, typeof PageController>
  basePath?: string
  ordnanceSurveyApiKey?: string
  formId?: string
  routePrefix?: string
  isPreview?: boolean
}

export interface FormContextOptions extends FormModelOptions {
  errors?: FormSubmissionError[]
}

type SummaryRequest = FormContextRequest & {
  yar: Request['yar']
}

export async function getFormModel(
  slug: string,
  state: JourneyState,
  options: FormModelOptions = {}
) {
  const services = options.services ?? defaultServices
  const { formsService } = services
  const isPreview = isPreviewState(state, options)
  const formState = resolveState(state)

  const metadata = await formsService.getFormMetadata(slug)
  assertFormAvailable(metadata)

  const definition = await formsService.getFormDefinition(
    metadata.id,
    formState
  )

  if (!definition) {
    throw Boom.notFound(
      `No definition found for form metadata ${metadata.id} (${slug}) ${state}`
    )
  }

  return new FormModel(
    definition,
    {
      basePath:
        options.basePath ??
        buildBasePath(options.routePrefix ?? '', slug, formState, isPreview),
      ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,
      formId: options.formId ?? metadata.id
    },
    services,
    options.controllers
  )
}

export async function getFormContext(
  { server, yar }: Pick<Request, 'server' | 'yar'>,
  slug: string,
  state: JourneyState = FormStatus.Live,
  options: FormContextOptions = {}
): Promise<FormContext> {
  const formModel = await resolveFormModel(server, slug, state, options)

  const cacheService = getCacheService(server)

  const summaryRequest: SummaryRequest = {
    app: {},
    method: 'get',
    params: {
      path: 'summary',
      slug,
      ...(isPreviewState(state, options) && {
        state: resolveState(state)
      })
    },
    path: `/${formModel.basePath}/summary`,
    query: {},
    url: new URL(
      `/${formModel.basePath}/summary`,
      'https://form-context.local'
    ),
    server,
    yar
  }

  const cachedState = await cacheService.getState(
    summaryRequest as unknown as AnyRequest
  )

  const formState = {
    ...cachedState,
    $$__referenceNumber: cachedState.$$__referenceNumber
  } as unknown as FormSubmissionState

  return formModel.getFormContext(
    summaryRequest,
    formState,
    options.errors ?? []
  )
}

export async function resolveFormModel(
  server: Server,
  slug: string,
  state: JourneyState,
  options: FormModelOptions = {}
) {
  const services = options.services ?? defaultServices
  const { formsService } = services

  const metadata = await formsService.getFormMetadata(slug)
  assertFormAvailable(metadata)
  const formState = resolveState(state)
  const isPreview = options.isPreview ?? isPreviewState(state, options)
  const stateMetadata = metadata[formState]

  if (!stateMetadata) {
    throw Boom.notFound(
      `No '${formState}' state for form metadata ${metadata.id}`
    )
  }

  // The models cache is created lazily per server instance

  server.app.models ??= new Map<string, { model: FormModel; updatedAt: Date }>()

  const cache = server.app.models

  const cacheKey = `${metadata.id}_${formState}_${isPreview}`
  let entry = cache.get(cacheKey)

  if (!entry || !isEqual(entry.updatedAt, stateMetadata.updatedAt)) {
    const definition = await formsService.getFormDefinition(
      metadata.id,
      formState
    )

    if (!definition) {
      throw Boom.notFound(
        `No definition found for form metadata ${metadata.id} (${slug}) ${state}`
      )
    }

    checkEmailAddressForLiveFormSubmission(
      metadata.notificationEmail,
      isPreview
    )

    const routePrefix =
      options.routePrefix ?? server.realm.modifiers.route.prefix

    const model = new FormModel(
      definition,
      {
        basePath:
          options.basePath ??
          buildBasePath(routePrefix, slug, formState, isPreview),
        ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,
        formId: options.formId ?? metadata.id
      },
      services,
      options.controllers
    )

    entry = { model, updatedAt: stateMetadata.updatedAt }
    cache.set(cacheKey, entry)
  }

  return entry.model
}

function buildBasePath(
  routePrefix: string,
  slug: string,
  state: FormStatus,
  isPreview: boolean
) {
  const base = (
    isPreview
      ? `${routePrefix}${PREVIEW_PATH_PREFIX}/${state}/${slug}`
      : `${routePrefix}/${slug}`
  ).replace(/\/{2,}/g, '/')

  return base.startsWith('/') ? base.slice(1) : base
}

export function getFirstJourneyPage(
  context?: Pick<FormContext, 'relevantPages'>
) {
  if (!context?.relevantPages) {
    return undefined
  }

  const lastPageReached = context.relevantPages.at(-1)
  const penultimatePageReached = context.relevantPages.at(-2)

  if (
    lastPageReached instanceof TerminalPageController &&
    penultimatePageReached
  ) {
    return penultimatePageReached
  }

  return lastPageReached
}

function resolveState(state: JourneyState): FormStatus {
  return state === 'preview' ? FormStatus.Live : state
}

function isPreviewState(
  state: JourneyState,
  options: FormModelOptions = {}
): boolean {
  return options.isPreview ?? state === 'preview'
}
