import React, {
  ChangeEventHandler,
  FormEventHandler,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import {
  Box,
  Text,
  IconButton,
  Icon,
  Icons,
  Input,
  Avatar,
  Button,
  Overlay,
  OverlayBackdrop,
  OverlayCenter,
  Modal,
  Dialog,
  Header,
  config,
  color,
  Spinner,
  PopOut,
  RectCords,
  Chip,
} from 'folds';
import { Method } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import { SettingsSelect } from '../../../components/settings-select/SettingsSelect';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getAccountData, setAccountData } from '../../../utils/accountData';
import { presenceStateFromSetting } from '../../../hooks/usePresenceUpdater';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { UserAvatar } from '../../../components/user-avatar';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { nameInitials } from '../../../utils/common';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useFilePicker } from '../../../hooks/useFilePicker';
import { useObjectURL } from '../../../hooks/useObjectURL';
import { stopPropagation } from '../../../utils/keyboard';
import { ImageEditor } from '../../../components/image-editor';
import { ModalWide } from '../../../styles/Modal.css';
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
import { CompactUploadCardRenderer } from '../../../components/upload-card';
import { useCapabilities } from '../../../hooks/useCapabilities';
import { Presence, useUserPresence } from '../../../hooks/useUserPresence';
import { ProfileDecoration } from './ProfileDecoration';
import { EmojiBoard } from '../../../components/emoji-board';
import { useStatusPresets } from '../../../hooks/useStatusPresets';
import {
  BUILT_IN_STATUS_PRESETS,
  StatusPreset,
  makePresetId,
  normalizeLabel,
} from '../../../utils/statusPresets';

type ProfileProps = {
  profile: UserProfile;
  userId: string;
};
function ProfileAvatar({ profile, userId }: ProfileProps) {
  const mx = useMatrixClient();
  const useAuthentication = useMediaAuthentication();
  const capabilities = useCapabilities();
  const [alertRemove, setAlertRemove] = useState(false);
  const disableSetAvatar = capabilities['m.set_avatar_url']?.enabled === false;

  const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
  const avatarUrl = profile.avatarUrl
    ? (mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined)
    : undefined;

  const [imageFile, setImageFile] = useState<File>();
  const imageFileURL = useObjectURL(imageFile);
  const uploadAtom = useMemo(() => {
    if (imageFile) return createUploadAtom(imageFile);
    return undefined;
  }, [imageFile]);

  const pickFile = useFilePicker(setImageFile, false);

  const handleRemoveUpload = useCallback(() => {
    setImageFile(undefined);
  }, []);

  const handleUploaded = useCallback(
    (upload: UploadSuccess) => {
      const { mxc } = upload;
      mx.setAvatarUrl(mxc);
      handleRemoveUpload();
    },
    [mx, handleRemoveUpload],
  );

  const handleRemoveAvatar = () => {
    mx.setAvatarUrl('');
    setAlertRemove(false);
  };

  return (
    <SettingTile
      title={
        <Text as="span" size="L400">
          Avatar
        </Text>
      }
      after={
        <Avatar size="500" radii="300">
          <UserAvatar
            userId={userId}
            src={avatarUrl}
            renderFallback={() => <Text size="H4">{nameInitials(defaultDisplayName)}</Text>}
          />
        </Avatar>
      }
    >
      {uploadAtom ? (
        <Box gap="200" direction="Column">
          <CompactUploadCardRenderer
            uploadAtom={uploadAtom}
            onRemove={handleRemoveUpload}
            onComplete={handleUploaded}
          />
        </Box>
      ) : (
        <Box gap="200">
          <Button
            onClick={() => pickFile('image/*')}
            size="300"
            variant="Secondary"
            fill="Soft"
            outlined
            radii="300"
            disabled={disableSetAvatar}
          >
            <Text size="B300">Upload</Text>
          </Button>
          {avatarUrl && (
            <Button
              size="300"
              variant="Critical"
              fill="None"
              radii="300"
              disabled={disableSetAvatar}
              onClick={() => setAlertRemove(true)}
            >
              <Text size="B300">Remove</Text>
            </Button>
          )}
        </Box>
      )}

      {imageFileURL && (
        <Overlay open={false} backdrop={<OverlayBackdrop />}>
          <OverlayCenter>
            <FocusTrap
              focusTrapOptions={{
                initialFocus: false,
                onDeactivate: handleRemoveUpload,
                clickOutsideDeactivates: true,
                escapeDeactivates: stopPropagation,
              }}
            >
              <Modal className={ModalWide} variant="Surface" size="500">
                <ImageEditor
                  name={imageFile?.name ?? 'Unnamed'}
                  url={imageFileURL}
                  requestClose={handleRemoveUpload}
                />
              </Modal>
            </FocusTrap>
          </OverlayCenter>
        </Overlay>
      )}

      <Overlay open={alertRemove} backdrop={<OverlayBackdrop />}>
        <OverlayCenter>
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              onDeactivate: () => setAlertRemove(false),
              clickOutsideDeactivates: true,
              escapeDeactivates: stopPropagation,
            }}
          >
            <Dialog variant="Surface">
              <Header
                style={{
                  padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
                  borderBottomWidth: config.borderWidth.B300,
                }}
                variant="Surface"
                size="500"
              >
                <Box grow="Yes">
                  <Text size="H4">Remove Avatar</Text>
                </Box>
                <IconButton
                  size="300"
                  onClick={() => setAlertRemove(false)}
                  radii="300"
                  aria-label="Cancel"
                >
                  <Icon src={Icons.Cross} />
                </IconButton>
              </Header>
              <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
                <Box direction="Column" gap="200">
                  <Text priority="400">Are you sure you want to remove profile avatar?</Text>
                </Box>
                <Button variant="Critical" onClick={handleRemoveAvatar}>
                  <Text size="B400">Remove</Text>
                </Button>
              </Box>
            </Dialog>
          </FocusTrap>
        </OverlayCenter>
      </Overlay>
    </SettingTile>
  );
}

