import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Copy, Laptop, Smartphone, TriangleAlert } from 'lucide-react'
import { useState, type FormEvent } from 'react'

import { SettingsLayout } from './SettingsLayout'
import {
  Alert,
  Badge,
  Button,
  Card,
  CardBody,
  CardFooter,
  CardHeader,
  EmptyState,
  Input,
  Spinner,
} from '@/components/ui'
import { useAuth } from '@/features/auth/AuthProvider'
import { PASSWORD_HINT } from '@/features/auth/passwordPolicy'
import { ApiError, api } from '@/lib/api'
import type {
  Device,
  PaginatedResponse,
  SecurityEvent,
  TwoFactorEnrolment,
  TwoFactorState,
} from '@/types/api'

/** Human labels for the security events shown to the account owner. */
const EVENT_LABELS: Record<string, string> = {
  login_success: 'Signed in',
  login_failed: 'Failed sign-in attempt',
  logout: 'Signed out',
  password_changed: 'Password changed',
  password_reset_requested: 'Password reset requested',
  email_verified: 'Email address confirmed',
  two_factor_enabled: 'Two-factor authentication enabled',
  two_factor_disabled: 'Two-factor authentication disabled',
  token_created: 'New device authorised',
  token_revoked: 'Device signed out',
  suspicious_login: 'Sign-in from an unrecognised device',
}

/**
 * Wraps an SVG string as a data URI for use in an `img` element.
 *
 * `encodeURIComponent` rather than base64 so multi-byte characters survive --
 * `btoa` throws on anything outside Latin-1.
 */
