import { Button, Divider, TextField } from '@mui/material'
import { getPublicKey, nip19 } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import {
  AuthController,
  MetadataController,
  NostrController
} from '../../controllers'
import {
  updateKeyPair,
  updateLoginMethod,
  updateNsecbunkerPubkey,
  updateNsecbunkerRelays
} from '../../store/actions'
import { LoginMethods } from '../../store/auth/types'
import { Dispatch } from '../../store/store'
import { npubToHex, queryNip05, timeout } from '../../utils'
import { hexToBytes } from '@noble/hashes/utils'
import { NIP05_REGEX } from '../../constants'

import styles from './styles.module.scss'

import { TimeoutError } from '../../types/errors/TimeoutError'
const EXTENSION_LOGIN_DELAY_SECONDS = 5
const EXTENSION_LOGIN_TIMEOUT_SECONDS = EXTENSION_LOGIN_DELAY_SECONDS + 55

export const Nostr = () => {
  const [searchParams] = useSearchParams()

  const dispatch: Dispatch = useDispatch()
  const navigate = useNavigate()

  const authController = new AuthController()
  const metadataController = MetadataController.getInstance()
  const nostrController = NostrController.getInstance()

  const [isLoading, setIsLoading] = useState(false)
  const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
  const [isExtensionSlow, setIsExtensionSlow] = useState(false)
  const [inputValue, setInputValue] = useState('')
  const [authUrl, setAuthUrl] = useState<string>()

  const [isNostrExtensionAvailable, setIsNostrExtensionAvailable] =
    useState(false)

  useEffect(() => {
    setTimeout(() => {
      setIsNostrExtensionAvailable(!!window.nostr)
    }, 500)
  }, [])

  /**
   * Call login function when enter is pressed
   */
  const handleInputKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
    if (event.code === 'Enter' || event.code === 'NumpadEnter') {
      event.preventDefault()
      login()
    }
  }

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

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

    navigate(path)
  }

  const loginWithExtension = async () => {
    let waitTimeout: number | undefined
    try {
      // Wait EXTENSION_LOGIN_DELAY_SECONDS before showing extension delay message
      waitTimeout = window.setTimeout(() => {
        setIsExtensionSlow(true)
      }, EXTENSION_LOGIN_DELAY_SECONDS * 1000)

      setIsLoading(true)
      setLoadingSpinnerDesc('Capturing pubkey from nostr extension')

      const pubkey = await nostrController.capturePublicKey()
      dispatch(updateLoginMethod(LoginMethods.extension))

      setLoadingSpinnerDesc('Authenticating and finding metadata')
      const redirectPath = await Promise.race([
        authController.authAndGetMetadataAndRelaysMap(pubkey),
        timeout(EXTENSION_LOGIN_TIMEOUT_SECONDS * 1000)
      ])

      if (redirectPath) {
        navigateAfterLogin(redirectPath)
      }
    } catch (error) {
      if (error instanceof TimeoutError) {
        // Just log the error, no toast, user has already been notified with the loading screen
        console.error("Extension didn't respond in time")
      } else {
        toast.error('Error capturing public key from nostr extension: ' + error)
      }
    } finally {
      // Clear the wait timeout so we don't change the state unnecessarily
      window.clearTimeout(waitTimeout)

      setIsLoading(false)
      setLoadingSpinnerDesc('')
      setIsExtensionSlow(false)
    }
  }

  /**
   * Login with NSEC or HEX private key
   * @param privateKey in HEX format
   */
  const loginWithNsec = async (privateKey?: Uint8Array) => {
    let nsec = ''

    if (privateKey) {
      nsec = nip19.nsecEncode(privateKey)
    } else {
      nsec = inputValue

      try {
        privateKey = nip19.decode(nsec).data as Uint8Array
      } catch (err) {
        toast.error(`Error decoding the nsec. ${err}`)
      }
    }

    if (!privateKey) {
      toast.error(
        'Snap, we failed to convert the private key you provided. Please make sure key is valid.'
      )
      setIsLoading(false)
      return
    }

    const publickey = getPublicKey(privateKey)

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

    setIsLoading(true)
    setLoadingSpinnerDesc('Authenticating and finding metadata')

    const redirectPath = await authController
      .authAndGetMetadataAndRelaysMap(publickey)
      .catch((err) => {
        toast.error('Error occurred in authentication: ' + err)
        return null
      })

    if (redirectPath) navigateAfterLogin(redirectPath)

    setIsLoading(false)
    setLoadingSpinnerDesc('')
  }

  const loginWithNsecBunker = async () => {
    let relays: string[] | undefined
    let pubkey: string | undefined

    setIsLoading(true)

    const displayError = (message: string) => {
      toast.error(message)
      setIsLoading(false)
      setLoadingSpinnerDesc('')
    }

    if (inputValue.match(NIP05_REGEX)) {
      const nip05Profile = await queryNip05(inputValue).catch((err) => {
        toast.error('An error occurred while querying nip05 profile: ' + err)
        return null
      })

      if (nip05Profile) {
        pubkey = nip05Profile.pubkey
        relays = nip05Profile.relays
      }
    } else if (inputValue.startsWith('npub')) {
      pubkey = nip19.decode(inputValue).data as string
      const metadataEvent = await metadataController
        .findMetadata(pubkey)
        .catch(() => {
          return null
        })

      if (!metadataEvent) {
        return displayError('metadata not found!')
      }

      const metadataContent =
        metadataController.extractProfileMetadataContent(metadataEvent)

      if (!metadataContent?.nip05) {
        return displayError('nip05 not present in metadata')
      }

      const nip05Profile = await queryNip05(inputValue).catch((err) => {
        toast.error('An error occurred while querying nip05 profile: ' + err)
        return null
      })

      if (nip05Profile) {
        if (nip05Profile.pubkey !== pubkey) {
          return displayError(
            'pubkey in nip05 does not match with provided npub'
          )
        }

        relays = nip05Profile.relays
      }
    }

    if (!relays || relays.length === 0) {
      return displayError('No relay found for nsecbunker')
    }

    if (!pubkey) {
      return displayError('pubkey not found')
    }

    setLoadingSpinnerDesc('Initializing nsecBunker')
    await nostrController.nsecBunkerInit(relays)

    setLoadingSpinnerDesc('Creating nsecbunker singer')
    await nostrController
      .createNsecBunkerSigner(pubkey)
      .then(async (signer) => {
        signer.on('authUrl', (url: string) => {
          setAuthUrl(url)
        })

        dispatch(updateLoginMethod(LoginMethods.nsecBunker))
        dispatch(updateNsecbunkerPubkey(pubkey))
        dispatch(updateNsecbunkerRelays(relays))

        setLoadingSpinnerDesc('Authenticating and finding metadata')

        const redirectPath = await authController
          .authAndGetMetadataAndRelaysMap(pubkey!)
          .catch((err) => {
            toast.error('Error occurred in authentication: ' + err)
            return null
          })

        if (redirectPath) navigateAfterLogin(redirectPath)
      })
      .catch((err) => {
        toast.error(
          'An error occurred while creating nsecbunker signer: ' + err
        )
      })
      .finally(() => {
        setIsLoading(false)
        setLoadingSpinnerDesc('')
      })
  }

  const loginWithBunkerConnectionString = async () => {
    // Extract the key
    const keyStartIndex = inputValue.indexOf('bunker://') + 'bunker://'.length
    const keyEndIndex = inputValue.indexOf('?relay=')
    const key = inputValue.substring(keyStartIndex, keyEndIndex)

    const pubkey = npubToHex(key)

    if (!pubkey) {
      toast.error('Invalid pubkey in bunker connection string.')
      setIsLoading(false)
      return
    }

    // Extract the relay value
    const relayIndex = inputValue.indexOf('relay=')
    const relay = inputValue.substring(
      relayIndex + 'relay='.length,
      inputValue.length
    )

    setIsLoading(true)
    setLoadingSpinnerDesc('Initializing bunker NDK')

    await nostrController.nsecBunkerInit([relay])

    setLoadingSpinnerDesc('Creating remote signer')
    await nostrController
      .createNsecBunkerSigner(pubkey)
      .then(async (signer) => {
        signer.on('authUrl', (url: string) => {
          setAuthUrl(url)
        })

        dispatch(updateLoginMethod(LoginMethods.nsecBunker))
        dispatch(updateNsecbunkerPubkey(pubkey))
        dispatch(updateNsecbunkerRelays([relay]))

        setLoadingSpinnerDesc('Authenticating and finding metadata')

        const redirectPath = await authController
          .authAndGetMetadataAndRelaysMap(pubkey!)
          .catch((err) => {
            toast.error('Error occurred in authentication: ' + err)
            return null
          })

        if (redirectPath) navigateAfterLogin(redirectPath)
      })
      .catch((err) => {
        toast.error(
          'An error occurred while creating nsecbunker signer: ' + err
        )
      })
      .finally(() => {
        setIsLoading(false)
        setLoadingSpinnerDesc('')
      })
  }

  const login = () => {
    if (inputValue.startsWith('bunker://')) {
      return loginWithBunkerConnectionString()
    }

    if (inputValue.startsWith('nsec')) {
      return loginWithNsec()
    }
    if (inputValue.startsWith('npub')) {
      return loginWithNsecBunker()
    }
    if (inputValue.match(NIP05_REGEX)) {
      return loginWithNsecBunker()
    }

    // Check if maybe hex nsec
    try {
      const privateKey = hexToBytes(inputValue)
      const publickey = getPublicKey(privateKey)

      if (publickey) return loginWithNsec(privateKey)
    } catch (err) {
      console.warn('err', err)
    }

    toast.error(
      'Invalid format, please use: private key (hex), nsec..., bunker:// or nip05 format.'
    )
    return
  }

  if (authUrl) {
    return (
      <iframe
        title="Nsecbunker auth"
        src={authUrl}
        width="100%"
        height="500px"
      />
    )
  }

  return (
    <>
      {isLoading && (
        <LoadingSpinner desc={loadingSpinnerDesc}>
          {isExtensionSlow && (
            <>
              <p>
                Your nostr extension is not responding. Check these
                alternatives:{' '}
                <a href="https://github.com/aljazceru/awesome-nostr?tab=readme-ov-file#nip-07-browser-extensions">
                  https://github.com/aljazceru/awesome-nostr
                </a>
              </p>
              <br />
              <Button
                fullWidth
                variant="contained"
                onClick={() => {
                  setLoadingSpinnerDesc('')
                  setIsLoading(false)
                  setIsExtensionSlow(false)
                }}
              >
                Close
              </Button>
            </>
          )}
        </LoadingSpinner>
      )}

      {isNostrExtensionAvailable && (
        <>
          <label className={styles.label} htmlFor="extension-login">
            Login by using a browser extension
          </label>
          <Button
            id="extension-login"
            onClick={loginWithExtension}
            variant="contained"
          >
            Extension Login
          </Button>
          <Divider
            sx={{
              fontSize: '16px'
            }}
          >
            or
          </Divider>
        </>
      )}
      <TextField
        onKeyDown={handleInputKeyDown}
        label="nip05 login / nip46 bunker string"
        helperText="Private key (Not recommended)"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        fullWidth
        margin="dense"
      />

      <Button
        disabled={!inputValue}
        onClick={login}
        variant="contained"
        fullWidth
      >
        Login
      </Button>
    </>
  )
}
