import {replace, RouterAction, push} from 'react-router-redux'
import _ from 'lodash'
import qs from 'qs'
import {Dispatch} from 'redux'

import {
  getDashboards as getDashboardsAJAX,
  getDashboard as getDashboardAJAX,
  updateDashboard as updateDashboardAJAX,
  deleteDashboard as deleteDashboardAJAX,
  updateDashboardCell as updateDashboardCellAJAX,
  addDashboardCell as addDashboardCellAJAX,
  deleteDashboardCell as deleteDashboardCellAJAX,
  createDashboard as createDashboardAJAX,
} from 'src/dashboards/apis'
import {getMe} from 'src/shared/apis/auth'
import {hydrateTemplates} from 'src/tempVars/utils/graph'

import {notify} from 'src/shared/actions/notifications'
import {errorThrown} from 'src/shared/actions/errors'
import {stripPrefix} from 'src/utils/basepath'

import {
  templateSelectionsFromQueryParams,
  templateSelectionsFromTemplates,
} from 'src/dashboards/utils/tempVars'
import {validTimeRange, validAbsoluteTimeRange} from 'src/dashboards/utils/time'
import {getClonedDashboardCell} from 'src/dashboards/utils/cellGetters'
import {
  notifyDashboardDeleted,
  notifyDashboardDeleteFailed,
  notifyCellAdded,
  notifyCellDeleted,
  notifyDashboardImportFailed,
  notifyDashboardImported,
  notifyDashboardNotFound,
  notifyInvalidZoomedTimeRangeValueInURLQuery,
  notifyInvalidTimeRangeValueInURLQuery,
  notifyInvalidQueryParam,
} from 'src/shared/copy/notifications'

import {getDeep} from 'src/utils/wrappers'

import {DEFAULT_TIME_RANGE} from 'src/shared/data/timeRanges'

// Types
import {
  Dashboard,
  Cell,
  TimeRange,
  Source,
  Template,
  TemplateValue,
  TemplateType,
  Status,
  RefreshRate,
} from 'src/types'
import {NewDefaultCell} from 'src/types/dashboards'

export enum ActionType {
  LoadDashboards = 'LOAD_DASHBOARDS',
  LoadDashboard = 'LOAD_DASHBOARD',
  SetDashboardTimeRange = 'SET_DASHBOARD_TIME_RANGE',
  SetDashboardZoomedTimeRange = 'SET_DASHBOARD_ZOOMED_TIME_RANGE',
  UpdateDashboard = 'UPDATE_DASHBOARD',
  CreateDashboard = 'CREATE_DASHBOARD',
  DeleteDashboard = 'DELETE_DASHBOARD',
  DeleteDashboardFailed = 'DELETE_DASHBOARD_FAILED',
  AddDashboardCell = 'ADD_DASHBOARD_CELL',
  DeleteDashboardCell = 'DELETE_DASHBOARD_CELL',
  SyncDashboardCell = 'SYNC_DASHBOARD_CELL',
  EditCellQueryStatus = 'EDIT_CELL_QUERY_STATUS',
  TemplateVariableLocalSelected = 'TEMPLATE_VARIABLE_LOCAL_SELECTED',
  UpdateTemplates = 'UPDATE_TEMPLATES',
  SetHoverTime = 'SET_HOVER_TIME',
  SetActiveCell = 'SET_ACTIVE_CELL',
  SetDashboardTimeV1 = 'SET_DASHBOARD_TIME_V1',
  SetDashboardRefresh = 'SET_DASHBOARD_REFRESH',
  RetainRangesDashboardTimeV1 = 'RETAIN_RANGES_DASHBOARD_TIME_V1',
  RetainDashboardRefresh = 'RETAIN_DASHBOARD_REFRESH',
}

interface LoadDashboardsAction {
  type: ActionType.LoadDashboards
  payload: {
    dashboards: Dashboard[]
    dashboardID: string
  }
}

interface LoadDashboardAction {
  type: ActionType.LoadDashboard
  payload: {
    dashboard: Dashboard
  }
}

interface RetainRangesDashTimeV1Action {
  type: ActionType.RetainRangesDashboardTimeV1
  payload: {
    dashboardIDs: number[]
  }
}

interface RetainDashRefreshAction {
  type: ActionType.RetainDashboardRefresh
  payload: {
    dashboardIDs: number[]
  }
}

interface SetTimeRangeAction {
  type: ActionType.SetDashboardTimeRange
  payload: {
    timeRange: TimeRange
  }
}

interface SetZoomedTimeRangeAction {
  type: ActionType.SetDashboardZoomedTimeRange
  payload: {
    zoomedTimeRange: TimeRange
  }
}

