import { Event, getPublicKey, kinds, nip19 } from 'nostr-tools'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Outlet, useNavigate, useSearchParams } from 'react-router-dom'
import { AppBar } from '../components/AppBar/AppBar'
import { LoadingSpinner } from '../components/LoadingSpinner'
import {
  AuthController,
  MetadataController,
  NostrController
} from '../controllers'
import {
  restoreState,
  setMetadataEvent,
  updateKeyPair,
  updateLoginMethod,
  updateNostrLoginAuthMethod,
  updateUserAppData
} from '../store/actions'
import { setUserRobotImage } from '../store/userRobotImage/action'
import {
  getRoboHashPicture,
  getUsersAppData,
  loadState,
  subscribeForSigits
} from '../utils'
import { useAppDispatch, useAppSelector } from '../hooks'
import styles from './style.module.scss'
import { useLogout } from '../hooks/useLogout'
import { LoginMethod } from '../store/auth/types'
import { NostrLoginAuthOptions } from 'nostr-login/dist/types'
import { init as initNostrLogin } from 'nostr-login'

export const MainLayout = () => {
  const [searchParams, setSearchParams] = useSearchParams()
  const navigate = useNavigate()
  const dispatch = useAppDispatch()
  const logout = useLogout()
  const [isLoading, setIsLoading] = useState(true)
  const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState(`Loading App`)
  const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
  const authState = useAppSelector((state) => state.auth)
  const usersAppData = useAppSelector((state) => state.userAppData)

  // Ref to track if `subscribeForSigits` has been called
  const hasSubscribed = useRef(false)

  const navigateAfterLogin = (path: string) => {
    const callbackPath = searchParams.get('callbackPath')

    if (callbackPath) {
      // base64 decoded path
      const path = atob(callbackPath)
      navigate(path)
      return
    }

    navigate(path)
  }

  const login = useCallback(async () => {
    const nostrController = NostrController.getInstance()
    const authController = new AuthController()
    const pubkey = await nostrController.capturePublicKey()

    dispatch(updateLoginMethod(LoginMethod.nostrLogin))

    const redirectPath =
      await authController.authAndGetMetadataAndRelaysMap(pubkey)

    if (redirectPath) {
      navigateAfterLogin(redirectPath)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dispatch])

  useEffect(() => {
    // Developer login with ?nsec= (not recommended)
    const nsec = searchParams.get('nsec')
    if (!nsec) return

    // Clear nsec from the url immediately
    searchParams.delete('nsec')
    setSearchParams(searchParams)

    if (!authState?.loggedIn) {
      if (!nsec.startsWith('nsec')) {
        console.error('Invalid format, use private key (nsec)')
        return
      }

      try {
        const privateKey = nip19.decode(nsec).data as Uint8Array
        if (!privateKey) {
          console.error('Failed to convert the private key.')
          return
        }

        const publickey = getPublicKey(privateKey)

        dispatch(
          updateKeyPair({
            private: nsec,
            public: publickey
          })
        )
        dispatch(updateLoginMethod(LoginMethod.privateKey))

        const authController = new AuthController()
        authController
          .authAndGetMetadataAndRelaysMap(publickey)
          .catch((err) => {
            console.error('Error occurred in authentication: ' + err)
            return null
          })
      } catch (err) {
        console.error(`Error decoding the nsec. ${err}`)
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dispatch, searchParams])

  useEffect(() => {
    const handleNostrAuth = (_: string, opts: NostrLoginAuthOptions) => {
      if (opts.type === 'login' || opts.type === 'signup') {
        dispatch(updateNostrLoginAuthMethod(opts.method))
        login()
      } else if (opts.type === 'logout') {
        // Clear `subscribeForSigits` as called after the logout
        hasSubscribed.current = false
      }
    }

    // Initialize the nostr-login
    initNostrLogin({
      methods: ['connect', 'extension', 'local'],
      noBanner: true,
      onAuth: handleNostrAuth
    }).catch((error) => {
      console.error('Failed to initialize Nostr-Login', error)
    })

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dispatch])

  useEffect(() => {
    const metadataController = MetadataController.getInstance()

    const restoredState = loadState()
    if (restoredState) {
      dispatch(restoreState(restoredState))

      const { loggedIn, loginMethod, usersPubkey } = restoredState.auth

      if (loggedIn) {
        if (!loginMethod || !usersPubkey) return logout()

        // Update user profile metadata, old state might be outdated
        const handleMetadataEvent = (event: Event) => {
          dispatch(setMetadataEvent(event))
        }

        metadataController.on(usersPubkey, (kind: number, event: Event) => {
          if (kind === kinds.Metadata) {
            handleMetadataEvent(event)
          }
        })

        metadataController.findMetadata(usersPubkey).then((metadataEvent) => {
          if (metadataEvent) handleMetadataEvent(metadataEvent)
        })
      } else {
        setIsLoading(false)
      }
    } else {
      setIsLoading(false)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dispatch])

  /**
   * Subscribe for the sigits
   */
  useEffect(() => {
    if (authState && isLoggedIn && usersAppData) {
      const pubkey = authState.usersPubkey || authState.keyPair?.public

      if (pubkey && !hasSubscribed.current) {
        // Call `subscribeForSigits` only if it hasn't been called before
        // #193 disabled websocket subscribtion, until #194 is done
        subscribeForSigits(pubkey)

        // Mark `subscribeForSigits` as called
        hasSubscribed.current = true
      }
    }
  }, [authState, isLoggedIn, usersAppData])

  /**
   * When authState change user logged in / or app reloaded
   * we set robohash avatar in the global state based on user npub
   * so that avatar will be consistent across the app when kind 0 is empty
   */
  useEffect(() => {
    if (authState && isLoggedIn) {
      const pubkey = authState.usersPubkey || authState.keyPair?.public

      if (pubkey) {
        dispatch(setUserRobotImage(getRoboHashPicture(pubkey)))
      }

      setIsLoading(true)
      setLoadingSpinnerDesc(`Loading SIGit history...`)
      getUsersAppData()
        .then((appData) => {
          if (appData) {
            dispatch(updateUserAppData(appData))
          }
        })
        .finally(() => setIsLoading(false))
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dispatch, isLoggedIn])

  if (isLoading) return <LoadingSpinner desc={loadingSpinnerDesc} />

  const isDev = import.meta.env.MODE === 'development'

  return (
    <>
      <AppBar />
      <main
        className={styles.main}
        // Add dev debug flag to show special dev css
        // (visible marks border on verify)
        {...(isDev && {
          'data-dev': true
        })}
      >
        <Outlet />
      </main>
    </>
  )
}
