/* eslint-disable no-param-reassign */
import { Request, Response, NextFunction } from 'express'

// Utils
import FiltersFormUtils from '../async-filters-form/utils'
import LocalsHelper from '../../../utils/localsHelper'
import FiltersUtils from '../../_filters/utils'
import { removeDuplicates } from '../../../utils/reportStoreHelper'
import UserStoreItemBuilder from '../../../utils/UserStoreItemBuilder'

// Types
import type ReportingService from '../../../services/reportingService'
import { ReportType, RequestFormData, RequestStatus } from '../../../types/UserReports'
import type { ExecutionData, ChildReportExecutionData } from '../../../types/ExecutionData'
import type { AsyncReportUtilsParams, RequestDataResult, RequestReportData } from '../../../types/AsyncReportUtils'
import type { RenderFiltersReturnValue, SetQueryFromFiltersResult } from '../async-filters-form/types'
import type { components } from '../../../types/api'
import type { DashboardDefinition } from '../../_dashboards/dashboard/types'
import type { Services } from '../../../types/Services'
import type DashboardService from '../../../services/dashboardService'

/**
 * Updates the store with the request details
 *
 * @param {Request} req
 * @param {Response} res
 * @param {Services} services
 * @param {SetQueryFromFiltersResult} querySummaryData
 * @param {string} executionData
 * @return {*}  {Promise<string>}
 */
export const updateStore = async ({
  req,
  res,
  services,
  queryData,
  executionData,
  childExecutionData,
}: {
  req: Request
  res: Response
  services: Services
  queryData?: SetQueryFromFiltersResult
  executionData: ExecutionData
  childExecutionData: Array<ChildReportExecutionData>
}): Promise<string> => {
  const { search, id, type } = req.body
  const { userId, definitionsPath, dpdPathFromQuery } = LocalsHelper.getValues(res)

  await removeDuplicates({ storeService: services.requestedReportService, userId, id, search })
  await removeDuplicates({ storeService: services.recentlyViewedService, userId, id, search })

  const reportData: RequestFormData = req.body

  let requestedReportData
  switch (type) {
    case ReportType.REPORT:
      requestedReportData = new UserStoreItemBuilder(reportData)
        .addExecutionData(executionData)
        .addChildExecutionData(childExecutionData)
        .addFilters(queryData.filterData)
        .addSortData(queryData.sortData)
        .addDefinitionsPath(definitionsPath, dpdPathFromQuery)
        .addRequestUrls()
        .addQuery(queryData)
        .addStatus(RequestStatus.SUBMITTED)
        .addTimestamp()
        .build()
      break
    case ReportType.DASHBOARD: {
      requestedReportData = new UserStoreItemBuilder(reportData)
        .addExecutionData(executionData)
        .addChildExecutionData(childExecutionData)
        .addFilters(queryData.filterData)
        .addDefinitionsPath(definitionsPath, dpdPathFromQuery)
        .addRequestUrls()
        .addQuery(queryData)
        .addStatus(RequestStatus.SUBMITTED)
        .addTimestamp()
        .addMetrics(JSON.parse(req.body.sections))
        .build()
      break
    }
    default:
      break
  }

  await services.requestedReportService.addReport(userId, requestedReportData)

  return requestedReportData.url.polling.pathname
}

async function requestChildReports(
  childVariants: Array<components['schemas']['ChildVariantDefinition']>,
  reportingService: ReportingService,
  token: string,
  reportId: string,
  queryData: SetQueryFromFiltersResult,
  dataProductDefinitionsPath: string,
): Promise<Array<ChildReportExecutionData>> {
  return Promise.all(
    childVariants.map((childVariant) =>
      reportingService
        .requestAsyncReport(token, reportId, childVariant.id, {
          ...queryData.query,
          dataProductDefinitionsPath,
        })
        .then((response) => {
          const { executionId, tableId } = response
          return { executionId, tableId, variantId: childVariant.id }
        }),
    ),
  )
}

const requestProduct = async ({
  req,
  res,
  token,
  dashboardService,
  reportingService,
}: {
  req: Request
  res: Response
  token: string
  dashboardService: DashboardService
  reportingService: ReportingService
}): Promise<{
  executionData: ExecutionData
  childExecutionData: Array<ChildReportExecutionData>
  queryData: SetQueryFromFiltersResult
}> => {
  const { definitionsPath: dataProductDefinitionsPath } = LocalsHelper.getValues(res)
  const { reportId, id, type } = req.body

  let fields
  let queryData
  let executionId
  let tableId
  let definition
  let childVariants: components['schemas']['ChildVariantDefinition'][] = []

  if (type === ReportType.REPORT) {
    definition = await reportingService.getDefinition(token, reportId, id, dataProductDefinitionsPath)

    fields = definition ? definition.variant.specification.fields : []
    queryData = FiltersFormUtils.setQueryFromFilters(req, fields)
    ;({ executionId, tableId } = await reportingService.requestAsyncReport(token, reportId, id, {
      ...queryData.query,
      dataProductDefinitionsPath,
    }))
    childVariants = definition.variant.childVariants ?? []
  }

  if (type === ReportType.DASHBOARD) {
    definition = await dashboardService.getDefinition(token, id, reportId, dataProductDefinitionsPath)

    fields = definition ? definition.filterFields : []
    queryData = FiltersFormUtils.setQueryFromFilters(req, fields)
    ;({ executionId, tableId } = await dashboardService.requestAsyncDashboard(token, reportId, id, {
      ...queryData.query,
      dataProductDefinitionsPath,
    }))
  }

  const childExecutionData = await requestChildReports(
    childVariants,
    reportingService,
    token,
    reportId,
    queryData,
    dataProductDefinitionsPath,
  )

  return {
    executionData: { executionId, tableId },
    childExecutionData,
    queryData,
  }
}