interface UpdateDashboardAction {
  type: ActionType.UpdateDashboard
  payload: {
    dashboard: Dashboard
  }
}

interface CreateDashboardAction {
  type: ActionType.CreateDashboard
  payload: {
    dashboard: Dashboard
  }
}

interface DeleteDashboardAction {
  type: ActionType.DeleteDashboard
  payload: {
    dashboard: Dashboard
  }
}

interface DeleteDashboardFailedAction {
  type: ActionType.DeleteDashboardFailed
  payload: {
    dashboard: Dashboard
  }
}

interface SyncDashboardCellAction {
  type: ActionType.SyncDashboardCell
  payload: {
    dashboard: Dashboard
    cell: Cell
  }
}

interface AddDashboardCellAction {
  type: ActionType.AddDashboardCell
  payload: {
    dashboard: Dashboard
    cell: Cell
  }
}

interface DeleteDashboardCellAction {
  type: ActionType.DeleteDashboardCell
  payload: {
    dashboard: Dashboard
    cell: Cell
  }
}

interface EditCellQueryStatusAction {
  type: ActionType.EditCellQueryStatus
  payload: {
    queryID: string
    status: Status
  }
}

interface TemplateVariableLocalSelectedAction {
  type: ActionType.TemplateVariableLocalSelected
  payload: {
    dashboardID: string
    templateID: string
    value: TemplateValue
  }
}

interface UpdateTemplatesAction {
  type: ActionType.UpdateTemplates
  payload: {
    templates: Template[]
  }
}

interface SetHoverTimeAction {
  type: ActionType.SetHoverTime
  payload: {
    hoverTime: string
  }
}

interface SetActiveCellAction {
  type: ActionType.SetActiveCell
  payload: {
    activeCellID: string
  }
}

interface SetDashTimeV1Action {
  type: ActionType.SetDashboardTimeV1
  payload: {
    dashboardID: string
    timeRange: TimeRange
  }
}

interface SetDashRefreshAction {
  type: ActionType.SetDashboardRefresh
  payload: {
    dashboardID: string
    refreshRate: RefreshRate
  }
}

export type Action =
  | LoadDashboardsAction
  | LoadDashboardAction
  | RetainRangesDashTimeV1Action
  | RetainDashRefreshAction
  | SetTimeRangeAction
  | SetZoomedTimeRangeAction
  | UpdateDashboardAction
  | CreateDashboardAction
  | DeleteDashboardAction
  | DeleteDashboardFailedAction
  | SyncDashboardCellAction
  | AddDashboardCellAction
  | DeleteDashboardCellAction
  | EditCellQueryStatusAction
  | TemplateVariableLocalSelectedAction
  | UpdateTemplatesAction
  | SetHoverTimeAction
  | SetActiveCellAction
  | SetDashTimeV1Action
  | SetDashRefreshAction

export const loadDashboards = (
  dashboards: Dashboard[],
  dashboardID?: string
): LoadDashboardsAction => ({
  type: ActionType.LoadDashboards,
  payload: {
    dashboards,
    dashboardID,
  },
})

export const loadDashboard = (dashboard: Dashboard): LoadDashboardAction => ({
  type: ActionType.LoadDashboard,
  payload: {dashboard},
})

export const setDashTimeV1 = (
  dashboardID: string,
  timeRange: TimeRange
): SetDashTimeV1Action => ({
  type: ActionType.SetDashboardTimeV1,
  payload: {dashboardID, timeRange},
})

export const setDashRefresh = (
  dashboardID: string,
  refreshRate: RefreshRate
): SetDashRefreshAction => ({
  type: ActionType.SetDashboardRefresh,
  payload: {dashboardID, refreshRate},
})

export const retainRangesDashTimeV1 = (
  dashboardIDs: number[]
): RetainRangesDashTimeV1Action => ({
  type: ActionType.RetainRangesDashboardTimeV1,
  payload: {dashboardIDs},
})

export const retainDashRefresh = (
  dashboardIDs: number[]
): RetainDashRefreshAction => ({
  type: ActionType.RetainDashboardRefresh,
  payload: {dashboardIDs},
})

export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
  type: ActionType.SetDashboardTimeRange,
  payload: {timeRange},
})

export const setZoomedTimeRange = (
  zoomedTimeRange: TimeRange
): SetZoomedTimeRangeAction => ({
  type: ActionType.SetDashboardZoomedTimeRange,
  payload: {zoomedTimeRange},
})

