import { Button, Link, Paper, Step, StepButton, StepLabel, Stepper, Typography } from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { Formik, setNestedObjectValues } from 'formik'
import _ from 'lodash'
import React, { useEffect, useRef, useState } from 'react'
import { Prompt } from 'react-router-dom'
import * as yup from 'yup'

import GeneralErrorAlert from 'src/components/general/GeneralErrorAlert'
import { ExperimentFormData } from 'src/lib/form-data'
import { AutocompleteItem, experimentFullNewSchema, Metric, Segment } from 'src/lib/schemas'
import { DataSourceResult } from 'src/utils/data-loading'

import LoadingButtonContainer from '../../general/LoadingButtonContainer'
import Audience from './Audience'
import BasicInfo from './BasicInfo'
import Beginning from './Beginning'
import Metrics from './Metrics'

export interface ExperimentFormCompletionBag {
  userCompletionDataSource: DataSourceResult<AutocompleteItem[]>
  eventCompletionDataSource: DataSourceResult<AutocompleteItem[]>
  exclusionGroupCompletionDataSource: DataSourceResult<AutocompleteItem[]>
}

enum StageId {
  Beginning,
  BasicInfo,
  Audience,
  Metrics,
  Submit,
}

interface Stage {
  id: StageId
  title: string
  validatableFields: string[]
}

const stages: Stage[] = [
  {
    id: StageId.Beginning,
    title: 'Start',
    validatableFields: ['experiment.p2Url'],
  },
  {
    id: StageId.BasicInfo,
    title: 'Basic Info',
    validatableFields: [
      'experiment.name',
      'experiment.description',
      'experiment.startDatetime',
      'experiment.endDatetime',
      'experiment.ownerLogin',
    ],
  },
  {
    id: StageId.Audience,
    title: 'Audience',
    validatableFields: [
      'experiment.platform',
      'experiment.existingUsersAllowed',
      'experiment.segments',
      'experiment.variations',
    ],
  },
  {
    id: StageId.Metrics,
    title: 'Metrics',
    validatableFields: ['experiment.metricAssignments', 'experiment.exposureEvents'],
  },
  {
    id: StageId.Submit,
    title: 'Submit',
    validatableFields: [],
  },
]

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      display: 'flex',
      flexDirection: 'column',
    },
    navigation: {
      flexShrink: 0,
      marginRight: theme.spacing(1),
      marginTop: theme.spacing(2),
      maxWidth: 600,
      [theme.breakpoints.down('xs')]: {
        '& .MuiStepLabel-labelContainer': {
          display: 'none',
        },
      },
    },
    form: {
      flex: 1,
      display: 'flex',
    },
    formPart: {
      flex: '1  0',
      maxWidth: 980,
      padding: theme.spacing(2, 0),
    },
    formPartActions: {
      display: 'flex',
      justifyContent: 'flex-end',
      '& .MuiButton-root': {
        marginLeft: theme.spacing(2),
      },
    },
    paper: {
      padding: theme.spacing(3, 4),
      marginBottom: theme.spacing(2),
    },
  }),
)

