import { List, Map } from 'immutable'
import React, { useEffect, useReducer, useState } from 'react'
import { Dataset } from './models/dataset'
import { Experiment } from './models/experiment'
import { Project } from './models/project'
import { STATUS_API_CALL_INTERVAL } from './constants'
import { Status } from './models/status'
import { toast } from 'react-toastify'

export const GlobalContext = React.createContext({})

const showErrorToast = (message) => {
  toast.error(message, {
    position: 'bottom-center',
    autoClose: 5000,
    hideProgressBar: true,
    closeOnClick: true,
    pauseOnHover: true,
    draggable: false
  })
}

const showNetworkErrorToast = (err) => {
  const { url, method } = err.config
  let message = `${method.toUpperCase()}: ${url} "${err.message}"`
  if (err.response) {
    const errorMessage = JSON.stringify(err.response.data)
    const { status } = err.response
    message = `${method.toUpperCase()}: ${url} "${status}: ${errorMessage}"`
  }
  showErrorToast(message)
}

const updateFunc = (data, setData) => (datum) => {
  const index = data.findIndex((d) => d.id === datum.id)
  setData(data.set(index, datum))
  return datum.update().catch((err) => showNetworkErrorToast(err))
}

const deleteFunc = (data, setData) => (datum) => {
  const newData = data.filter((d) => d.id !== datum.id)
  setData(newData)
  return datum.delete().catch((err) => showNetworkErrorToast(err))
}

// Reducers
const experimentReducer = (experiments, action) => {
  const actions = {
    fetch: () => experiments.set(action.projectId, List(action.experiments)),
    create: () => {
      if (experiments.has(action.projectId)) {
        const target = experiments.get(action.projectId)
        const newExperiments = target.unshift(action.experiment)
        return experiments.set(action.projectId, newExperiments)
      }
      return experiments.set(action.projectId, List([action.experiment]))
    },
    update: () => {
      const { experiment } = action
      const target = experiments.get(action.projectId)
      const index = target.findIndex((ex) => ex.id === experiment.id)
      const newExperiments = target.set(index, experiment)
      return experiments.set(action.projectId, newExperiments)
    },
    delete: () => {
      const { experiment } = action
      const target = experiments.get(action.projectId)
      const newExperiments = target.filter((ex) => ex.id !== experiment.id)
      return experiments.set(action.projectId, newExperiments)
    }
  }
  return actions[action.type]()
}

export const GlobalProvider = ({ children }) => {
  const [datasets, setDatasets] = useState(List([]))
  const [projects, setProjects] = useState(List([]))
  const [experiments, dispatch] = useReducer(experimentReducer, Map({}))
  const [examples, setExamples] = useState(Map({}))
  const [status, setStatus] = useState({})
  const [statusTime, setStatusTime] = useState(Date.now())

  // Initialization
  useEffect(() => {
    Dataset.getAll()
      .then((newDatasets) => setDatasets(List(newDatasets)))
      .catch((err) => showNetworkErrorToast(err))

    Project.getAll()
      .then((newProjects) => setProjects(List(newProjects)))
      .catch((err) => showNetworkErrorToast(err))

    Status.get()
      .then((newStatus) => setStatus(newStatus))
      .catch((err) => showNetworkErrorToast(err))
  }, [])

  // Periodical API calls
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      setStatusTime(Date.now())
      Status.get()
        .then((newStatus) => setStatus(newStatus))
        .catch((err) => showNetworkErrorToast(err))
    }, STATUS_API_CALL_INTERVAL)
    return () => {
      clearTimeout(timeoutId)
    }
  }, [statusTime])

  // Actions

  const uploadDataset = (file, isImage, zipFile, progressCallback) => (
    Dataset.upload(file, isImage, zipFile, progressCallback)
      .then((dataset) => {
        setDatasets(datasets.unshift(dataset))
        return dataset
      })
      .catch((err) => showNetworkErrorToast(err))
  )

  const deleteDataset = deleteFunc(datasets, setDatasets)

  const updateDataset = updateFunc(datasets, setDatasets)

  const createProject = (datasetId, name, algorithm, progressCallback) => (
    Project.create(datasetId, name, algorithm, progressCallback)
      .then((project) => {
        setProjects(projects.unshift(project))
        return project
      })
      .catch((err) => showNetworkErrorToast(err))
  )

  const deleteProject = deleteFunc(projects, setProjects)

  const updateProject = updateFunc(projects, setProjects)

  const fetchExperiments = (projectId) => (
    Experiment.getAll(projectId)
      .then((newExperiments) => {
        dispatch({
          type: 'fetch',
          projectId: projectId,
          experiments: newExperiments
        })
        return newExperiments
      })
      .catch((err) => showNetworkErrorToast(err))
  )

  const createExperiment = (projectId, name, config, progressCallback) => (
    Experiment.create(projectId, name, config, progressCallback)
      .then((experiment) => {
        dispatch({
          type: 'create',
          projectId: projectId,
          experiment: experiment
        })
        return experiment
      })
      .catch((err) => showNetworkErrorToast(err))
  )

  const deleteExperiment = (experiment) => {
    dispatch({
      type: 'delete',
      projectId: experiment.projectId,
      experiment: experiment
    })
    return experiment.delete().catch((err) => showNetworkErrorToast(err))
  }

  const updateExperiment = (experiment) => {
    dispatch({
      type: 'update',
      projectId: experiment.projectId,
      experiment: experiment
    })
    return experiment.update().catch((err) => showNetworkErrorToast(err))
  }

  const cancelExperiment = (experiment) => {
    dispatch({
      type: 'update',
      projectId: experiment.projectId,
      experiment: experiment.set('isActive', false)
    })
    return experiment.cancel().catch((err) => showNetworkErrorToast(err))
  }

  const fetchExampleObservations = (dataset) => {
    dataset.getExampleObservations()
      .then((observations) => {
        const newExamples = examples.set(dataset.id, observations)
        setExamples(newExamples)
        return observations
      })
      .catch((err) => showErrorToast(err))
  }

  return (
    <GlobalContext.Provider
      value={{
        datasets,
        projects,
        experiments,
        examples,
        status,
        uploadDataset,
        deleteDataset,
        updateDataset,
        createProject,
        updateProject,
        deleteProject,
        fetchExperiments,
        createExperiment,
        deleteExperiment,
        updateExperiment,
        cancelExperiment,
        fetchExampleObservations,
        showErrorToast,
        showNetworkErrorToast
      }}
    >
      {children}
    </GlobalContext.Provider>
  )
}