export const updateDashboard = (
  dashboard: Dashboard
): UpdateDashboardAction => ({
  type: ActionType.UpdateDashboard,
  payload: {dashboard},
})

export const createDashboard = (
  dashboard: Dashboard
): CreateDashboardAction => ({
  type: ActionType.CreateDashboard,
  payload: {dashboard},
})

export const deleteDashboard = (
  dashboard: Dashboard
): DeleteDashboardAction => ({
  type: ActionType.DeleteDashboard,
  payload: {dashboard},
})

export const deleteDashboardFailed = (
  dashboard: Dashboard
): DeleteDashboardFailedAction => ({
  type: ActionType.DeleteDashboardFailed,
  payload: {dashboard},
})

export const syncDashboardCell = (
  dashboard: Dashboard,
  cell: Cell
): SyncDashboardCellAction => ({
  type: ActionType.SyncDashboardCell,
  payload: {dashboard, cell},
})

export const addDashboardCell = (
  dashboard: Dashboard,
  cell: Cell
): AddDashboardCellAction => ({
  type: ActionType.AddDashboardCell,
  payload: {dashboard, cell},
})

export const deleteDashboardCell = (
  dashboard: Dashboard,
  cell: Cell
): DeleteDashboardCellAction => ({
  type: ActionType.DeleteDashboardCell,
  payload: {dashboard, cell},
})

export const editCellQueryStatus = (
  queryID: string,
  status: Status
): EditCellQueryStatusAction => ({
  type: ActionType.EditCellQueryStatus,
  payload: {queryID, status},
})

export const templateVariableLocalSelected = (
  dashboardID: string,
  templateID: string,
  value: TemplateValue
): TemplateVariableLocalSelectedAction => ({
  type: ActionType.TemplateVariableLocalSelected,
  payload: {dashboardID, templateID, value},
})

export const updateTemplates = (
  templates: Template[]
): UpdateTemplatesAction => ({
  type: ActionType.UpdateTemplates,
  payload: {templates},
})

export const setHoverTime = (hoverTime: string): SetHoverTimeAction => ({
  type: ActionType.SetHoverTime,
  payload: {hoverTime},
})

export const setActiveCell = (activeCellID: string): SetActiveCellAction => ({
  type: ActionType.SetActiveCell,
  payload: {activeCellID},
})

export const updateTimeRangeQueryParams = (
  updatedQueryParams: Record<string, unknown>
): RouterAction => {
  const {search, pathname} = window.location
  const strippedPathname = stripPrefix(pathname)

  const parsed = _.omit(qs.parse(search, {ignoreQueryPrefix: true}), [
    'lower',
    'upper',
    'zoomedLower',
    'zoomedUpper',
  ])

  const newQueryParams = _.pickBy(
    {
      ...parsed,
      ...updatedQueryParams,
    },
    v => !!v
  )

  const newSearch = qs.stringify(newQueryParams)
  const newLocation = {pathname: strippedPathname, search: `?${newSearch}`}

  return push(newLocation)
}

export const updateQueryParams = (
  updatedQueryParams: Record<string, unknown>
): RouterAction => {
  const {search, pathname} = window.location
  const strippedPathname = stripPrefix(pathname)

  const newQueryParams = _.pickBy(
    {
      ...qs.parse(search, {ignoreQueryPrefix: true}),
      ...updatedQueryParams,
    },
    v => !!v
  )

  const newSearch = qs.stringify(newQueryParams)
  const newLocation = {pathname: strippedPathname, search: `?${newSearch}`}

  return replace(newLocation)
}

const getDashboard = (state, dashboardId: string): Dashboard => {
  const dashboard = state.dashboardUI.dashboards.find(d => d.id === dashboardId)

  if (!dashboard) {
    throw new Error(`Could not find dashboard with id '${dashboardId}'`)
  }

  return dashboard
}

// Thunkers

export const getDashboardsAsync = () => async (
  dispatch: Dispatch<Action>
): Promise<Dashboard[]> => {
  try {
    const {
      data: {dashboards},
    } = await getDashboardsAJAX()
    dispatch(loadDashboards(dashboards))
    return dashboards
  } catch (error) {
    console.error(error)
    dispatch(errorThrown(error))
  }
}

export const getChronografVersion = () => async (): Promise<string> => {
  try {
    const results = await getMe()
    const version = _.get(results, 'headers.x-chronograf-version')
    return version
  } catch (error) {
    console.error(error)
  }
}

