import { hasComponentsEvenIfNoNext } from '@defra/forms-model';
import Boom from '@hapi/boom';
import { StatusCodes } from 'http-status-codes';
import { COMPONENT_STATE_ERROR, PAYMENT_EXPIRED_NOTIFICATION } from "../../../constants.js";
import { ComponentCollection } from "../components/ComponentCollection.js";
import { PaymentField } from "../components/PaymentField.js";
import { checkEmailAddressForLiveFormSubmission, checkFormStatus, createError, getCacheService } from "../helpers.js";
import { SummaryViewModel } from "../models/index.js";
import { QuestionPageController } from "./QuestionPageController.js";
import { InvalidComponentStateError, PaymentErrorTypes, PaymentPreAuthError, PaymentSubmissionError } from "./errors.js";
import { buildMainRecords, buildRepeaterRecords } from "./helpers/submission.js";
import { DEFAULT_PAYMENT_HELP_URL, formatCurrency, formatPaymentDate } from "../../payment/helper.js";
import { FormAction } from "../../../routes/types.js";
export class SummaryPageController extends QuestionPageController {
  allowSaveAndExit = true;

  /**
   * The controller which is used when Page["controller"] is defined as "./pages/summary.js"
   */

  /**
   * Finds the PaymentField component across all pages in the model.
   * Payment pages are skipped in the normal page walk
   */
  findPaymentField() {
    return this.model.pages.flatMap(page => page.collection.fields).find(field => field instanceof PaymentField);
  }
  constructor(model, pageDef) {
    super(model, pageDef);
    this.viewName = 'summary';

    // Components collection
    this.collection = new ComponentCollection(hasComponentsEvenIfNoNext(pageDef) ? pageDef.components : [], {
      model,
      page: this
    });
  }
  getSummaryViewModel(request, context) {
    const viewModel = new SummaryViewModel(request, this, context);
    const {
      query
    } = request;
    const {
      payload,
      errors,
      state
    } = context;
    const paymentField = this.findPaymentField();
    if (paymentField) {
      const resolvedAmount = PaymentField.resolveAmount(paymentField.options, this.model, state);
      const paymentState = paymentField.getPaymentStateFromState(state);
      if (paymentState?.amount === resolvedAmount) {
        viewModel.paymentState = paymentState;
        viewModel.paymentDetails = this.buildPaymentDetails(paymentField, paymentState);
      }
      if (resolvedAmount > 0) {
        viewModel.paymentRequired = true;
      }
      if (paymentState && paymentState.preAuth?.status === 'success' && paymentState.amount === resolvedAmount) {
        viewModel.paymentPreAuthorized = true;
      }
    }
    const components = this.collection.getViewModel(payload, errors, query);
    viewModel.backLink = this.getBackLink(request, context);
    viewModel.feedbackLink = this.feedbackLink;
    viewModel.phaseTag = this.phaseTag;
    viewModel.components = components;
    viewModel.allowSaveAndExit = this.shouldShowSaveAndExit(request.server);
    viewModel.errors = errors;
    return viewModel;
  }
  buildPaymentDetails(paymentField, paymentState) {
    const rows = [{
      key: {
        text: 'Payment for'
      },
      value: {
        text: paymentState.description
      }
    }, {
      key: {
        text: 'Total amount'
      },
      value: {
        text: formatCurrency(paymentState.amount)
      }
    }, {
      key: {
        text: 'Reference'
      },
      value: {
        text: paymentState.reference
      }
    }];
    if (paymentState.preAuth?.createdAt) {
      rows.push({
        key: {
          text: 'Date of payment'
        },
        value: {
          text: formatPaymentDate(paymentState.preAuth.createdAt)
        }
      });
    }
    return {
      title: {
        text: 'Payment details'
      },
      summaryList: {
        rows
      }
    };
  }

  /**
   * Returns an async function. This is called in plugin.ts when there is a GET request at `/{id}/{path*}`,
   */
  makeGetRouteHandler() {
    return async (request, context, h) => {
      const {
        viewName
      } = this;

      // After GOV.UK Pay callback, auto-submit the form instead of
      // showing CYA again. The payment is already pre-authorized.
      if (request.query.paymentComplete === 'true') {
        return this.handleFormSubmit(request, context, h);
      }
      await this.reconcilePaymentState(request, context);
      const viewModel = this.getSummaryViewModel(request, context);
      viewModel.hasMissingNotificationEmail = await this.hasMissingNotificationEmail(request, context);
      return h.view(viewName, viewModel);
    };
  }