function ProfileDisplayName({ profile, userId }: ProfileProps) {
  const mx = useMatrixClient();
  const capabilities = useCapabilities();
  const disableSetDisplayname = capabilities['m.set_displayname']?.enabled === false;

  const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
  const [displayName, setDisplayName] = useState<string>(defaultDisplayName);

  const [changeState, changeDisplayName] = useAsyncCallback(
    useCallback((name: string) => mx.setDisplayName(name), [mx]),
  );
  const changingDisplayName = changeState.status === AsyncStatus.Loading;

  useEffect(() => {
    setDisplayName(defaultDisplayName);
  }, [defaultDisplayName]);

  const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
    const name = evt.currentTarget.value;
    setDisplayName(name);
  };

  const handleReset = () => {
    setDisplayName(defaultDisplayName);
  };

  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
    evt.preventDefault();
    if (changingDisplayName) return;

    const target = evt.target as HTMLFormElement | undefined;
    const displayNameInput = target?.displayNameInput as HTMLInputElement | undefined;
    const name = displayNameInput?.value;
    if (!name) return;

    changeDisplayName(name);
  };

  const hasChanges = displayName !== defaultDisplayName;
  return (
    <SettingTile
      title={
        <Text as="span" size="L400">
          Display Name
        </Text>
      }
    >
      <Box direction="Column" grow="Yes" gap="100">
        <Box
          as="form"
          onSubmit={handleSubmit}
          gap="200"
          aria-disabled={changingDisplayName || disableSetDisplayname}
        >
          <Box grow="Yes" direction="Column">
            <Input
              required
              name="displayNameInput"
              aria-label="Display name"
              value={displayName}
              onChange={handleChange}
              variant="Secondary"
              radii="300"
              maxLength={255}
              style={{ paddingRight: config.space.S200 }}
              readOnly={changingDisplayName || disableSetDisplayname}
              after={
                hasChanges &&
                !changingDisplayName && (
                  <IconButton
                    type="reset"
                    onClick={handleReset}
                    size="300"
                    radii="300"
                    variant="Secondary"
                    aria-label="Reset display name"
                  >
                    <Icon src={Icons.Cross} size="100" />
                  </IconButton>
                )
              }
            />
          </Box>
          <Button
            size="400"
            variant={hasChanges ? 'Success' : 'Secondary'}
            fill={hasChanges ? 'Solid' : 'Soft'}
            outlined
            radii="300"
            disabled={!hasChanges || changingDisplayName}
            type="submit"
          >
            {changingDisplayName && <Spinner variant="Success" fill="Solid" size="300" />}
            <Text size="B400">Save</Text>
          </Button>
        </Box>
      </Box>
    </SettingTile>
  );
}