const renderDashboardRequestData = async ({
  token,
  reportId,
  definitionPath,
  services,
  definition,
}: {
  token: string
  reportId: string
  definitionPath: string
  services: Services
  definition: DashboardDefinition
}) => {
  const productDefinitions = await services.reportingService.getDefinitions(token, definitionPath)
  const productDefinition = productDefinitions.find(
    (def: components['schemas']['ReportDefinitionSummary']) => def.id === reportId,
  )
  const reportName = productDefinition.name
  const { name, description, sections, filterFields: fields } = definition

  return {
    reportName,
    name,
    description,
    sections: sections || [],
    fields,
  }
}

const renderReportRequestData = async (definition: components['schemas']['SingleVariantReportDefinition']) => {
  return {
    definition,
    reportName: definition.name,
    name: definition.variant.name,
    description: definition.variant.description || definition.description,
    template: definition.variant.specification,
    fields: definition?.variant?.specification?.fields,
    interactive: definition?.variant?.interactive,
  }
}

const getDefintionByType = async (req: Request, res: Response, next: NextFunction, services: Services) => {
  const { token, definitionsPath } = LocalsHelper.getValues(res)
  const { reportId, id, variantId, type } = req.params

  let definition
  if (type === ReportType.REPORT) {
    definition = await services.reportingService.getDefinition(token, reportId, variantId || id, definitionsPath)
  }

  if (type === ReportType.DASHBOARD) {
    definition = await services.dashboardService.getDefinition(token, variantId || id, reportId, definitionsPath)
  }

  return definition
}

export default {
  /**
   * Sends the request for the async report
   *
   * @param {AsyncReportUtilsParams} { req, res, services }
   * @return {*}
   */
  request: async ({ req, res, services }: AsyncReportUtilsParams) => {
    const { token } = LocalsHelper.getValues(res)
    const requestArgs = { req, res, token }

    const { executionData, queryData, childExecutionData } = await requestProduct({
      ...requestArgs,
      dashboardService: services.dashboardService,
      reportingService: services.reportingService,
    })

    let redirect = ''
    if (executionData) {
      redirect = await updateStore({
        req,
        res,
        services,
        queryData,
        executionData,
        childExecutionData,
      })
    }

    return redirect
  },

  cancelRequest: async ({ req, res, services }: AsyncReportUtilsParams) => {
    const { token, userId, definitionsPath } = LocalsHelper.getValues(res)
    const { reportId, id, executionId, type } = req.body

    let service
    if (type === ReportType.REPORT) service = services.reportingService
    if (type === ReportType.DASHBOARD) service = services.dashboardService

    const response = await service.cancelAsyncRequest(token, reportId, id, executionId, definitionsPath)

    if (response && response.cancellationSucceeded) {
      await services.requestedReportService.updateStatus(executionId, userId, RequestStatus.ABORTED)
    }
  },

  /**
   * Returns the data required for rendering the async filters component
   *
   * @param {AsyncReportUtilsParams} { req, res, dataSources }
   * @return {*}
   */
  renderRequest: async ({ req, res, services, next }: AsyncReportUtilsParams): Promise<RequestDataResult | boolean> => {
    try {
      const { token, csrfToken, definitionsPath: definitionPath, dpdPathFromQuery } = LocalsHelper.getValues(res)
      const { reportId, type, id } = req.params
      const { definition } = req.body
      const definitionApiArgs = { token, reportId, definitionPath, services }

      let name
      let reportName
      let description
      let template
      let fields: components['schemas']['FieldDefinition'][]
      let sections
      let interactive
      let defaultInteractiveQueryString
      let filtersData

      if (type === ReportType.REPORT) {
        ;({ name, reportName, description, fields, interactive } = await renderReportRequestData(definition))
      }

      if (type === ReportType.DASHBOARD) {
        ;({ name, reportName, description, sections, fields } = await renderDashboardRequestData({
          ...definitionApiArgs,
          definition,
        }))
      }

      if (fields) {
        filtersData = <RenderFiltersReturnValue>await FiltersFormUtils.renderFilters(fields, interactive)
        filtersData.filters = FiltersUtils.setFilterValuesFromRequest(filtersData.filters, req)
        defaultInteractiveQueryString = FiltersUtils.setFilterQueryFromFilterDefinition(fields, true)
      }

      const reportData: RequestReportData = {
        reportName,
        name,
        description,
        reportId,
        id,
        ...(dpdPathFromQuery && { definitionPath }),
        ...(defaultInteractiveQueryString?.length && { defaultInteractiveQueryString }),
        csrfToken,
        template,
        sections,
        type: type as ReportType,
      }

      return {
        title: `Request ${type}`,
        filtersDescription: `Customise your ${type} using the filters below and submit your request.`,
        filtersData,
        reportData,
      }
    } catch (error) {
      next(error)
      return false
    }
  },

  getFiltersFromReqBody: (req: Request) => {
    return Object.keys(req.body)
      .filter((attr) => attr.includes('filters.'))
      .filter((attr) => !!req.body[attr])
      .map((attr) => {
        return { name: attr, value: req.body[attr] }
      })
  },

  getDefintionByType,
}