const removeUnselectedTemplateValues = (dashboard: Dashboard): Template[] => {
  const templates = getDeep<Template[]>(dashboard, 'templates', []).map(
    template => {
      if (
        template.type === TemplateType.CSV ||
        template.type === TemplateType.Map
      ) {
        return template
      }

      const value = template.values.find(val => val.selected)
      const values = value ? [value] : []

      return {...template, values}
    }
  )
  return templates
}

export const putDashboard = (dashboard: Dashboard) => async (
  dispatch: Dispatch<Action>
): Promise<void> => {
  try {
    // save only selected template values to server
    const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
      dashboard
    )
    const {
      data: dashboardWithOnlySelectedTemplateValues,
    } = await updateDashboardAJAX({
      ...dashboard,
      templates: templatesWithOnlySelectedValues,
    })
    // save all template values to redux
    dispatch(
      updateDashboard({
        ...dashboardWithOnlySelectedTemplateValues,
        templates: dashboard.templates,
      })
    )
  } catch (error) {
    console.error(error)
    dispatch(errorThrown(error))
  }
}

export const putDashboardByID = (dashboardID: string) => async (
  dispatch: Dispatch<Action>,
  getState
): Promise<void> => {
  try {
    const dashboard = getDashboard(getState(), dashboardID)
    const templates = removeUnselectedTemplateValues(dashboard)
    await updateDashboardAJAX({...dashboard, templates})
  } catch (error) {
    console.error(error)
    dispatch(errorThrown(error))
  }
}

export const updateDashboardCell = (
  dashboard: Dashboard,
  cell: Cell | NewDefaultCell
) => async (dispatch: Dispatch<Action>): Promise<void> => {
  try {
    const {data} = await updateDashboardCellAJAX(cell)
    dispatch(syncDashboardCell(dashboard, data))
  } catch (error) {
    console.error(error)
    dispatch(errorThrown(error))
  }
}

export const deleteDashboardAsync = (dashboard: Dashboard) => async (
  dispatch: Dispatch<Action>
): Promise<void> => {
  dispatch(deleteDashboard(dashboard))
  try {
    await deleteDashboardAJAX(dashboard)
    dispatch(notify(notifyDashboardDeleted(dashboard.name)))
  } catch (error) {
    dispatch(
      errorThrown(
        error,
        notifyDashboardDeleteFailed(dashboard.name, error.data.message)
      )
    )
    dispatch(deleteDashboardFailed(dashboard))
  }
}

export const addDashboardCellAsync = (
  dashboard: Dashboard,
  cell: Partial<Cell>
) => async (dispatch: Dispatch<Action>): Promise<void> => {
  try {
    const {data} = await addDashboardCellAJAX(dashboard, cell)
    dispatch(addDashboardCell(dashboard, data))
    dispatch(notify(notifyCellAdded(data.name)))
  } catch (error) {
    console.error(error)
    dispatch(errorThrown(error))
  }
}

export const sendDashboardCellAsync = (
  dashboard: Dashboard,
  cell: Partial<Cell>
) => async (
  dispatch: Dispatch<Action>
): Promise<{success: boolean; dashboard: Dashboard}> => {
  try {
    const {data} = await addDashboardCellAJAX(dashboard, cell)
    dispatch(addDashboardCell(dashboard, data))
    return {success: true, dashboard}
  } catch (error) {
    console.error(error)
    dispatch(errorThrown(error))
    return {success: false, dashboard}
  }
}

export const cloneDashboardCellAsync = (
  dashboard: Dashboard,
  cell: Cell
) => async (dispatch: Dispatch<Action>): Promise<void> => {
  try {
    const clonedCell = getClonedDashboardCell(dashboard, cell)
    const {data} = await addDashboardCellAJAX(dashboard, clonedCell)
    dispatch(addDashboardCell(dashboard, data))
    dispatch(notify(notifyCellAdded(clonedCell.name)))
  } catch (error) {
    console.error(error)
    dispatch(errorThrown(error))
  }
}

export const deleteDashboardCellAsync = (
  dashboard: Dashboard,
  cell: Cell
) => async (dispatch: Dispatch<Action>): Promise<void> => {
  try {
    await deleteDashboardCellAJAX(cell)
    dispatch(deleteDashboardCell(dashboard, cell))
    dispatch(notify(notifyCellDeleted(cell.name)))
  } catch (error) {
    console.error(error)
    dispatch(errorThrown(error))
  }
}