export const STATUS_EXPIRY_KEY = (id: string) => `lotus-status-expiry-${id}`;
export const STATUS_MSG_KEY = (id: string) => `lotus-status-msg-${id}`;

const CLEAR_AFTER_OPTIONS = [
  { label: 'Never', value: '0' },
  { label: '30 minutes', value: String(30 * 60 * 1000) },
  { label: '1 hour', value: String(60 * 60 * 1000) },
  { label: '4 hours', value: String(4 * 60 * 60 * 1000) },
  { label: '8 hours', value: String(8 * 60 * 60 * 1000) },
  { label: 'Until midnight', value: 'today' },
  { label: '1 day', value: String(24 * 60 * 60 * 1000) },
  { label: '7 days', value: String(7 * 24 * 60 * 60 * 1000) },
];

function getMsFromOption(value: string): number {
  if (value === '0') return 0;
  if (value === 'today') {
    const eod = new Date();
    eod.setHours(23, 59, 59, 999);
    return eod.getTime() - Date.now();
  }
  return parseInt(value, 10);
}

function ProfileStatus() {
  const mx = useMatrixClient();
  const userId = mx.getUserId()!;
  const presence = useUserPresence(userId);
  const [presenceStatus] = useSetting(settingsAtom, 'presenceStatus');
  const [hidePresence] = useSetting(settingsAtom, 'hidePresence');

  const initialStatus = presence?.status ?? localStorage.getItem(STATUS_MSG_KEY(userId)) ?? '';
  const [statusMsg, setStatusMsg] = useState<string>(initialStatus);
  // True while the user has unsaved local edits — prevents a server presence
  // echo from overwriting what the user is currently typing/inserting.
  const statusDirtyRef = useRef(false);
  // The last remote status we synced into the input. Presence heartbeats fire
  // every few seconds carrying the SAME status; the sync effect must only react
  // when the remote value actually changes, otherwise a repeated heartbeat can
  // overwrite an unsaved local edit (e.g. an emoji just inserted) the instant
  // the dirty flag is out of sync. Seeded with the value the input started on.
  const lastSyncedRemoteRef = useRef<string>(initialStatus);
  // The value we most recently applied (with the apply time). A presence
  // heartbeat can echo the PREVIOUS status just after we save the new one; that
  // stale echo would otherwise revert the input. We ignore non-matching echoes
  // until our own echo lands or a short window elapses (bounded so a genuinely
  // dropped echo can't block real cross-device updates forever).
  const pendingAppliedRef = useRef<{ value: string; ts: number } | null>(null);
  const [clearAfter, setClearAfter] = useState('0');
  const [emojiAnchor, setEmojiAnchor] = useState<RectCords>();
  const { presets, addPreset, removePreset } = useStatusPresets();

  // Sync input when another device changes the status.
  // Skipped while the user has unsaved local edits to avoid clobbering
  // mid-flight input (e.g. an emoji being inserted), and while presence data has
  // not loaded yet (presence === undefined is "no info", NOT a clear).
  useEffect(() => {
    if (statusDirtyRef.current || !presence) return;
    // An offline/invisible presence carries an empty status_msg by design (see
    // usePresenceUpdater.setOffline), so ignore it — reading it as a clear would
    // wipe the saved status on every invisible toggle.
    if (presence.presence === Presence.Offline) return;
    const remoteStatus = presence.status ?? '';
    // Only act on an actual remote change. Repeated heartbeats carrying the same
    // status are ignored so they can never clobber an unsaved local edit.
    if (remoteStatus === lastSyncedRemoteRef.current) return;
    lastSyncedRemoteRef.current = remoteStatus;
    const pending = pendingAppliedRef.current;
    if (pending) {
      if (remoteStatus === pending.value) {
        pendingAppliedRef.current = null; // our own echo landed — accept it
      } else if (Date.now() - pending.ts < 15_000) {
        return; // stale echo of the previous status; ignore within the window
      } else {
        pendingAppliedRef.current = null; // window elapsed — accept external changes
      }
    }
    if (remoteStatus) {
      setStatusMsg(remoteStatus);
      localStorage.setItem(STATUS_MSG_KEY(userId), remoteStatus);
    } else {
      // Another device CLEARED the status. Mirror it locally AND drop the stored
      // value, otherwise usePresenceUpdater.readStatus() re-sends the stale
      // message on the next presence heartbeat.
      setStatusMsg('');
      localStorage.removeItem(STATUS_MSG_KEY(userId));
    }
  }, [presence, userId]);

  const [saveState, saveStatus] = useAsyncCallback(
    useCallback(
      (msg: string) =>
        mx.setPresence({
          // Derive presence from the user's chosen setting so writing a status
          // never overrides Invisible/DND/Idle (e.g. outing an Invisible user).
          presence: presenceStateFromSetting(presenceStatus, hidePresence),
          status_msg: msg,
        }),
      [mx, presenceStatus, hidePresence],
    ),
  );
  const saving = saveState.status === AsyncStatus.Loading;

  const handleEmojiSelect = useCallback((unicode: string) => {
    statusDirtyRef.current = true;
    setStatusMsg((prev) => prev + unicode);
    setEmojiAnchor(undefined);
  }, []);

  const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
    statusDirtyRef.current = true;
    setStatusMsg(evt.currentTarget.value);
  };

  // Save a status message + auto-clear timer. Shared by the Save button and the
  // one-click presets so all three go through exactly the same server write and
  // localStorage bookkeeping.
  const applyStatus = useCallback(
    (rawMsg: string, clearAfterValue: string) => {
      statusDirtyRef.current = false;
      const msg = rawMsg.trim();
      // Guard against a stale presence echo reverting this value (see the sync effect).
      pendingAppliedRef.current = { value: msg, ts: Date.now() };
      saveStatus(msg).catch(() => undefined);

      if (msg) {
        localStorage.setItem(STATUS_MSG_KEY(userId), msg);
      } else {
        localStorage.removeItem(STATUS_MSG_KEY(userId));
      }

      const delayMs = getMsFromOption(clearAfterValue);
      if (msg && delayMs > 0) {
        // Persist the expiry timestamp; the always-mounted StatusExpiryMonitor
        // (ClientNonUIFeatures) fires the auto-clear even when Settings is closed.
        localStorage.setItem(STATUS_EXPIRY_KEY(userId), String(Date.now() + delayMs));
      } else {
        localStorage.removeItem(STATUS_EXPIRY_KEY(userId));
      }
    },
    [saveStatus, userId],
  );

  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
    evt.preventDefault();
    if (saving) return;
    applyStatus(statusMsg, clearAfter);
  };

  // Preset click = one-click apply: reflect it in the inputs and save immediately.
  const applyPreset = useCallback(
    (preset: StatusPreset) => {
      if (saving) return;
      setStatusMsg(preset.label);
      setClearAfter(preset.clearAfter);
      applyStatus(preset.label, preset.clearAfter);
    },
    [saving, applyStatus],
  );

  const handleSaveCurrent = useCallback(() => {
    const label = statusMsg.trim();
    if (!label) return;
    addPreset({ id: makePresetId(), label, clearAfter }).catch(() => undefined);
  }, [statusMsg, clearAfter, addPreset]);

  // Hide any saved preset that duplicates a built-in (it already shows under
  // "Quick statuses"), so the same status can't appear as two chips.
  const customPresets = useMemo(() => {
    const builtInLabels = new Set(BUILT_IN_STATUS_PRESETS.map((p) => normalizeLabel(p.label)));
    return presets.filter((p) => !builtInLabels.has(normalizeLabel(p.label)));
  }, [presets]);

  const handleClear = () => {
    statusDirtyRef.current = false;
    pendingAppliedRef.current = { value: '', ts: Date.now() };
    setStatusMsg('');
    localStorage.removeItem(STATUS_MSG_KEY(userId));
    localStorage.removeItem(STATUS_EXPIRY_KEY(userId));
    // Preserve the user's chosen presence when clearing the status message.
    mx.setPresence({
      presence: presenceStateFromSetting(presenceStatus, hidePresence),
      status_msg: '',
    }).catch(() => undefined);
  };

  const hasChanges = statusMsg !== (presence?.status ?? '');

  return (
    <SettingTile
      title={
        <Text as="span" size="L400">
          Status Message
        </Text>
      }
      description={
        <Text size="T200" priority="300">
          Shown below your name in member lists. Supports emoji.
        </Text>
      }
    >
      <Box direction="Column" grow="Yes" gap="200">
        {/* Quick statuses — built-in presets, one click applies message + timer */}
        <Box direction="Column" gap="100">
          <Text id="status-quick-heading" size="T200" priority="300">
            Quick statuses
          </Text>
          <Box gap="100" wrap="Wrap" role="group" aria-labelledby="status-quick-heading">
            {BUILT_IN_STATUS_PRESETS.map((preset) => (
              <Chip
                key={preset.id}
                type="button"
                variant="Secondary"
                fill="Soft"
                radii="Pill"
                disabled={saving}
                onClick={() => applyPreset(preset)}
              >
                <Text as="span" size="B300">
                  {preset.label}
                </Text>
              </Chip>
            ))}
          </Box>
        </Box>

        {/* Your presets — saved from the current status; synced via account data */}
        <Box direction="Column" gap="100">
          <Text id="status-custom-heading" size="T200" priority="300">
            Your presets
          </Text>
          <Box gap="200" wrap="Wrap" role="group" aria-labelledby="status-custom-heading">
            {customPresets.map((preset) => (
              // gap="0" keeps the chip and its delete X reading as one unit; the
              // parent row's larger gap="200" separates one preset from the next.
              <Box key={preset.id} alignItems="Center" gap="0">
                <Chip
                  type="button"
                  variant="Secondary"
                  fill="Soft"
                  radii="Pill"
                  disabled={saving}
                  onClick={() => applyPreset(preset)}
                >
                  <Text as="span" size="B300">
                    {preset.label}
                  </Text>
                </Chip>
                <IconButton
                  type="button"
                  size="300"
                  radii="Pill"
                  variant="Secondary"
                  fill="None"
                  aria-label={`Delete preset ${preset.label}`}
                  onClick={() => removePreset(preset.id).catch(() => undefined)}
                >
                  <Icon size="100" src={Icons.Cross} />
                </IconButton>
              </Box>
            ))}
            <Chip
              type="button"
              variant="Success"
              fill="Soft"
              radii="Pill"
              outlined
              disabled={saving || !statusMsg.trim()}
              onClick={handleSaveCurrent}
              before={<Icon size="100" src={Icons.Plus} />}
            >
              <Text as="span" size="B300">
                Save current
              </Text>
            </Chip>
          </Box>
        </Box>

        <Box as="form" onSubmit={handleSubmit} gap="200" alignItems="Center" aria-disabled={saving}>
          <Box grow="Yes" direction="Column" gap="100">
            <Input
              name="statusMsgInput"
              aria-label="Status message"
              value={statusMsg}
              onChange={handleChange}
              placeholder="What's on your mind?"
              variant="Secondary"
              radii="300"
              readOnly={saving}
              maxLength={64}
            />
            <Text
              size="T200"
              style={{
                textAlign: 'right',
                opacity: statusMsg.length >= 56 ? 1 : 0.45,
                color:
                  statusMsg.length >= 64
                    ? color.Critical.Main
                    : statusMsg.length >= 56
                      ? color.Warning.Main
                      : undefined,
              }}
            >
              {statusMsg.length} / 64
            </Text>
          </Box>
          <PopOut
            anchor={emojiAnchor}
            position="Top"
            align="End"
            content={
              <EmojiBoard
                imagePackRooms={[]}
                returnFocusOnDeactivate={false}
                onEmojiSelect={handleEmojiSelect}
                // A status message is plain-text presence (`status_msg`), so it
                // can't hold a custom mxc-image emoji — show unicode only, so every
                // emoji in the picker actually inserts (custom ones silently no-op'd
                // because there's no onCustomEmojiSelect here).
                hideCustomEmojis
                requestClose={() => setEmojiAnchor(undefined)}
              />
            }
          >
            <IconButton
              type="button"
              size="400"
              radii="400"
              variant="Surface"
              fill="Soft"
              outlined
              aria-label="Insert emoji"
              aria-expanded={!!emojiAnchor}
              aria-haspopup="dialog"
              onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
                const rect = evt.currentTarget.getBoundingClientRect();
                setEmojiAnchor((prev) => (prev ? undefined : rect));
              }}
            >
              <Icon src={Icons.Smile} size="400" />
            </IconButton>
          </PopOut>
          <Button
            size="400"
            variant={hasChanges ? 'Success' : 'Secondary'}
            fill={hasChanges ? 'Solid' : 'Soft'}
            outlined
            radii="300"
            disabled={!hasChanges || saving}
            type="submit"
          >
            {saving && <Spinner variant="Success" fill="Solid" size="300" />}
            <Text size="B400">Save</Text>
          </Button>
        </Box>
        {saveState.status === AsyncStatus.Error && (
          <Text size="T200" style={{ color: color.Critical.Main }}>
            Failed to save status — server may be rate limiting. Try again.
          </Text>
        )}
        <Box alignItems="Center" gap="200">
          <Text size="T200" priority="300" style={{ whiteSpace: 'nowrap', flexShrink: 0 }}>
            Auto-clear after:
          </Text>
          <SettingsSelect
            value={clearAfter}
            options={CLEAR_AFTER_OPTIONS}
            onChange={setClearAfter}
            aria-label="Auto-clear status after"
          />
        </Box>
        {(presence?.status || statusMsg) && (
          <Button
            size="300"
            variant="Critical"
            fill="None"
            radii="300"
            type="button"
            onClick={handleClear}
            disabled={saving}
          >
            <Text size="B300">Clear Status</Text>
          </Button>
        )}
      </Box>
    </SettingTile>
  );
}