const ExperimentForm = ({
  indexedMetrics,
  indexedSegments,
  initialExperiment,
  onSubmit,
  completionBag,
  formSubmissionError,
}: {
  indexedMetrics: Record<number, Metric>
  indexedSegments: Record<number, Segment>
  initialExperiment: ExperimentFormData
  completionBag: ExperimentFormCompletionBag
  onSubmit: (formData: unknown) => Promise<void>
  formSubmissionError?: Error
}): JSX.Element => {
  const classes = useStyles()

  const rootRef = useRef<HTMLDivElement>(null)

  const [currentStageId, setActiveStageId] = useState<StageId>(StageId.Beginning)
  const currentStageIndex = stages.findIndex((stage) => stage.id === currentStageId)

  const [completeStages, setCompleteStages] = useState<StageId[]>([])
  const [errorStages, setErrorStages] = useState<StageId[]>([])

  useEffect(() => {
    rootRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' })
  }, [currentStageId])

  // Preventing accidental non-react-router navigate-aways:
  const preventSubmissionRef = useRef<boolean>(false)
  useEffect(() => {
    // istanbul ignore next; trivial
    // Sure we can test that these lines run but what is really important is how they
    // behave in browsers, which IMO is too complicated to write tests for in this case.
    const eventListener = (event: BeforeUnloadEvent) => {
      if (preventSubmissionRef.current) {
        event.preventDefault()
        // Chrome requires returnValue to be set
        event.returnValue = ''
      }
    }
    window.addEventListener('beforeunload', eventListener)
    return () => {
      window.removeEventListener('beforeunload', eventListener)
    }
  }, [])

  return (
    <Formik
      initialValues={{ experiment: initialExperiment }}
      onSubmit={onSubmit}
      validationSchema={yup.object({ experiment: experimentFullNewSchema })}
    >
      {(formikProps) => {
        const getStageErrors = async (stage: Stage) => {
          return _.pick(await formikProps.validateForm(), stage.validatableFields)
        }

        const isStageValid = async (stage: Stage): Promise<boolean> => {
          const errors = await formikProps.validateForm()
          return !stage.validatableFields.some((field) => _.get(errors, field))
        }

        const updateStageState = async (stage: Stage) => {
          if (stage.id === StageId.Submit) {
            return
          }

          if (await isStageValid(stage)) {
            setErrorStages((prevValue) => _.difference(prevValue, [stage.id]))
            setCompleteStages((prevValue) => _.union(prevValue, [stage.id]))
          } else {
            setErrorStages((prevValue) => _.union(prevValue, [stage.id]))
            setCompleteStages((prevValue) => _.difference(prevValue, [stage.id]))
          }
        }

        const changeStage = (stageId: StageId) => {
          setActiveStageId(stageId)
          void updateStageState(stages[currentStageIndex])

          if (errorStages.includes(stageId)) {
            void getStageErrors(stages[stageId]).then((stageErrors) =>
              formikProps.setTouched(setNestedObjectValues(stageErrors, true)),
            )
          }
          if (stageId === StageId.Submit) {
            stages.map(updateStageState)
          }
        }

        const prevStage = () => {
          const prevStage = stages[currentStageIndex - 1]
          prevStage && changeStage(prevStage.id)
        }
        const nextStage = () => {
          const nextStage = stages[currentStageIndex + 1]
          nextStage && changeStage(nextStage.id)
        }

        preventSubmissionRef.current = formikProps.dirty && !formikProps.isSubmitting

        return (
          <div className={classes.root}>
            {/* This is required for React Router navigate-away prevention */}
            <Prompt
              when={preventSubmissionRef.current}
              message='You have unsaved data, are you sure you want to leave?'
            />
            <Paper className={classes.navigation}>
              <Stepper nonLinear activeStep={currentStageId} orientation='horizontal'>
                {stages.map((stage) => (
                  <Step key={stage.id} completed={stage.id !== currentStageId && completeStages.includes(stage.id)}>
                    <StepButton onClick={() => changeStage(stage.id)}>
                      <StepLabel error={stage.id !== currentStageId && errorStages.includes(stage.id)}>
                        {stage.title}
                      </StepLabel>
                    </StepButton>
                  </Step>
                ))}
              </Stepper>
            </Paper>
            <div ref={rootRef}>
              {/* Explanation: This should be fine as we aren't hiding behaviour that can't be accessed otherwise. */}
              {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
              <form className={classes.form} onSubmit={formikProps.handleSubmit} noValidate>
                {/* Prevent implicit submission of the form on enter. */}
                {/* See https://stackoverflow.com/a/51507806 */}
                <button type='submit' disabled style={{ display: 'none' }} aria-hidden='true'></button>
                {currentStageId === StageId.Beginning && (
                  <div className={classes.formPart}>
                    <Paper className={classes.paper}>
                      <Beginning />
                    </Paper>
                    <div className={classes.formPartActions}>
                      <Button onClick={nextStage} variant='contained' color='primary'>
                        Begin
                      </Button>
                    </div>
                  </div>
                )}
                {currentStageId === StageId.BasicInfo && (
                  <div className={classes.formPart}>
                    <Paper className={classes.paper}>
                      <BasicInfo completionBag={completionBag} />
                    </Paper>
                    <div className={classes.formPartActions}>
                      <Button onClick={prevStage}>Previous</Button>
                      <Button onClick={nextStage} variant='contained' color='primary'>
                        Next
                      </Button>
                    </div>
                  </div>
                )}
                {currentStageId === StageId.Audience && (
                  <div className={classes.formPart}>
                    <Paper className={classes.paper}>
                      <Audience {...{ formikProps, indexedSegments, completionBag }} />
                    </Paper>
                    <div className={classes.formPartActions}>
                      <Button onClick={prevStage}>Previous</Button>
                      <Button onClick={nextStage} variant='contained' color='primary'>
                        Next
                      </Button>
                    </div>
                  </div>
                )}
                {currentStageId === StageId.Metrics && (
                  <div className={classes.formPart}>
                    <Paper className={classes.paper}>
                      <Metrics {...{ indexedMetrics, completionBag, formikProps }} />
                    </Paper>
                    <div className={classes.formPartActions}>
                      <Button onClick={prevStage}>Previous</Button>
                      <Button onClick={nextStage} variant='contained' color='primary'>
                        Next
                      </Button>
                    </div>
                  </div>
                )}
                {currentStageId === StageId.Submit && (
                  <div className={classes.formPart}>
                    <Paper className={classes.paper}>
                      <Typography variant='h4' gutterBottom>
                        Confirm and Submit Your Experiment
                      </Typography>
                      <Typography variant='body2' gutterBottom>
                        Now is a good time to{' '}
                        <Link href='https://github.com/Automattic/experimentation-platform/wiki' target='_blank'>
                          check our wiki&apos;s experiment creation checklist
                        </Link>{' '}
                        and confirm everything is in place.
                      </Typography>

                      <Typography variant='body2' gutterBottom>
                        Once you submit your experiment it will be set to staging, where it can be edited up until you
                        set it to running.
                      </Typography>
                      <Typography variant='body2' gutterBottom>
                        <strong> When you are ready, click the Submit button below.</strong>
                      </Typography>
                    </Paper>
                    <GeneralErrorAlert error={formSubmissionError} />
                    <div className={classes.formPartActions}>
                      <Button onClick={prevStage}>Previous</Button>
                      <LoadingButtonContainer isLoading={formikProps.isSubmitting}>
                        <Button
                          type='submit'
                          variant='contained'
                          color='secondary'
                          disabled={formikProps.isSubmitting || errorStages.length > 0}
                        >
                          Submit
                        </Button>
                      </LoadingButtonContainer>
                    </div>
                  </div>
                )}
              </form>
            </div>
          </div>
        )
      }}
    </Formik>
  )
}

export default ExperimentForm