import { useState, useMemo, useEffect, useRef, useCallback } from 'react'
import window from 'global/window.js'
import debounce from 'lodash.debounce'
import { v4 as uuid } from 'uuid'

import * as reducers from '../../game-logic/reducers/index.js'
import eventHandlers from '../../handlers/ui-events.js'

import { features } from '../../config.js'
import {
  DEFAULT_ROOM,
  INITIAL_STORAGE_LIMIT,
  STAGE_TITLE_MAP,
  STANDARD_LOAN_AMOUNT,
  STANDARD_VIEW_LIST,
} from '../../constants.js'
import { scarecrow } from '../../data/items.js'
import {
  dialogView,
  fieldMode,
  stageFocusType,
  toolLevel,
  toolType,
} from '../../enums.js'
import { getData, postData } from '../../fetch-utils.js'
import { PROGRESS_SAVED_MESSAGE } from '../../strings.js'
import { LOAN_INCREASED } from '../../templates.js'
import { createNewField } from '../../utils/createNewField.js'
import { createNewForest } from '../../utils/createNewForest.js'
import { doesMenuObstructStage } from '../../utils/doesMenuObstructStage.js'
import { generateCow } from '../../utils/generateCow.js'
import { getAvailableShopInventory } from '../../utils/getAvailableShopInventory.js'
import { getLevelEntitlements } from '../../utils/getLevelEntitlements.js'
import { getPeerMetadata } from '../../utils/getPeerMetadata.js'
import { levelAchieved } from '../../utils/levelAchieved.js'
import { moneyTotal } from '../../utils/moneyTotal.js'
import { noop } from '../../utils/noop.js'
import { nullArray } from '../../utils/nullArray.js'
import { reduceByPersistedKeys } from '../../utils/reduceByPersistedKeys.js'
import { sleep } from '../../utils/sleep.js'
import { transformStateDataForImport } from '../../utils/transformStateDataForImport.js'

import { BoundHandlers } from './Farmhand.context.js'
import { FarmhandProps } from './FarmhandReducers.js'
import { FarmhandService } from './FarmhandService.js'
import { getInventoryQuantities } from './helpers/getInventoryQuantities.js'

import { useFarmhandNavigation } from './hooks/useFarmhandNavigation.js'
import { useFarmhandNetwork } from './hooks/useFarmhandNetwork.js'
import {
  useFarmhandDayAdvancement,
  NextDayStateRecord,
} from './hooks/useFarmhandDayAdvancement.js'
import { useFarmhandNotifications } from './hooks/useFarmhandNotifications.js'
import { useFarmhandReducers } from './useFarmhandReducers.js'
import { usePrevious } from './usePrevious.js'

const { CLEANUP, HARVEST, MINE, WATER } = fieldMode