export const importDashboardAsync = (dashboard: Dashboard) => async (
  dispatch: Dispatch<Action>
): Promise<void> => {
  try {
    // save only selected template values to server
    const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
      dashboard
    )

    const results = await createDashboardAJAX({
      ...dashboard,
      templates: templatesWithOnlySelectedValues,
    })

    const dashboardWithOnlySelectedTemplateValues = _.get(results, 'data')

    // save all template values to redux
    dispatch(
      createDashboard({
        ...dashboardWithOnlySelectedTemplateValues,
        templates: dashboard.templates,
      })
    )

    const {
      data: {dashboards},
    } = await getDashboardsAJAX()

    dispatch(loadDashboards(dashboards))

    dispatch(notify(notifyDashboardImported()))
  } catch (error) {
    const errorMessage = _.get(
      error,
      'data.message',
      'Could not upload dashboard'
    )
    dispatch(notify(notifyDashboardImportFailed('', errorMessage)))
    console.error(error)
    dispatch(errorThrown(error))
  }
}

const updateTimeRangeFromQueryParams = (dashboardID: string) => (
  dispatch: Dispatch<Action>,
  getState
): void => {
  const {dashTimeV1} = getState()
  const queryParams = qs.parse(window.location.search, {
    ignoreQueryPrefix: true,
  })

  const timeRangeFromQueries: TimeRange = {
    lower: queryParams.lower as string | undefined,
    upper: queryParams.upper as string | undefined,
  }

  const zoomedTimeRangeFromQueries: TimeRange = {
    lower: queryParams.zoomedLower as string | undefined,
    upper: queryParams.zoomedUpper as string | undefined,
  }

  let validatedTimeRange = validTimeRange(timeRangeFromQueries)

  if (!validatedTimeRange.lower) {
    const dashboardTimeRange = dashTimeV1.ranges.find(
      r => r.dashboardID === dashboardID
    )

    validatedTimeRange = dashboardTimeRange || DEFAULT_TIME_RANGE

    if (timeRangeFromQueries.lower || timeRangeFromQueries.upper) {
      dispatch(notify(notifyInvalidTimeRangeValueInURLQuery()))
    }
  }

  dispatch(setDashTimeV1(dashboardID, validatedTimeRange))

  const validatedZoomedTimeRange = validAbsoluteTimeRange(
    zoomedTimeRangeFromQueries
  )

  if (
    !validatedZoomedTimeRange.lower &&
    (queryParams.zoomedLower || queryParams.zoomedUpper)
  ) {
    dispatch(notify(notifyInvalidZoomedTimeRangeValueInURLQuery()))
  }

  dispatch(setZoomedTimeRange(validatedZoomedTimeRange))

  const updatedQueryParams = {
    lower: validatedTimeRange.lower,
    upper: validatedTimeRange.upper,
    zoomedLower: validatedZoomedTimeRange.lower,
    zoomedUpper: validatedZoomedTimeRange.upper,
  }

  dispatch(updateQueryParams(updatedQueryParams))
}

export const getDashboardWithTemplatesAsync = (
  dashboardId: string,
  source: Source,
  sources: Source[]
) => async (dispatch): Promise<void> => {
  let dashboard: Dashboard

  try {
    dashboard = await getDashboardAJAX(dashboardId)
  } catch {
    dispatch(replace(`/sources/${source.id}/dashboards`))
    dispatch(notify(notifyDashboardNotFound(dashboardId)))

    return
  }

  const selections = templateSelectionsFromQueryParams()

  const templates = await hydrateTemplates(dashboard.templates, sources, {
    source,
    selections,
  })

  _.each(selections, (val, key) => {
    const result = _.some(
      templates,
      temp =>
        temp.tempVar === key &&
        _.some(temp.values, v => {
          if (v.key) {
            return v.key === val
          }
          return v.value === val
        })
    )

    if (!result) {
      dispatch(notify(notifyInvalidQueryParam(key)))
    }
  })

  dispatch(loadDashboard({...dashboard, templates}))
  dispatch(updateTemplateQueryParams(dashboardId))
  dispatch(updateTimeRangeFromQueryParams(dashboardId))
}

export const rehydrateTemplatesAsync = (
  dashboardId: string,
  source: Source,
  sources: Source[]
) => async (dispatch, getState): Promise<void> => {
  const dashboard = getDashboard(getState(), dashboardId)

  const templates = await hydrateTemplates(dashboard.templates, sources, {
    source,
  })

  dispatch(updateTemplates(templates))
  dispatch(updateTemplateQueryParams(dashboardId))
}

export const updateTemplateQueryParams = (dashboardId: string) => (
  dispatch: Dispatch<Action>,
  getState
): void => {
  const templates = getDashboard(getState(), dashboardId).templates
  const updatedQueryParams = {
    tempVars: templateSelectionsFromTemplates(templates),
  }

  dispatch(updateQueryParams(updatedQueryParams))
}