const COMMON_TIMEZONES = [
  'UTC',
  'America/New_York',
  'America/Chicago',
  'America/Denver',
  'America/Los_Angeles',
  'America/Toronto',
  'America/Vancouver',
  'America/Sao_Paulo',
  'Europe/London',
  'Europe/Paris',
  'Europe/Berlin',
  'Europe/Moscow',
  'Africa/Cairo',
  'Asia/Dubai',
  'Asia/Kolkata',
  'Asia/Singapore',
  'Asia/Tokyo',
  'Asia/Shanghai',
  'Australia/Sydney',
  'Pacific/Auckland',
];

function ProfilePronouns() {
  const mx = useMatrixClient();
  const userId = mx.getUserId()!;

  const [pronouns, setPronouns] = useState<string>('');
  const [savedPronouns, setSavedPronouns] = useState<string>('');

  useEffect(() => {
    mx.http
      .authedRequest<{ 'm.pronouns': string }>(
        Method.Get,
        `/profile/${encodeURIComponent(userId)}/m.pronouns`,
      )
      .then((res) => {
        const val = res['m.pronouns'] ?? '';
        setPronouns(val);
        setSavedPronouns(val);
      })
      .catch(() => {
        setPronouns('');
        setSavedPronouns('');
      });
  }, [mx, userId]);

  const [saveState, savePronouns] = useAsyncCallback(
    useCallback(
      (value: string) =>
        mx.http
          .authedRequest(
            Method.Put,
            `/profile/${encodeURIComponent(userId)}/m.pronouns`,
            undefined,
            { 'm.pronouns': value },
          )
          .then(() => {
            setSavedPronouns(value);
          }),
      [mx, userId],
    ),
  );
  const saving = saveState.status === AsyncStatus.Loading;

  const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
    setPronouns(evt.currentTarget.value);
  };

  const handleReset = () => {
    setPronouns(savedPronouns);
  };

  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
    evt.preventDefault();
    if (saving) return;
    savePronouns(pronouns.trim());
  };

  const hasChanges = pronouns !== savedPronouns;

  return (
    <SettingTile
      title={
        <Text as="span" size="L400">
          Pronouns
        </Text>
      }
      description={
        <Text size="T200" priority="300">
          Shown on your profile. Visible to other users.
        </Text>
      }
    >
      <Box direction="Column" grow="Yes" gap="100">
        <Box as="form" onSubmit={handleSubmit} gap="200" aria-disabled={saving}>
          <Box grow="Yes" direction="Column">
            <Input
              name="pronounsInput"
              aria-label="Pronouns"
              value={pronouns}
              onChange={handleChange}
              placeholder="e.g. they/them, she/her"
              variant="Secondary"
              radii="300"
              maxLength={64}
              readOnly={saving}
              after={
                hasChanges &&
                !saving && (
                  <IconButton
                    type="reset"
                    onClick={handleReset}
                    size="300"
                    radii="300"
                    variant="Secondary"
                    aria-label="Reset pronouns"
                  >
                    <Icon src={Icons.Cross} size="100" />
                  </IconButton>
                )
              }
            />
          </Box>
          <Button
            size="400"
            variant={hasChanges ? 'Success' : 'Secondary'}
            fill={hasChanges ? 'Solid' : 'Soft'}
            outlined
            radii="300"
            disabled={!hasChanges || saving}
            type="submit"
          >
            {saving && <Spinner variant="Success" fill="Solid" size="300" />}
            <Text size="B400">Save</Text>
          </Button>
        </Box>
        {saveState.status === AsyncStatus.Error && (
          <Text size="T200" style={{ color: color.Critical.Main }}>
            Failed to save pronouns. Try again.
          </Text>
        )}
      </Box>
    </SettingTile>
  );
}