  /**
   * Checks if the resolved payment amount has changed since pre-auth
   * and invalidates stale payment state if so.
   */
  async reconcilePaymentState(request, context) {
    const paymentField = this.findPaymentField();
    if (!paymentField) {
      return;
    }
    const resolvedAmount = PaymentField.resolveAmount(paymentField.options, this.model, context.state);
    const paymentState = paymentField.getPaymentStateFromState(context.state);
    if (paymentState && paymentState.amount !== resolvedAmount) {
      const cacheService = getCacheService(request.server);
      await cacheService.resetComponentStates(request, [paymentField.name]);
    }
  }

  /**
   * Returns an async function. This is called in plugin.ts when there is a POST request at `/{id}/{path*}`.
   * If a form is incomplete, a user will be redirected to the start page.
   */
  makePostRouteHandler() {
    return async (request, context, h) => {
      const {
        action
      } = request.payload;
      if (action === FormAction.SaveAndExit) {
        return this.handleSaveAndExit(request, context, h);
      }
      return this.handleFormSubmit(request, context, h);
    };
  }
  async handleFormSubmit(request, context, h) {
    const {
      model
    } = this;
    const {
      params
    } = request;
    const cacheService = getCacheService(request.server);
    const {
      formsService
    } = this.model.services;
    const {
      getFormMetadata
    } = formsService;
    const formMetadata = await getFormMetadata(params.slug);
    const {
      notificationEmail
    } = formMetadata;
    const {
      isPreview
    } = checkFormStatus(request.params);
    checkEmailAddressForLiveFormSubmission(notificationEmail, isPreview);
    if (notificationEmail) {
      const viewModel = this.getSummaryViewModel(request, context);
      try {
        await submitForm(context, formMetadata, request, viewModel, model, notificationEmail);
      } catch (error) {
        return this.handleSubmissionError(error, request, h);
      }
    }
    await cacheService.setConfirmationState(request, {
      confirmed: true,
      formId: context.state.formId,
      referenceNumber: context.referenceNumber
    });
    await cacheService.clearState(request);
    return this.proceed(request, h, this.getStatusPath());
  }

  /**
   * Handles errors during form submission
   */
  async handleSubmissionError(error, request, h) {
    if (error instanceof InvalidComponentStateError) {
      return this.handleInvalidComponentStateError(error, request, h);
    }
    if (error instanceof PaymentPreAuthError) {
      return this.handlePaymentPreAuthError(error, request, h);
    }
    if (error instanceof PaymentSubmissionError) {
      return this.handlePaymentSubmissionError(error, request, h);
    }
    throw error;
  }

  /**
   * Handles InvalidComponentStateError during submission
   */
  async handleInvalidComponentStateError(error, request, h) {
    const cacheService = getCacheService(request.server);
    const govukError = createError(error.component.name, error.userMessage);
    request.yar.flash(COMPONENT_STATE_ERROR, govukError, true);
    await cacheService.resetComponentStates(request, error.getStateKeys());
    return this.proceed(request, h, error.component.page?.path);
  }

  /**
   * Handles PaymentPreAuthError during submission
   */
  async handlePaymentPreAuthError(error, request, h) {
    const cacheService = getCacheService(request.server);
    if (error.shouldResetState) {
      await cacheService.resetComponentStates(request, error.getStateKeys());
      if (error.errorType === PaymentErrorTypes.PaymentExpired) {
        request.yar.flash(PAYMENT_EXPIRED_NOTIFICATION, true, true);
        return this.proceed(request, h, error.component.page?.path);
      }
    }

    // Payment page is skipped in the page walk, so proceed() would redirect
    // to an earlier page. For PaymentIncomplete, redirect directly to the
    // payment page using its href.
    if (error.errorType === PaymentErrorTypes.PaymentIncomplete && error.component.page) {
      return h.redirect(error.component.page.getHref(error.component.page.path)).code(StatusCodes.SEE_OTHER);
    }
    const govukError = createError(error.component.name, error.userMessage);
    request.yar.flash(COMPONENT_STATE_ERROR, govukError, true);
    const redirectPath = error.shouldResetState ? error.component.page?.path : undefined;
    return this.proceed(request, h, redirectPath);
  }

