import { type Server } from '@hapi/hapi'
import * as Hoek from '@hapi/hoek'
import unset from 'lodash/unset.js'

import { config } from '../../config/index.js'
import { type createServer } from '../index.js'
import {
  type AnyFormRequest,
  type CacheRequest,
  type FormConfirmationState,
  type FormPayload,
  type FormState,
  type FormSubmissionError,
  type FormSubmissionState
} from '../plugins/engine/types.js'

const partition = 'cache'

export enum ADDITIONAL_IDENTIFIER {
  Confirmation = ':confirmation'
}

export class CacheService {
  /**
   * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}
   */
  cache
  logger: Server['logger']

  constructor({ server, cacheName }: { server: Server; cacheName?: string }) {
    if (!cacheName) {
      server.log(
        'warn',
        'You are using the default hapi cache. Please provide a cache name in plugin registration options.'
      )
    }

    this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })
    this.logger = server.logger
  }

  async getState(request: CacheRequest): Promise<FormSubmissionState> {
    const key = this.Key(request)
    const cached = await this.cache.get(key)

    return cached ?? {}
  }

  async setState(request: CacheRequest, state: FormSubmissionState) {
    const key = this.Key(request)
    const ttl = config.get('sessionTimeout')

    await this.cache.set(key, state, ttl)

    return this.getState(request)
  }

  async getConfirmationState(
    request: CacheRequest
  ): Promise<FormConfirmationState> {
    const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)
    const value = await this.cache.get(key)

    return value ?? {}
  }

  async setConfirmationState(
    request: CacheRequest,
    confirmationState: FormConfirmationState
  ) {
    const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)
    const ttl = config.get('confirmationSessionTimeout')

    return this.cache.set(key, confirmationState, ttl)
  }

  async clearState(request: CacheRequest) {
    if (request.yar.id) {
      await this.cache.drop(this.Key(request))
    }
  }

  getFlash(
    request: AnyFormRequest
  ): { errors: FormSubmissionError[] } | undefined {
    const key = this.Key(request)
    const messages = request.yar.flash(key.id)

    if (Array.isArray(messages) && messages.length) {
      return messages.at(0)
    }
  }

  setFlash(
    request: AnyFormRequest,
    message: { errors: FormSubmissionError[] }
  ) {
    const key = this.Key(request)

    request.yar.flash(key.id, message)
  }

  /**
   * Resets (removes) component states from the form state by their keys.
   * Supports both flat keys and nested paths.
   * @param request - The Hapi request object
   * @param componentNames - Array of state keys to remove. Uses lodash's unset syntax. Can be:
   * - Flat keys: `'componentName'` for top-level state
   * - Nested paths: `"upload['/my-page']"` or `'upload./my-page'` for nested state
   * @example
   * ```typescript
   * // Remove a flat component state
   * await cacheService.resetComponentStates(request, ['emailAddress'])
   *
   * // Remove nested upload state for a specific page
   * await cacheService.resetComponentStates(request, ["upload['/file-upload-page']"])
   *
   * // Remove multiple states at once
   * await cacheService.resetComponentStates(request, [
   *   'componentName',
   *   "upload['/my-page']"
   * ])
   * ```
   * @returns The updated state after removal
   */
  async resetComponentStates(request: CacheRequest, componentNames: string[]) {
    const state = await this.getState(request)

    for (const componentName of componentNames) {
      unset(state, componentName)
    }

    return this.setState(request, state)
  }

  /**
   * The key used to store user session data against.
   * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`
   * @param request - hapi request object
   * @param additionalIdentifier - appended to the id
   */
  Key(request: CacheRequest, additionalIdentifier?: ADDITIONAL_IDENTIFIER) {
    if (!request.yar.id) {
      throw new Error('No session ID found')
    }

    const state = request.params.state ?? ''
    const slug = request.params.slug ?? ''
    const key = `${request.yar.id}:${state}:${slug}:`

    return {
      segment: partition,
      id: `${key}${additionalIdentifier ?? ''}`
    }
  }
}

/**
 * State merge helper
 * 1. Merges objects (form fields)
 * 2. Overwrites arrays
 */
export function merge<StateType extends FormState | FormPayload>(
  state: StateType,
  update: object
): StateType {
  return Hoek.merge(state, update, {
    mergeArrays: false
  })
}