function ProfileTimezone() {
  const mx = useMatrixClient();
  const userId = mx.getUserId()!;

  const [timezone, setTimezone] = useState<string>('');
  const [savedTimezone, setSavedTimezone] = useState<string>('');

  useEffect(() => {
    const cached = getAccountData<{ timezone: string }>(mx, 'im.lotus.timezone');
    if (cached?.timezone) {
      setTimezone(cached.timezone);
      setSavedTimezone(cached.timezone);
    }
    // Also fetch from server in case account data hasn't synced yet
    mx.http
      .authedRequest<{ timezone: string }>(
        Method.Get,
        `/user/${encodeURIComponent(userId)}/account_data/im.lotus.timezone`,
      )
      .then((res) => {
        const val = res.timezone ?? '';
        setTimezone(val);
        setSavedTimezone(val);
      })
      .catch(() => {
        /* no stored timezone yet */
      });
  }, [mx, userId]);

  const [saveState, saveTimezone] = useAsyncCallback(
    useCallback(
      (value: string) =>
        Promise.all([
          // Self-fallback: account data is readable by useExtendedProfile for the
          // own user even on servers without extended-profile (m.tz) support.
          setAccountData(mx, 'im.lotus.timezone', { timezone: value }),
          // Mirror the pronouns write path so OTHER users can read the timezone
          // via the m.tz profile field. Best-effort: standard Synapse rejects
          // unknown profile fields, so a failure here must not fail the save.
          mx.http
            .authedRequest(Method.Put, `/profile/${encodeURIComponent(userId)}/m.tz`, undefined, {
              'm.tz': value,
            })
            .catch(() => undefined),
        ]).then(() => {
          setSavedTimezone(value);
        }),
      [mx, userId],
    ),
  );
  const saving = saveState.status === AsyncStatus.Loading;

  const handleReset = () => {
    setTimezone(savedTimezone);
  };

  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
    evt.preventDefault();
    if (saving) return;
    saveTimezone(timezone);
  };

  const hasChanges = timezone !== savedTimezone;

  return (
    <SettingTile
      title={
        <Text as="span" size="L400">
          Timezone
        </Text>
      }
      description={
        <Text size="T200" priority="300">
          Your local timezone. Visible to other users.
        </Text>
      }
    >
      <Box direction="Column" grow="Yes" gap="100">
        <Box as="form" onSubmit={handleSubmit} gap="200" alignItems="Center" aria-disabled={saving}>
          <Box grow="Yes" direction="Column">
            <SettingsSelect
              value={timezone}
              options={[
                { value: '', label: '— select timezone —' },
                ...COMMON_TIMEZONES.map((tz) => ({ value: tz, label: tz })),
              ]}
              onChange={setTimezone}
              disabled={saving}
              aria-label="Timezone"
            />
          </Box>
          {hasChanges && !saving && (
            <IconButton
              type="button"
              onClick={handleReset}
              size="400"
              radii="300"
              variant="Secondary"
              aria-label="Reset timezone"
            >
              <Icon src={Icons.Cross} size="100" />
            </IconButton>
          )}
          <Button
            size="400"
            variant={hasChanges ? 'Success' : 'Secondary'}
            fill={hasChanges ? 'Solid' : 'Soft'}
            outlined
            radii="300"
            disabled={!hasChanges || saving}
            type="submit"
          >
            {saving && <Spinner variant="Success" fill="Solid" size="300" />}
            <Text size="B400">Save</Text>
          </Button>
        </Box>
        {saveState.status === AsyncStatus.Error && (
          <Text size="T200" style={{ color: color.Critical.Main }}>
            Failed to save timezone. Try again.
          </Text>
        )}
      </Box>
    </SettingTile>
  );
}

export function Profile() {
  const mx = useMatrixClient();
  const userId = mx.getUserId()!;
  const profile = useUserProfile(userId);

  return (
    <Box direction="Column" gap="100">
      <Text size="L400">Profile</Text>
      <SequenceCard
        className={SequenceCardStyle}
        variant="SurfaceVariant"
        direction="Column"
        gap="400"
      >
        <ProfileAvatar userId={userId} profile={profile} />
        <ProfileDisplayName userId={userId} profile={profile} />
        <ProfileStatus />
        <ProfilePronouns />
        <ProfileTimezone />
        <ProfileDecoration />
      </SequenceCard>
    </Box>
  );
}