  /**
   * Handles PaymentSubmissionError during submission
   */
  handlePaymentSubmissionError(error, request, h) {
    const helpUrl = error.helpLink ?? DEFAULT_PAYMENT_HELP_URL;
    const helpLinkHtml = ` or you can <a href="${helpUrl}" target="_blank" rel="noopener noreferrer" class="govuk-link">contact us (opens in new tab)</a> and quote your reference number to arrange a refund`;
    const govukError = createError('submission', `There was a problem and your form was not submitted. Try submitting the form again${helpLinkHtml}.`);
    request.yar.flash(COMPONENT_STATE_ERROR, govukError, true);
    return this.proceed(request, h);
  }
  get postRouteOptions() {
    return {
      ext: {
        onPreHandler: {
          method(request, h) {
            return h.continue;
          }
        }
      }
    };
  }
}
export async function submitForm(context, formMetadata, request, summaryViewModel, model, emailAddress) {
  await finaliseComponents(request, formMetadata, context, model);
  const paymentWasCaptured = hasPaymentBeenCaptured(context, model);
  const formStatus = checkFormStatus(request.params);
  const logTags = ['submit', 'submissionApi'];
  request.logger.info(logTags, 'Preparing email', formStatus);
  const items = getFormSubmissionData(summaryViewModel.context, summaryViewModel.details, model);
  try {
    request.logger.info(logTags, 'Submitting data');
    const submitResponse = await submitData(model, items, emailAddress, request.yar.id);
    if (submitResponse === undefined) {
      throw Boom.badRequest('Unexpected empty response from submit api');
    }
    await model.services.outputService.submit(context, request, model, emailAddress, items, submitResponse, formMetadata);
  } catch (err) {
    if (paymentWasCaptured) {
      throw new PaymentSubmissionError(context.referenceNumber, formMetadata.contact?.online?.url);
    }
    throw err;
  }
}

/**
 * Checks if any payment component has been captured
 */
function hasPaymentBeenCaptured(context, model) {
  for (const page of model.pages) {
    for (const field of page.collection.fields) {
      if (field instanceof PaymentField) {
        const paymentState = field.getPaymentStateFromState(context.state);
        if (paymentState?.capture?.status === 'success') {
          return true;
        }
      }
    }
  }
  return false;
}

/**
 * Finalises any components that need post-processing before form submission. Candidates usually involve
 * those that have external state.
 * Examples include:
 * - file uploads which are 'persisted' before submission
 * - payments which are 'captured' before submission
 */
async function finaliseComponents(request, metadata, context, model) {
  // Get fields from relevant pages (normal components)
  // plus PaymentField from all pages (payment page is skipped in the page walk)
  const allFields = new Set([...context.relevantPages.flatMap(page => page.collection.fields), ...model.pages.flatMap(page => page.collection.fields).filter(field => field instanceof PaymentField)]);
  for (const component of allFields) {
    await component.onSubmit(request, metadata, context);
  }
}
function submitData(model, items, retrievalKey, sessionId) {
  const {
    formSubmissionService
  } = model.services;
  const {
    submit
  } = formSubmissionService;
  const payload = {
    sessionId,
    retrievalKey,
    main: buildMainRecords(items),
    repeaters: buildRepeaterRecords(items)
  };
  return submit(payload);
}
export function getFormSubmissionData(context, details, model) {
  const items = context.relevantPages.map(({
    href
  }) => details.flatMap(({
    items
  }) => items.filter(({
    page
  }) => page.href === href))).flat();
  const paymentItems = getPaymentFieldItems(context, model);
  return [...items, ...paymentItems];
}

/**
 * Gets DetailItems for PaymentField components
 * PaymentField is excluded from summaryDetails for UI but needs to be in submission data
 */
function getPaymentFieldItems(context, model) {
  const items = [];
  for (const page of model.pages) {
    for (const field of page.collection.fields) {
      if (field instanceof PaymentField) {
        items.push({
          name: field.name,
          page,
          title: field.title,
          label: field.label,
          field,
          state: context.state,
          href: page.href,
          value: field.getDisplayStringFromState(context.state)
        });
      }
    }
  }
  return items;
}
//# sourceMappingURL=SummaryPageController.js.map