import Feather from '@expo/vector-icons/Feather'
import { useAuth } from '@state/AuthProvider'
import { useQuery } from '@tanstack/react-query'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { usePreventScreenCapture } from 'expo-screen-capture'
import * as WebBrowser from 'expo-web-browser'
import React, { useEffect, useRef, useState } from 'react'
import {
  ActivityIndicator,
  Keyboard,
  KeyboardAvoidingView,
  Platform,
  Pressable,
  ScrollView as RNScrollView,
  TouchableWithoutFeedback,
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useConfirmDialog } from 'src/hooks/useConfirmDialog'
import { APP } from 'src/lib/branding'
import { ICON } from 'src/lib/icons'
import { authQueries } from 'src/lib/queries/auth'
import type { LoginOutcome } from 'src/state/AuthProvider'
import {
  AlertDialog,
  Button,
  Form,
  Input,
  Text,
  useTheme,
  useToastController,
  XStack,
  YStack,
} from 'tamagui'

const DOMAIN_REGEX = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/

// Strip scheme prefix, trailing slash, surrounding whitespace, and lowercase
// before DOMAIN_REGEX runs — common pastes/autocompletes (`https://x.social`,
// `x.social/`, ` x.social `) would otherwise be rejected by the strict regex.
function normalizeDomainInput(raw: string): string {
  return raw
    .trim()
    .toLowerCase()
    .replace(/^https?:\/\//, '')
    .replace(/\/+$/, '')
}

export default function LoginScreen() {
  // Block screenshots / screen recording while the user is on the credential
  // surface. Defends against shoulder-surfing capture (the user's own device
  // recording in the background, screen-share apps, accessibility services
  // grabbing frames). Released automatically when the screen unmounts.
  usePreventScreenCapture('login')
  const params = useLocalSearchParams<{ server?: string; addAccount?: string }>()
  const isAddMode = params.addAccount === '1'

  // Cold-start seed: only honour `params.server` if it parses as a domain.
  // The same gate runs in the useEffect below for navigations that change
  // the param after mount; both paths need to refuse hostile deep links
  // (`halflight://handleLogin?server=attacker.com/<…>`) instead of
  // pre-populating the input one tap away from a real OAuth round-trip.
  const seededServer =
    params.server && DOMAIN_REGEX.test(params.server) ? params.server : ''
  const [server, setServer] = useState(seededServer)
  const [loading, setLoading] = useState(false)
  const [searchValue, setSearchValue] = useState(seededServer)
  const [isValidDomain, setIsValidDomain] = useState(seededServer.length > 0)
  const scrollViewRef = useRef(null)
  const inputRef = useRef<any>(null)
  const { login } = useAuth()
  const router = useRouter()
  const theme = useTheme()
  const toast = useToastController()
  const { confirm, dialog: confirmDialog } = useConfirmDialog()

  const showError = (message: string) => {
    // `preset: 'error'` is read by GlobalToast and mapped to Tamagui's red
    // sub-theme. `duration` is bumped above the 5s default so users get
    // time to read the longer post-submit messages (server-name embedded
    // copy can run 80+ chars).
    toast.show('Login failed', { message, preset: 'error', duration: 6500 })
  }

  // Navigate explicitly on success — handleLogin lives outside both
  // Stack.Protected guards (so it's reachable in both auth states), which
  // means we no longer get the implicit "auth flips → router pops" magic
  // that the route group provided.
  const onLoginSuccess = () => {
    if (isAddMode) {
      // dismissTo pops back to the Accounts screen if it's in history,
      // otherwise pushes onto the stack. The Accounts screen lands in
      // Phase 6; the cast is here because expo-router's typed-routes
      // graph doesn't include the file yet — drop it once Phase 6 lands.
      router.dismissTo('/(auth)/settings/accounts' as any)
    } else {
      router.replace('/(auth)/(tabs)/home')
    }
  }

  const handleOutcome = async (outcome: LoginOutcome) => {
    if (outcome.ok) {
      onLoginSuccess()
      return
    }
    if (outcome.reason === 'preflight') {
      // Mastodon Pass §6.1 — for unsupported forks, surface a confirm-style
      // dialog instead of a transient toast so the user can tap through to a
      // page where they can flag interest in fork support. Other preflight
      // reasons (unreachable / outdated / mastodon-too-old) stay on the
      // toast — the message already tells the user what to do.
      if (outcome.preflightReason === 'unsupported-server') {
        const learnMore = await confirm({
          title: 'Server not supported',
          message: outcome.message,
          confirmLabel: 'Learn more',
          cancelLabel: 'Close',
        })
        if (learnMore) {
          await WebBrowser.openBrowserAsync(APP.supportUrl)
        }
        return
      }
      showError(outcome.message)
      return
    }
    if (outcome.reason === 'callback-error') {
      showError(outcome.message)
      return
    }
    if (outcome.reason === 'cap-reached') {
      showError(outcome.message)
      return
    }
    if (outcome.reason === 'cancelled') {
      if ('needs2faRetry' in outcome) {
        // Unpatched server: the webview-dismissed-on-/ strand may be the
        // upstream 2FA-redirect bug, offer a retry. Copy stays vague about
        // the cause because the same strand happens for non-2FA reasons
        // (network hiccup, user tapped Cancel on the approval page).
        const ok = await confirm({
          title: 'Login incomplete',
          message:
            'If you have two-factor authentication enabled, your server may require an extra step. Would you like to try again?',
          confirmLabel: 'Try again',
        })
        if (ok) await handleOutcome(await outcome.retry())
        return
      }
      // Server preserves the in-flight URL through 2FA (capability flag
      // on), so dismissal is a real cancel. Surface a toast — silent
      // failure would leave the user wondering what happened.
      toast.show('Login cancelled', { duration: 3500 })
      return
    }
  }

  useEffect(() => {
    // Refuse to seed state on a malformed `?server=` deep-link param. A
    // hostile `halflight://handleLogin?server=attacker.com/?<…>` would
    // otherwise land on the screen with the field pre-populated and one tap
    // away from a real OAuth round-trip. Submit-time validation is the
    // second line of defence (`handleLogin` and `handleFindInstance`); the
    // useEffect-time gate keeps the typed-URL path from ever pre-filling
    // an invalid value.
    if (params.server && DOMAIN_REGEX.test(params.server)) {
      setServer(params.server)
      setSearchValue(params.server)
      setIsValidDomain(true)
    }
  }, [params.server])

  // Server suggestions are decorative — never block screen render on this
  // network call. pixelfed.org being slow/unreachable would otherwise leave
  // the user staring at an empty screen while React Query retries
  // (per client.ts retry config) for ~7s before giving up.
  const { data: serversData } = useQuery(authQueries.openServers())

  const filteredServers = React.useMemo(() => {
    if (!serversData || !searchValue) return []

    const query = searchValue.toLowerCase()
    return serversData
      .filter((s) => s.domain.toLowerCase().includes(query))
      .slice(0, 3)
      .map((s) => s.domain)
  }, [serversData, searchValue])

  const handleSearchInputChange = (text: string) => {
    setSearchValue(text)
    const normalized = normalizeDomainInput(text)
    const valid = DOMAIN_REGEX.test(normalized)
    setIsValidDomain(valid)
    if (valid) {
      setServer(normalized)
    }
  }

  const handleSuggestionSelect = async (suggestion: string) => {
    setServer(suggestion)
    setSearchValue(suggestion)
    setIsValidDomain(true)
    Keyboard.dismiss()
    // Mastodon Pass Phase 1 §2.4 — scope template is now derived from the
    // preflight-classified server type; callers no longer pass it.
    await handleOutcome(await login(suggestion, { isAddAccount: isAddMode }))
  }

  const handleLogin = async () => {
    // Re-normalize at submit time: covers iOS autofill paths that populate
    // the input without firing onChangeText, and the disabled-button bypass
    // below (button is tappable even when isValidDomain is false so the
    // toast can explain *why*).
    const normalized = normalizeDomainInput(searchValue || server)
    if (!normalized || !DOMAIN_REGEX.test(normalized)) {
      showError('Please enter a valid server domain')
      return
    }

    setLoading(true)
    try {
      await handleOutcome(await login(normalized, { isAddAccount: isAddMode }))
    } catch (_error) {
      // Most auth failures now resolve as typed LoginOutcomes (preflight,
      // callback-error, cap-reached) and route through handleOutcome above.
      // This catch is the truly-unexpected fallback — keep the message
      // specific enough that a user can tell whether to retry or change
      // server, even when the underlying error is opaque.
      showError(`Couldn't sign in to ${normalized}. Please try again.`)
    } finally {
      setLoading(false)
    }
  }

  const navigateToSignup = () => {
    router.push('/handleSignup')
  }

  const handleFindInstance = async () => {
    await WebBrowser.openAuthSessionAsync(
      'https://recovery.pixelfed.org/forgot-instance',
      `${APP.scheme}://login`
    ).then(async (res) => {
      if (res.type === 'success' && res.url) {
        const parsedUrl = new URL(res.url)
        const searchParams = new URLSearchParams(parsedUrl.search)
        const curDomain = searchParams.get('domain') ?? ''
        // The forgot-instance redirect is a recovery-flow callback that
        // ultimately surfaces a `?domain=…` value any well-known-compliant
        // host can rewrite. Gate `login()` on the regex so a malformed
        // value can't reach the OAuth round-trip; show a toast and refuse
        // to seed state otherwise.
        if (!DOMAIN_REGEX.test(curDomain)) {
          showError('Invalid server domain')
          return
        }
        setServer(curDomain)
        setSearchValue(curDomain)
        setIsValidDomain(true)
        await handleOutcome(await login(curDomain, { isAddAccount: isAddMode }))
      }
    })
  }

  const helpBody = `${APP.name} connects to the Fediverse — a decentralized photo sharing network made up of many independent servers (or "instances").\n\nEach server has its own rules, moderators, and community. Your account can still interact with users on any other server.\n\nEnter the domain of the server you'd like to sign in to. If you don't have one yet, tap "Sign up" on the previous screen to browse open communities.`

  return (
    <SafeAreaView
      style={{
        backgroundColor: theme.background.val,
        flexGrow: 1,
      }}
      edges={['top']}
    >
      <KeyboardAvoidingView
        behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
        style={{ flex: 1 }}
      >
        <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
          <RNScrollView
            ref={scrollViewRef}
            contentContainerStyle={{ flexGrow: 1 }}
            keyboardShouldPersistTaps="handled"
            onScroll={() => Keyboard.dismiss()}
          >
            <Form onSubmit={handleLogin} flex={1}>
              <YStack px="$4" pt="$4" pb="$6" gap="$4" flexGrow={1} minHeight="100%">
                <XStack justifyContent="flex-end">
                  <AlertDialog>
                    <AlertDialog.Trigger asChild>
                      <Pressable
                        hitSlop={12}
                        accessibilityRole="button"
                        accessibilityLabel="About this app"
                      >
                        <Feather
                          name="help-circle"
                          size={ICON.md}
                          color={theme.color.val}
                        />
                      </Pressable>
                    </AlertDialog.Trigger>
                    <AlertDialog.Portal>
                      <AlertDialog.Overlay
                        key="overlay"
                        transition="quick"
                        opacity={0.5}
                        enterStyle={{ opacity: 0 }}
                        exitStyle={{ opacity: 0 }}
                      />
                      <AlertDialog.Content
                        key="content"
                        bordered
                        elevate
                        transition="quick"
                        gap="$3"
                        p="$5"
                        enterStyle={{ opacity: 0, y: -10 }}
                        exitStyle={{ opacity: 0, y: 10 }}
                      >
                        <AlertDialog.Title>About {APP.name}</AlertDialog.Title>
                        <AlertDialog.Description>{helpBody}</AlertDialog.Description>
                        <XStack gap="$3" justifyContent="flex-end">
                          <AlertDialog.Cancel asChild>
                            <Button size="$3" chromeless>
                              <Button.Text>Close</Button.Text>
                            </Button>
                          </AlertDialog.Cancel>
                          <AlertDialog.Action asChild>
                            <Button
                              size="$3"
                              onPress={() => WebBrowser.openBrowserAsync(APP.website)}
                            >
                              <Button.Text>More info</Button.Text>
                            </Button>
                          </AlertDialog.Action>
                        </XStack>
                      </AlertDialog.Content>
                    </AlertDialog.Portal>
                  </AlertDialog>
                </XStack>

                <YStack mt="$4" gap="$4">
                  <Text fontSize="$8" fontWeight="600" color="$color">
                    {isAddMode ? 'Add another account' : 'Sign in to your server'}
                  </Text>
                  <Text fontSize="$4" color="$colorSecondary">
                    {isAddMode
                      ? 'Enter the domain of the server hosting the account you want to add.'
                      : 'Enter the domain of your Pixelfed or Mastodon instance.'}
                  </Text>

                  <YStack gap="$3" mt="$2">
                    <Input
                      ref={inputRef}
                      size="$5"
                      placeholder={APP.serverPlaceholder}
                      value={searchValue}
                      onChangeText={handleSearchInputChange}
                      autoCapitalize="none"
                      autoCorrect={false}
                      autoComplete="off"
                      keyboardType="url"
                      returnKeyType="go"
                      onSubmitEditing={() => {
                        if (isValidDomain && !loading) {
                          Keyboard.dismiss()
                          void handleLogin()
                        }
                      }}
                      focusStyle={{ borderColor: '$borderColorStrong' }}
                    />

                    {filteredServers.length > 0 && (
                      <YStack
                        bg="$backgroundSecondary"
                        borderColor="$borderColor"
                        borderWidth={1}
                        borderRadius="$4"
                        overflow="hidden"
                      >
                        {filteredServers.map((suggestion, index) => (
                          <XStack
                            key={suggestion}
                            onPress={() => handleSuggestionSelect(suggestion)}
                            px="$4"
                            py="$3"
                            alignItems="center"
                            cursor="pointer"
                            borderTopWidth={index === 0 ? 0 : 1}
                            borderColor="$borderColor"
                            pressStyle={{ opacity: 0.7, bg: '$backgroundTertiary' }}
                          >
                            <Text color="$color" fontSize="$5" fontWeight="500" flex={1}>
                              {suggestion}
                            </Text>
                            <Feather
                              name="arrow-right"
                              size={ICON.sm}
                              color={theme.colorTertiary.val}
                            />
                          </XStack>
                        ))}
                      </YStack>
                    )}

                    {isValidDomain && searchValue && filteredServers.length === 0 && (
                      <Text color="$colorSecondary" fontSize="$3">
                        Tap {isAddMode ? 'Add account' : 'Continue'} to sign in to{' '}
                        <Text color="$color" fontWeight="600">
                          {searchValue}
                        </Text>
                      </Text>
                    )}
                  </YStack>
                </YStack>

                <XStack gap="$2" alignItems="center">
                  <Text fontSize="$4" color="$colorSecondary">
                    Forgot your instance?
                  </Text>
                  <Pressable onPress={handleFindInstance}>
                    <Text fontSize="$4" color="$color" fontWeight="600">
                      Find it
                    </Text>
                  </Pressable>
                </XStack>

                <YStack flex={1} />

                <YStack gap="$3" alignItems="center">
                  {isAddMode ? null : (
                    <XStack gap="$2">
                      <Text fontSize="$4" color="$colorSecondary">
                        Don't have an account yet?
                      </Text>
                      <Pressable onPress={navigateToSignup}>
                        <Text fontSize="$4" color="$color" fontWeight="600">
                          Sign up
                        </Text>
                      </Pressable>
                    </XStack>
                  )}

                  {/* Tappable even when isValidDomain is false: the press
                      runs handleLogin which surfaces a toast explaining the
                      problem. A hard-disabled button with no feedback reads
                      as "broken" to users who pasted a URL with a scheme. */}
                  <Form.Trigger asChild disabled={loading}>
                    <Button size="$4" width="100%" opacity={!isValidDomain ? 0.6 : 1}>
                      {loading ? (
                        <ActivityIndicator color={theme.color.val} />
                      ) : (
                        <Button.Text fontSize="$4" fontWeight="600">
                          {isAddMode ? 'Add account' : 'Continue'}
                        </Button.Text>
                      )}
                    </Button>
                  </Form.Trigger>
                </YStack>
              </YStack>
            </Form>
          </RNScrollView>
        </TouchableWithoutFeedback>
      </KeyboardAvoidingView>
      {confirmDialog}
    </SafeAreaView>
  )
}