function svgDataUri(svg: string): string {
  return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`
}

export function SecuritySettingsPage() {
  const { user, refresh } = useAuth()
  const queryClient = useQueryClient()

  const twoFactor = useQuery({
    queryKey: ['two-factor'],
    queryFn: () => api.get<TwoFactorState>('/account/two-factor'),
  })

  const devices = useQuery({
    queryKey: ['devices'],
    queryFn: () => api.get<{ data: Device[] }>('/account/devices').then((r) => r.data),
  })

  const activity = useQuery({
    queryKey: ['security-activity'],
    queryFn: () => api.get<PaginatedResponse<SecurityEvent>>('/account/security/activity'),
  })

  return (
    <SettingsLayout title="Security" description="Protect the account that holds your portfolio.">
      <PasswordCard />

      <TwoFactorCard
        state={twoFactor.data}
        isLoading={twoFactor.isLoading}
        onChanged={async () => {
          await queryClient.invalidateQueries({ queryKey: ['two-factor'] })
          await refresh()
        }}
      />

      <Card>
        <CardHeader
          title="Signed-in devices"
          description="Revoke access for any device you no longer use."
          action={
            devices.data !== undefined && devices.data.length > 1 ? (
              <Button
                variant="secondary"
                size="sm"
                onClick={async () => {
                  await api.delete('/account/devices/others')
                  await queryClient.invalidateQueries({ queryKey: ['devices'] })
                }}
              >
                Sign out others
              </Button>
            ) : undefined
          }
        />

        <CardBody>
          {devices.isLoading ? (
            <Spinner label="Loading devices" />
          ) : devices.data === undefined || devices.data.length === 0 ? (
            <EmptyState
              icon={<Laptop className="size-5" />}
              title="No API devices"
              description="Devices appear here when you sign in from the mobile app or authorise an integration."
            />
          ) : (
            <ul className="flex flex-col divide-y divide-border-subtle">
              {devices.data.map((device) => (
                <li key={device.id} className="flex items-center justify-between gap-4 py-3">
                  <div className="flex min-w-0 items-center gap-3">
                    {device.platform === 'ios' || device.platform === 'android' ? (
                      <Smartphone aria-hidden className="size-4 shrink-0 text-text-muted" />
                    ) : (
                      <Laptop aria-hidden className="size-4 shrink-0 text-text-muted" />
                    )}

                    <div className="min-w-0">
                      <p className="flex items-center gap-2 truncate text-sm font-medium text-text-primary">
                        {device.name}
                        {device.is_current && <Badge tone="accent">This device</Badge>}
                      </p>
                      <p className="text-xs text-text-muted">
                        {device.last_used_at !== null
                          ? `Last used ${new Date(device.last_used_at).toLocaleString()}`
                          : 'Not used yet'}
                        {device.last_used_ip !== null && ` · ${device.last_used_ip}`}
                      </p>
                    </div>
                  </div>

                  {!device.is_current && (
                    <Button
                      variant="ghost"
                      size="sm"
                      onClick={async () => {
                        await api.delete(`/account/devices/${device.id}`)
                        await queryClient.invalidateQueries({ queryKey: ['devices'] })
                      }}
                    >
                      Revoke
                    </Button>
                  )}
                </li>
              ))}
            </ul>
          )}
        </CardBody>
      </Card>

      <Card>
        <CardHeader
          title="Recent activity"
          description="Anything here you do not recognise is worth acting on."
        />

        <CardBody>
          {activity.isLoading ? (
            <Spinner label="Loading activity" />
          ) : (
            <ul className="flex flex-col divide-y divide-border-subtle">
              {(activity.data?.data ?? []).map((event, index) => (
                <li key={index} className="flex items-start justify-between gap-4 py-3">
                  <div className="min-w-0">
                    <p className="flex items-center gap-2 text-sm text-text-primary">
                      {EVENT_LABELS[event.event] ?? event.event}
                      {event.event === 'suspicious_login' && (
                        <TriangleAlert
                          aria-label="Unrecognised device"
                          className="size-3.5 text-caution-text"
                        />
                      )}
                    </p>
                    <p className="truncate text-xs text-text-muted">
                      {event.ip_address ?? 'Unknown address'}
                      {event.country_code !== null && ` · ${event.country_code}`}
                    </p>
                  </div>

                  <span className="shrink-0 text-xs text-text-muted">
                    {event.occurred_at !== null
                      ? new Date(event.occurred_at).toLocaleString()
                      : '—'}
                  </span>
                </li>
              ))}
            </ul>
          )}
        </CardBody>
      </Card>

      {user?.two_factor_enabled === false && (
        <Alert tone="warning" title="Two-factor authentication is off">
          A domain portfolio is a valuable target. Turning on two-factor authentication is the
          single most effective thing you can do to protect it.
        </Alert>
      )}
    </SettingsLayout>
  )
}

function PasswordCard() {
  const [current, setCurrent] = useState('')
  const [next, setNext] = useState('')
  const [confirmation, setConfirmation] = useState('')
  const [error, setError] = useState<ApiError | null>(null)
  const [notice, setNotice] = useState<string | null>(null)
  const [isSaving, setIsSaving] = useState(false)

  const submit = async (event: FormEvent) => {
    event.preventDefault()
    setError(null)
    setNotice(null)
    setIsSaving(true)

    try {
      const response = await api.put<{ message: string }>('/account/password', {
        current_password: current,
        password: next,
        password_confirmation: confirmation,
      })

      setNotice(response.message)
      setCurrent('')
      setNext('')
      setConfirmation('')
    } catch (caught) {
      if (caught instanceof ApiError) {
        setError(caught)
      } else {
        throw caught
      }
    } finally {
      setIsSaving(false)
    }
  }

  return (
    <Card>
      <CardHeader title="Password" description="Changing it signs out your other devices." />

      <form onSubmit={submit}>
        <CardBody className="flex flex-col gap-5">
          {notice !== null && <Alert tone="success">{notice}</Alert>}
          {error !== null && !error.isValidation && (
            <Alert tone="error" title="Could not change password">
              {error.message}
            </Alert>
          )}

          <Input
            label="Current password"
            type="password"
            value={current}
            onChange={(event) => setCurrent(event.target.value)}
            autoComplete="current-password"
            required
            error={error?.errorFor('current_password')}
          />

          <Input
            label="New password"
            type="password"
            value={next}
            onChange={(event) => setNext(event.target.value)}
            autoComplete="new-password"
            required
            hint={PASSWORD_HINT}
            error={error?.errorFor('password')}
          />

          <Input
            label="Confirm new password"
            type="password"
            value={confirmation}
            onChange={(event) => setConfirmation(event.target.value)}
            autoComplete="new-password"
            required
            error={
              confirmation !== '' && confirmation !== next
                ? 'The passwords do not match.'
                : undefined
            }
          />
        </CardBody>

        <CardFooter>
          <Button type="submit" isLoading={isSaving}>
            Change password
          </Button>
        </CardFooter>
      </form>
    </Card>
  )
}

function TwoFactorCard({
  state,
  isLoading,
  onChanged,
}: {
  state: TwoFactorState | undefined
  isLoading: boolean
  onChanged: () => Promise<void>
}) {
  const [password, setPassword] = useState('')
  const [enrolment, setEnrolment] = useState<TwoFactorEnrolment | null>(null)
  const [code, setCode] = useState('')
  const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null)
  const [error, setError] = useState<ApiError | null>(null)
  const [isBusy, setIsBusy] = useState(false)

  const run = async (action: () => Promise<void>) => {
    setError(null)
    setIsBusy(true)

    try {
      await action()
    } catch (caught) {
      if (caught instanceof ApiError) {
        setError(caught)
      } else {
        throw caught
      }
    } finally {
      setIsBusy(false)
    }
  }

  const begin = () =>
    run(async () => {
      setEnrolment(await api.post<TwoFactorEnrolment>('/account/two-factor', {
        current_password: password,
      }))
      setPassword('')
    })

  const confirm = () =>
    run(async () => {
      const response = await api.post<{ recovery_codes: string[] }>(
        '/account/two-factor/confirm',
        { code },
      )

      setRecoveryCodes(response.recovery_codes)
      setEnrolment(null)
      setCode('')
      await onChanged()
    })

  const disable = () =>
    run(async () => {
      await api.delete('/account/two-factor', { current_password: password })
      setPassword('')
      setRecoveryCodes(null)
      await onChanged()
    })

  return (
    <Card>
      <CardHeader
        title="Two-factor authentication"
        description="Require a code from your authenticator app when signing in."
        action={
          state?.enabled === true ? <Badge tone="positive">On</Badge> : <Badge tone="caution">Off</Badge>
        }
      />

      <CardBody className="flex flex-col gap-5">
        {isLoading && <Spinner label="Loading two-factor status" />}

        {error !== null && (
          <Alert tone="error" title="Could not continue">
            {error.message}
          </Alert>
        )}

        {/*
          Shown once, immediately after enrolment. Only hashes are stored
          server-side, so if these are lost the user needs a support-assisted
          reset -- worth saying plainly rather than letting them find out later.
        */}
        {recoveryCodes !== null && (
          <div className="rounded-lg border border-caution-soft bg-caution-soft p-4">
            <p className="text-sm font-medium text-text-primary">
              Save your recovery codes now
            </p>
            <p className="mt-1 text-sm text-text-secondary">
              Each code works once, and this is the only time they are shown. Store them somewhere
              safe — we cannot show them again.
            </p>

            <ul
              data-numeric
              className="mt-3 grid grid-cols-2 gap-2 rounded-md border border-border-subtle bg-surface p-3 font-mono text-sm text-text-primary"
            >
              {recoveryCodes.map((recoveryCode) => (
                <li key={recoveryCode}>{recoveryCode}</li>
              ))}
            </ul>

            <Button
              variant="secondary"
              size="sm"
              className="mt-3"
              leadingIcon={<Copy aria-hidden className="size-3.5" />}
              onClick={() => void navigator.clipboard.writeText(recoveryCodes.join('\n'))}
            >
              Copy codes
            </Button>
          </div>
        )}

        {enrolment !== null ? (
          <div className="flex flex-col gap-4">
            <p className="text-sm text-text-secondary">
              Scan this code with your authenticator app, then enter the six-digit code it shows.
            </p>

            {/*
              The QR code is rendered server-side, so the shared secret never
              travels to a third-party QR service. It is displayed through an
              `img` with a data URI rather than injected as markup: an SVG in
              image context cannot execute script, which keeps this safe without
              relying on the response being well-behaved.
            */}
            <img
              src={svgDataUri(enrolment.qr_code_svg)}
              alt="Two-factor authentication setup QR code"
              width={200}
              height={200}
              className="w-fit rounded-lg border border-border-subtle bg-white p-3"
            />

            <details className="text-sm text-text-secondary">
              <summary className="cursor-pointer">Cannot scan the code?</summary>
              <p className="mt-2">
                Enter this key manually:{' '}
                <code data-numeric className="font-mono text-text-primary">
                  {enrolment.secret}
                </code>
              </p>
            </details>

            <Input
              label="Six-digit code"
              value={code}
              onChange={(event) => setCode(event.target.value)}
              inputMode="numeric"
              maxLength={6}
              autoComplete="one-time-code"
              error={error?.errorFor('code')}
            />

            <div className="flex gap-3">
              <Button isLoading={isBusy} onClick={() => void confirm()}>
                Turn on two-factor
              </Button>
              <Button variant="ghost" onClick={() => setEnrolment(null)}>
                Cancel
              </Button>
            </div>
          </div>
        ) : (
          <div className="flex flex-col gap-4">
            {state?.enabled === true && (
              <p className="text-sm text-text-secondary">
                {state.recovery_codes_remaining} recovery code
                {state.recovery_codes_remaining === 1 ? '' : 's'} remaining.
              </p>
            )}

            {/*
              Both enabling and disabling ask for the password. Without that, a
              hijacked session could silently attach its own authenticator, or
              strip the protection off before draining a portfolio.
            */}
            <Input
              label="Current password"
              type="password"
              value={password}
              onChange={(event) => setPassword(event.target.value)}
              autoComplete="current-password"
              hint="Confirm your password to change this setting."
              error={error?.errorFor('current_password')}
            />

            <div>
              {state?.enabled === true ? (
                <Button variant="danger" isLoading={isBusy} onClick={() => void disable()}>
                  Turn off two-factor
                </Button>
              ) : (
                <Button isLoading={isBusy} onClick={() => void begin()}>
                  Set up two-factor
                </Button>
              )}
            </div>
          </div>
        )}
      </CardBody>
    </Card>
  )
}