export const useFarmhand = (props: FarmhandProps) => {
  // Extract props properly
  const {
    features: propsFeatures,
    match: { path = '', params: { room: paramRoom } = {} } = {},
  } = props

  const createInitialState = useCallback((): farmhand.state => {
    return {
      activePlayers: null,
      allowCustomPeerCowNames: false,
      cellarInventory: [],
      currentDialogView: dialogView.NONE as farmhand.dialogView,
      completedAchievements: {},
      cowForSale: generateCow() as farmhand.cow,
      cowBreedingPen: { cowId1: null, cowId2: null, daysUntilBirth: -1 },
      cowColorsPurchased: {},
      cowIdOfferedForTrade: '',
      cowInventory: [],
      cowsSold: {},
      cowsTraded: 0,
      cowTradeTimeoutId: -1,
      cropsHarvested: {},
      dayCount: 0,
      experience: 0,
      farmName: 'Unnamed',
      field: createNewField(),
      fieldMode: fieldMode.OBSERVE as farmhand.fieldMode,
      forest: createNewForest(),
      getCowAccept: noop,
      getCowReject: noop,
      getCowTradeRequest: noop,
      getPeerMetadata: noop,
      hasBooted: false,
      heartbeatTimeoutId: null,
      historicalDailyLosses: [],
      historicalDailyRevenue: [],
      historicalValueAdjustments: [],
      hoveredPlotRangeSize: 0,
      playerId: uuid(),
      inventory: [{ id: scarecrow.id, quantity: 1 }],
      inventoryLimit: INITIAL_STORAGE_LIMIT,
      isAwaitingCowTradeRequest: false,
      isAwaitingNetworkRequest: false,
      isCombineEnabled: false,
      isMenuOpen: !doesMenuObstructStage(),
      itemsSold: {},
      cellarItemsSold: {},
      isChatOpen: false,
      isDialogViewOpen: false,
      isOnline: path.startsWith('/online') ?? false,
      isWaitingForDayToCompleteIncrementing: false,
      learnedRecipes: {},
      loanBalance: STANDARD_LOAN_AMOUNT,
      loansTakenOut: 1,
      money: STANDARD_LOAN_AMOUNT,
      latestNotification: null,
      newDayNotifications: [],
      notificationLog: [],
      peers: {},
      peerRoom: null,
      pendingPeerMessages: [],
      latestPeerMessages: [],
      sendPeerMetadata: null,
      selectedCowId: '',
      selectedForestItemId: '',
      selectedItemId: '',
      priceCrashes: {},
      priceSurges: {},
      profitabilityStreak: 0,
      record7dayProfitAverage: 0,
      recordProfitabilityStreak: 0,
      recordSingleDayProfit: 0,
      revenue: 0,
      redirect: '',
      room: decodeURIComponent(props.match?.params?.room || DEFAULT_ROOM),
      sendCowAccept: noop,
      sendCowReject: noop,
      purchasedCombine: 0,
      purchasedComposter: 0,
      purchasedCowPen: 0,
      purchasedCellar: 0,
      purchasedField: 0,
      purchasedForest: 0,
      purchasedSmelter: 0,
      sendCowTradeRequest: noop,
      showHomeScreen: true,
      showNotifications: true,
      stageFocus: stageFocusType.HOME as farmhand.stageFocusType,
      todaysNotifications: [],
      todaysLosses: 0,
      todaysPurchases: {},
      todaysRevenue: 0,
      todaysStartingInventory: {},
      toolLevels: {
        [toolType.AXE as farmhand.toolType]: toolLevel.UNAVAILABLE as farmhand.toolLevel,
        [toolType.HOE as farmhand.toolType]: toolLevel.DEFAULT as farmhand.toolLevel,
        [toolType.SCYTHE as farmhand.toolType]: toolLevel.DEFAULT as farmhand.toolLevel,
        [toolType.SHOVEL as farmhand.toolType]: toolLevel.UNAVAILABLE as farmhand.toolLevel,
        [toolType.WATERING_CAN as farmhand.toolType]: toolLevel.DEFAULT as farmhand.toolLevel,
      } as Record<farmhand.toolType, farmhand.toolLevel>,
      useAlternateEndDayButtonPosition: false,
      valueAdjustments: {},
      version: import.meta.env?.VITE_FARMHAND_PACKAGE_VERSION ?? '',
    }
  }, [path, props.match?.params?.room])

  const [state, setState] = useState<farmhand.state>(createInitialState)
  const stateRef = useRef<farmhand.state>(state)

  stateRef.current = state

  // Falls back to the current room in state when the URL doesn't specify one,
  // mirroring the legacy class's componentDidUpdate default of
  // `params: { room: newRoom = room } = this.props.match`.
  const newRoom = paramRoom !== undefined ? paramRoom : state.room

  const nextDayStateRef = useRef<NextDayStateRecord | null>(null)
  const hasStartedBootRef = useRef(false)

  const boundReducers = useFarmhandReducers(setState)
  const boundReducersRef = useRef(boundReducers)

  boundReducersRef.current = boundReducers

  const prevState = usePrevious(state)

  const viewTitle =
    STAGE_TITLE_MAP[state.stageFocus as keyof typeof STAGE_TITLE_MAP]
  const fieldToolInventory = FarmhandService.getFieldToolInventory(
    state.inventory
  )
  const playerInventory = FarmhandService.computePlayerInventory(
    state.inventory,
    state.valueAdjustments
  )
  const plantableCropInventory = FarmhandService.getPlantableCropInventory(
    state.inventory
  )
  const plantableTreeInventory = FarmhandService.getPlantableTreeInventory(
    state.inventory
  )

  const levelEntitlements = useMemo(
    () => getLevelEntitlements(levelAchieved(state.experience)),
    [state.experience]
  )
  const shopInventory = useMemo(
    () => getAvailableShopInventory(levelEntitlements),
    [levelEntitlements]
  )

  const isForestUnlocked =
    levelEntitlements.stageFocusType[stageFocusType.FOREST]
  const isChatAvailable = state.isOnline && state.room !== DEFAULT_ROOM

  const viewList = useMemo(() => {
    const { CELLAR, COW_PEN, HOME, WORKSHOP, FOREST } = stageFocusType
    const list: farmhand.stageFocusType[] = [...STANDARD_VIEW_LIST]

    if (state.showHomeScreen) {
      list.unshift(HOME)
    }

    if (isForestUnlocked && features.FOREST) {
      list.push(FOREST)
    }

    if (state.purchasedCowPen) {
      list.push(COW_PEN)
    }

    list.push(WORKSHOP)

    if (state.purchasedCellar) {
      list.push(CELLAR)
    }

    return list
  }, [
    state.showHomeScreen,
    isForestUnlocked,
    state.purchasedCowPen,
    state.purchasedCellar,
  ])

  const peerMetadata = useMemo(() => getPeerMetadata(state), [state])

  const isInputBlocked =
    state.isAwaitingNetworkRequest ||
    state.isAwaitingCowTradeRequest ||
    state.isWaitingForDayToCompleteIncrementing

  const instanceProxyRef = useRef<any>(null)

  // Call sub-hooks
  const {
    openDialogView,
    closeDialogView,
    focusNextView,
    focusPreviousView,
  } = useFarmhandNavigation(setState, viewList)

  const {
    syncToRoom,
    scheduleHeartbeat,
    tradeForPeerCow,
    handleCowTradeTimeout,
    messagePeers,
  } = useFarmhandNetwork(
    state,
    setState,
    boundReducersRef,
    instanceProxyRef,
    prevState,
    newRoom,
    path,
    peerMetadata
  )

  const {
    clearPersistedData,
    persistState,
    updateServerForNextDay,
    incrementDay,
  } = useFarmhandDayAdvancement(
    stateRef,
    setState,
    props,
    boundReducersRef,
    nextDayStateRef
  )

  const {
    showInventoryFullNotifications,
    showRecipeLearnedNotifications,
  } = useFarmhandNotifications(state, boundReducersRef)

  // Instance proxy to mimic the legacy class "this" so ui-events.tsx can run unmodified
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const instanceProxy = useMemo(
    () => ({
      state,
      props,
      viewTitle,
      fieldToolInventory,
      playerInventory,
      plantableCropInventory,
      plantableTreeInventory,
      viewList,
      levelEntitlements,
      shopInventory,
      peerMetadata,
      isInputBlocked,
      isChatAvailable,
      isForestUnlocked,
      setState: (updater: any, callback?: () => void) => {
        setState(previous => {
          const next =
            typeof updater === 'function' ? updater(previous) : updater

          if (next === null || next === undefined) return previous

          const newState = { ...previous, ...next }

          if (callback) setTimeout(callback, 0)

          return newState
        })
      },
      ...boundReducersRef.current,
      purchaseItem: (item: any, quantity: number) =>
        boundReducersRef.current.purchaseItem(item, quantity),
      sellItem: (item: any, quantity: number) =>
        boundReducersRef.current.sellItem(item, quantity),
      showNotification: (msg: string, sev?: string) =>
        boundReducersRef.current.showNotification(msg, sev),
      prependPendingPeerMessage: (msg: string, sev?: string) =>
        boundReducersRef.current.prependPendingPeerMessage(msg, sev),
      forRange: (
        fn: any,
        limit: number,
        arg1: number,
        arg2: number,
        arg3?: any
      ) => boundReducersRef.current.forRange(fn, limit, arg1, arg2, arg3),
      createInitialState,
      getData,
      postData,
      tradeForPeerCow,
      handleCowTradeTimeout,
      clearPersistedData,
      syncToRoom,
      scheduleHeartbeat,
      showInventoryFullNotifications,
      showRecipeLearnedNotifications,
      persistState: (overrides = {}) =>
        persistState(instanceProxyRef.current?.state ?? state, overrides),
      updateServerForNextDay,
      incrementDay,
      openDialogView,
      closeDialogView,
      focusNextView,
      focusPreviousView,
      messagePeers,
    }),
    [
      state,
      props,
      viewTitle,
      fieldToolInventory,
      playerInventory,
      plantableCropInventory,
      plantableTreeInventory,
      viewList,
      levelEntitlements,
      shopInventory,
      peerMetadata,
      isInputBlocked,
      isChatAvailable,
      isForestUnlocked,
      tradeForPeerCow,
      handleCowTradeTimeout,
      clearPersistedData,
      syncToRoom,
      scheduleHeartbeat,
      showInventoryFullNotifications,
      showRecipeLearnedNotifications,
      persistState,
      updateServerForNextDay,
      incrementDay,
      openDialogView,
      closeDialogView,
      focusNextView,
      focusPreviousView,
      messagePeers,
      createInitialState,
    ]
  )

  instanceProxyRef.current = instanceProxy

  const handlers = useMemo(() => {
    const bound: any = { debounced: {} }

    Object.keys(eventHandlers).forEach(methodStr => {
      const method = methodStr as keyof typeof eventHandlers

      bound[method] = (...args: any[]) =>
        (eventHandlers[method] as any).apply(instanceProxyRef.current, args)
      bound.debounced[method] = debounce(bound[method], 50)
    })

    return bound as BoundHandlers<typeof eventHandlers> & {
      debounced: BoundHandlers<typeof eventHandlers>
    }
  }, [])

  const keyMap = useMemo(() => {
    const map: Record<string, string> = {
      incrementDay: 'shift+c',
      nextView: 'right',
      openAccounting: 'b',
      openAchievements: 'a',
      openLog: 'l',
      openPriceEvents: 'e',
      openStats: 's',
      openSettings: ',',
      openKeybindings: 'shift+?',
      previousView: 'left',
      toggleMenu: 'm',
    }

    nullArray(9).forEach((_: null, i: number) => {
      map[`numberKey${i + 1}`] = String(i + 1)
    })

    if (import.meta.env?.MODE === 'development') {
      Object.assign(map, { clearPersistedData: 'shift+d', waterAllPlots: 'w' })
    }

    return map
  }, [])

  const keyHandlers = useMemo(() => {
    const map: Record<string, () => void> = {
      incrementDay: () => incrementDay(),
      nextView: focusNextView,
      openAccounting: () => openDialogView(dialogView.ACCOUNTING),
      openAchievements: () => openDialogView(dialogView.ACHIEVEMENTS),
      openLog: () => openDialogView(dialogView.FARMERS_LOG),
      openPriceEvents: () => openDialogView(dialogView.PRICE_EVENTS),
      openStats: () => openDialogView(dialogView.STATS),
      openSettings: () => openDialogView(dialogView.SETTINGS),
      openKeybindings: () => openDialogView(dialogView.KEYBINDINGS),
      previousView: focusPreviousView,
      selectHoe: () => handlers.handleFieldModeSelect(CLEANUP),
      selectScythe: () => handlers.handleFieldModeSelect(HARVEST),
      selectWateringCan: () => handlers.handleFieldModeSelect(WATER),
      selectShovel: () => {
        if (state.toolLevels[toolType.SHOVEL] !== toolLevel.UNAVAILABLE) {
          handlers.handleFieldModeSelect(MINE)
        }
      },
      toggleMenu: () => handlers.handleMenuToggle(),
    }

    nullArray(9).forEach((_: null, i: number) => {
      map[`numberKey${i + 1}`] = () => {
        const viewName = viewList[i]

        if (typeof viewName === 'string') {
          setState(previous => ({
            ...previous,
            stageFocus: stageFocusType[viewName as keyof typeof stageFocusType],
          }))
        }
      }
    })

    if (import.meta.env?.MODE === 'development') {
      Object.assign(map, {
        clearPersistedData: () => clearPersistedData(),
        waterAllPlots: () => boundReducersRef.current.waterAllPlots(),
      })
    }

    return map
  }, [
    incrementDay,
    focusNextView,
    openDialogView,
    focusPreviousView,
    handlers,
    state.toolLevels,
    viewList,
    clearPersistedData,
  ])

  useEffect(() => {
    if (hasStartedBootRef.current) return

    hasStartedBootRef.current = true

    Object.defineProperty(window, 'farmhand', {
      get: () => instanceProxyRef.current,
      configurable: true,
    })

    let isMounted = true

    void (async () => {
      const persistedState = await props.localforage?.getItem('state')

      if (!isMounted) return

      if (persistedState) {
        const sanitizedState = transformStateDataForImport({
          ...createInitialState(),
          ...persistedState,
        })
        const { isCombineEnabled, newDayNotifications } = sanitizedState

        setState(previous => ({
          ...previous,
          ...sanitizedState,
          newDayNotifications: [],
          hasBooted: true,
        }))

        newDayNotifications.forEach(
          ({ message, severity }: farmhand.notification) => {
            setTimeout(
              () =>
                boundReducersRef.current.showNotification(message, severity),
              0
            )
          }
        )

        // NOTE: Intentional behavior change from the legacy class component:
        // this used to run once per queued newDayNotifications entry (and
        // not at all when there were none). It now runs unconditionally,
        // exactly once, whenever isCombineEnabled is true on boot.
        if (isCombineEnabled) {
          boundReducersRef.current.forRange(
            reducers.harvestPlot,
            Infinity,
            0,
            0
          )
        }
      } else {
        await incrementDay(true)

        setState(previous => ({ ...previous, historicalValueAdjustments: [] }))

        boundReducersRef.current.showNotification(
          LOAN_INCREASED('', STANDARD_LOAN_AMOUNT),
          'info'
        )
        setState(s => ({ ...s, hasBooted: true }))
      }

      syncToRoom()
    })()

    return () => {
      isMounted = false
    }
  }, [createInitialState, incrementDay, props.localforage, syncToRoom])

  // Post-increment day side effects
  useEffect(() => {
    if (!state.hasBooted || !nextDayStateRef.current) return

    let isMounted = true

    const {
      state: resolvedNextDayState,
      pendingNotifications,
      broadcastedPositionMessage,
      isFirstDay,
    } = nextDayStateRef.current

    nextDayStateRef.current = null

    void (async () => {
      try {
        await props.localforage?.setItem(
          'state',
          reduceByPersistedKeys({
            ...resolvedNextDayState,
            ...(isFirstDay && { historicalValueAdjustments: [] }),
            newDayNotifications: pendingNotifications,
          })
        )

        if (!isMounted) return

        const notifications = [...pendingNotifications]

        notifications
          .concat(
            isFirstDay
              ? []
              : [{ message: PROGRESS_SAVED_MESSAGE, severity: 'info' }]
          )
          .forEach(({ message, severity }) =>
            boundReducersRef.current.showNotification(message, severity)
          )

        if (resolvedNextDayState.isCombineEnabled) {
          if (resolvedNextDayState.stageFocus === stageFocusType.FIELD) {
            await sleep(1000)
          }

          if (!isMounted) return

          boundReducersRef.current.forRange(
            reducers.harvestPlot,
            Infinity,
            0,
            0
          )
        }
      } catch (e) {
        console.error(e)
        if (isMounted) {
          boundReducersRef.current.showNotification(JSON.stringify(e), 'error')
        }
      } finally {
        if (isMounted) {
          setState(previous => ({
            ...previous,
            isWaitingForDayToCompleteIncrementing: false,
          }))

          if (broadcastedPositionMessage) {
            boundReducersRef.current.prependPendingPeerMessage(
              broadcastedPositionMessage
            )
          }
        }
      }
    })()

    return () => {
      isMounted = false
    }
  }, [state.dayCount, state.hasBooted, props.localforage, setState])

  // Cleanup on unmount. Uses stateRef (rather than depending on
  // state.heartbeatTimeoutId/state.peerRoom directly) so this only runs once,
  // on unmount — depending on those values re-fires this cleanup on every
  // heartbeat tick, which leaves the peer room every ~10s while online.
  useEffect(() => {
    return () => {
      if (stateRef.current.heartbeatTimeoutId) {
        clearTimeout(stateRef.current.heartbeatTimeoutId)
      }

      stateRef.current.peerRoom?.leave()
    }
  }, [])

  // ComponentDidUpdate for non-network effects (achievements, cow selection, menu menu, money)
  useEffect(() => {
    if (!state.hasBooted || !prevState) return

    const updatedAchievementsState = reducers.updateAchievements(
      state,
      prevState
    )

    if (updatedAchievementsState !== state) {
      setState(previous => {
        const next = reducers.updateAchievements(previous, prevState)

        return next !== previous ? next : previous
      })
    }

    if (
      state.stageFocus === stageFocusType.COW_PEN &&
      prevState.stageFocus !== stageFocusType.COW_PEN
    ) {
      setState(previous => ({ ...previous, selectedCowId: '' }))
    }

    if (state.stageFocus !== prevState.stageFocus && state.isMenuOpen) {
      setState(previous => ({
        ...previous,
        isMenuOpen: !doesMenuObstructStage(),
      }))
    }

    if (state.money < prevState.money) {
      setState(previous => ({
        ...previous,
        todaysLosses: moneyTotal(
          previous.todaysLosses,
          state.money - prevState.money
        ),
      }))
    }

    showInventoryFullNotifications(prevState, state)
    showRecipeLearnedNotifications(prevState)
  }, [
    state,
    prevState,
    showInventoryFullNotifications,
    showRecipeLearnedNotifications,
  ])

  const redirect = state.redirect
  const gameState = {
    ...state,
    blockInput: isInputBlocked,
    features: propsFeatures ?? {},
    fieldToolInventory,
    isChatAvailable,
    levelEntitlements,
    plantableCropInventory,
    plantableTreeInventory,
    playerInventory,
    playerInventoryQuantities: getInventoryQuantities(state.inventory),
    shopInventory,
    viewList,
    viewTitle,
  }

  return {
    gameState,
    handlers,
    isInputBlocked,
    keyMap,
    keyHandlers,
    redirect,
    state,
    viewList,
    focusPreviousView,
    focusNextView,
    isChatAvailable,
  }
}
