// Widget shell. Three surfaces driven by GET /v3/whoami:
//
//   boot         → handshake + first whoami round-trip
//   disconnected → login action cards + the active login-flow form
//   connected    → the contacts list (single main surface); profile and
//                  session actions live in the Account modal behind the
//                  icon button next to the status pill
//
// The bridge is driven exclusively over its provisioning HTTP API
// (provisioning.ts) authenticated with MSC1960 OpenID credentials from the
// host. There is no transcript pane and no bot text-command parsing — every
// action reports its status inline, next to the control that fired it.
//
// Visual canon: «Боты» mockup at docs/design/new-direct-messages-design/
// project/stream-v2-dawn.jsx (BotsDesktop) — Dawn palette, fleet-violet
// accent, friendly product cards, no terminal styling.

import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import type { WidgetBootstrap } from './bootstrap';
import { WidgetApi } from './widget-api';
import { ProvisioningClient, type WhoamiLogin } from './provisioning';
import { describeApiError } from './errors';
import { createT, type T } from './i18n';
import { CodeForm, PasswordForm, PhoneForm, QrPanel, useLoginFlow } from './login';
import { Contacts } from './contacts';
import { useFirstAvatar } from './avatars';
import {
  Avatar,
  BackIcon,
  CommandCard,
  InfoIcon,
  LogoutIcon,
  Notice,
  PhoneIcon,
  QrIcon,
  RefreshIcon,
  Spinner,
  StatusPill,
  UserIcon,
} from './ui';

type Props = {
  bootstrap: WidgetBootstrap;
  // Constructed in main.tsx BEFORE the first render so its postMessage
  // listener is attached before the host's ClientWidgetApi fires the
  // capabilities request on iframe `load`.
  api: WidgetApi;
};

type Phase =
  | { kind: 'boot' }
  | { kind: 'error'; message: string }
  | { kind: 'disconnected' }
  | { kind: 'connected'; login: WhoamiLogin };

type NoticeState = { tone: 'error' | 'info'; text: string } | null;

const loginHandle = (login: WhoamiLogin): string => {
  const profile = login.profile ?? {};
  if (profile.username) return `@${profile.username}`;
  if (login.name) return login.name;
  if (profile.phone) return profile.phone.startsWith('+') ? profile.phone : `+${profile.phone}`;
  return login.id;
};

// --- About card + modal -------------------------------------------------------

type AboutModalProps = { t: T; onClose: () => void };

const AboutModal = ({ t, onClose }: AboutModalProps) => {
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  return (
    <div
      class="about-overlay"
      role="dialog"
      aria-modal="true"
      aria-label={t('about.title')}
      onClick={(e) => {
        if (e.target === e.currentTarget) onClose();
      }}
    >
      <div class="about-panel">
        <header class="about-header">
          <h2 class="about-title">{t('about.title')}</h2>
          <button
            type="button"
            class="about-close-x"
            onClick={onClose}
            aria-label={t('about.aria-close')}
          >
            ×
          </button>
        </header>
        <div class="about-body">
          <p>{t('about.body-1')}</p>
          <p>{t('about.body-2')}</p>
          <p>{t('about.body-3')}</p>
          <p>
            {t('about.github-label')}{' '}
            <a href={t('about.github-url')} target="_blank" rel="noreferrer">
              {t('about.github-url')}
            </a>
          </p>
          <p>{t('about.body-4')}</p>
        </div>
        <div class="about-footer">
          <button type="button" class="btn-primary" onClick={onClose}>
            {t('about.close')}
          </button>
        </div>
      </div>
    </div>
  );
};

// --- Logout card (confirm-in-place) ---------------------------------------------

type LogoutCardProps = { t: T; onConfirm: () => Promise<void> };

const LogoutCard = ({ t, onConfirm }: LogoutCardProps) => {
  const [confirming, setConfirming] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  // Sync double-submit guard for the microtask window between click and the
  // disabled state landing in the DOM.
  const inFlight = useRef(false);

  if (confirming) {
    return (
      <div class="command-card danger">
        <div class="command-card-confirm">
          <span class="command-card-confirm-prompt">{t('card.logout.confirm-prompt')}</span>
          <button
            type="button"
            class="command-card-confirm-yes"
            disabled={submitting}
            onClick={async () => {
              if (inFlight.current) return;
              inFlight.current = true;
              setSubmitting(true);
              try {
                await onConfirm();
              } finally {
                inFlight.current = false;
                setSubmitting(false);
                setConfirming(false);
              }
            }}
          >
            {submitting ? <Spinner /> : null}
            {t('card.logout.confirm-yes')}
          </button>
          <button
            type="button"
            class="command-card-confirm-no"
            disabled={submitting}
            onClick={() => setConfirming(false)}
          >
            {t('card.logout.confirm-no')}
          </button>
        </div>
      </div>
    );
  }

  return (
    <CommandCard
      icon={<LogoutIcon />}
      name={t('card.logout.name')}
      desc={t('card.logout.desc')}
      onClick={() => setConfirming(true)}
      danger
    />
  );
};

// --- Connected view -------------------------------------------------------------
// The account surface IS the main connected screen (profile card + action
// cards, telegram-bot style). «Контакты» is one of the cards — it swaps the
// surface for the contacts list (search + rows) with a back affordance.

type ConnectedViewProps = {
  login: WhoamiLogin;
  client: ProvisioningClient;
  api: WidgetApi;
  t: T;
  onNotice: (notice: NoticeState) => void;
  onLogout: () => Promise<void>;
  onOpenAbout: () => void;
};

const ConnectedView = ({
  login,
  client,
  api,
  t,
  onNotice,
  onLogout,
  onOpenAbout,
}: ConnectedViewProps) => {
  const [view, setView] = useState<'account' | 'contacts'>('account');
  // Header-row refresh control for the contacts list: bumping the token
  // re-fetches inside <Contacts>, which reports its in-flight state back so
  // the button can spin/disable.
  const [contactsReloadToken, setContactsReloadToken] = useState(0);
  const [contactsLoading, setContactsLoading] = useState(false);

  const profile = login.profile ?? {};
  const stateEvent = login.state?.state_event ?? '';
  // Transitional states (connecting/backfilling/starting) are healthy — only
  // genuinely broken sessions (BAD_CREDENTIALS, UNKNOWN_ERROR, …) get the
  // warning strip with the suggest-relink copy.
  const stateBad = !['', 'CONNECTED', 'CONNECTING', 'BACKFILLING', 'STARTING'].includes(stateEvent);

  // Own avatar comes from three sources of decreasing trust, and the first
  // one can be EMPTY or point at dead media (whoami's profile.avatar is only
  // filled once the bridge has met our own ghost), so the loader walks the
  // candidate list and shows the first that actually downloads:
  //   1. whoami profile.avatar;
  //   2. our own entry in the Telegram address book (reported up by Contacts
  //      once the list loads — the same pipeline as every other avatar);
  //   3. a one-shot resolve of our own Telegram id.
  const [selfContactMxc, setSelfContactMxc] = useState<string | undefined>(undefined);
  const [selfResolvedMxc, setSelfResolvedMxc] = useState<string | undefined>(undefined);
  useEffect(() => {
    let alive = true;
    client
      .resolveIdentifier(login.id)
      .then((self) => {
        if (alive && self?.avatar_url) setSelfResolvedMxc(self.avatar_url);
      })
      .catch(() => undefined);
    return () => {
      alive = false;
    };
  }, [client, login.id]);
  const avatarSrc = useFirstAvatar(api, [profile.avatar, selfContactMxc, selfResolvedMxc]);

  // Telegram's RemoteName prefers the username over the display name
  // (connector userToRemoteProfile), so for the headline we go the other way
  // round — the human name first — and drop the @username line when it would
  // just repeat the headline.
  const headline = profile.name || login.name || loginHandle(login);
  const username = profile.username ?? '';
  const showUsername =
    username !== '' && headline.replace(/^@/, '').toLowerCase() !== username.toLowerCase();

  return (
    <section class="section">
      <div class="section-recovery-row">
        <StatusPill tone="connected">
          {t('status.connected-as', { handle: loginHandle(login) })}
        </StatusPill>
        {view === 'contacts' ? (
          <>
            <button
              type="button"
              class={`icon-btn${contactsLoading ? ' spinning' : ''}`}
              title={t('contacts.refresh')}
              aria-label={t('contacts.refresh')}
              onClick={() => setContactsReloadToken((token) => token + 1)}
              disabled={contactsLoading}
            >
              <RefreshIcon />
            </button>
            <button
              type="button"
              class="icon-btn icon-btn-back"
              title={t('contacts.back')}
              aria-label={t('contacts.back')}
              onClick={() => setView('account')}
            >
              <BackIcon />
            </button>
          </>
        ) : null}
      </div>

      {stateBad ? (
        <Notice tone="warn">
          {t('account.state-bad', {
            reason: login.state?.message || login.state?.error || stateEvent,
          })}
        </Notice>
      ) : null}

      {view === 'account' ? (
        <div class="command-grid">
          {/* The profile card lives INSIDE the grid so it gets the same
           * width/height budget as the action cards (a full-bleed block
           * reads as a banner on wide web viewports). */}
          <div class="account-card">
            <Avatar name={headline} colorKey={login.id} size={48} src={avatarSrc} />
            <div class="account-main">
              <div class="account-name">{headline}</div>
              <div class="account-handles">
                {showUsername ? <span>@{username}</span> : null}
                {profile.phone ? (
                  <span>{profile.phone.startsWith('+') ? profile.phone : `+${profile.phone}`}</span>
                ) : null}
              </div>
            </div>
          </div>
          <CommandCard
            icon={<UserIcon />}
            name={t('card.contacts.name')}
            desc={t('card.contacts.desc')}
            onClick={() => setView('contacts')}
          />
          <CommandCard
            icon={<InfoIcon />}
            name={t('card.about.name')}
            desc={t('card.about.desc')}
            onClick={onOpenAbout}
          />
          {/* Destructive action sits last, after every constructive one. */}
          <LogoutCard t={t} onConfirm={onLogout} />
        </div>
      ) : (
        <Contacts
          client={client}
          api={api}
          t={t}
          selfId={login.id}
          onSelfAvatar={(mxc) => setSelfContactMxc(mxc)}
          reloadToken={contactsReloadToken}
          onLoadingChange={setContactsLoading}
          onOpenRoom={(roomId) => api.openMatrixRoom(roomId)}
          onError={(message) => onNotice({ tone: 'error', text: message })}
        />
      )}
    </section>
  );
};

// --- Main (provisioning configured) ----------------------------------------------

type MainProps = {
  api: WidgetApi;
  client: ProvisioningClient;
  t: T;
  handshakeOk: boolean;
};

const Main = ({ api, client, t, handshakeOk }: MainProps) => {
  const [phase, setPhase] = useState<Phase>({ kind: 'boot' });
  const [notice, setNotice] = useState<NoticeState>(null);
  const [aboutOpen, setAboutOpen] = useState(false);
  const phaseRef = useRef(phase);
  phaseRef.current = phase;

  const refresh = useCallback(
    async (opts: { quiet?: boolean } = {}): Promise<void> => {
      try {
        const whoami = await client.whoami();
        const login = whoami.logins?.[0];
        setPhase(login ? { kind: 'connected', login } : { kind: 'disconnected' });
      } catch (err) {
        // Quiet refreshes (visibility change, post-action recheck) keep the
        // last good surface; only the boot path degrades to the error screen.
        if (opts.quiet && phaseRef.current.kind !== 'boot') return;
        setPhase({ kind: 'error', message: describeApiError(err, t) });
      }
    },
    [client, t]
  );

  const flow = useLoginFlow(client, t, {
    onComplete: () => {
      setNotice({ tone: 'info', text: t('notice.login-success') });
      void refresh();
    },
    onError: (message) => setNotice({ tone: 'error', text: message }),
  });
  const flowUiKind = flow.ui.kind;

  // First whoami once the widget-api handshake completes.
  useEffect(() => {
    if (handshakeOk) void refresh();
  }, [handshakeOk, refresh]);

  // Re-check on tab return — bridge state can change from another device
  // (e.g. session revoked in Telegram's Settings → Devices). Never during an
  // active login flow: stomping the form mid-entry would lose user input.
  useEffect(() => {
    const onVisible = () => {
      if (document.visibilityState !== 'visible') return;
      if (flowUiKind !== 'idle') return;
      if (phaseRef.current.kind === 'boot') return;
      void refresh({ quiet: true });
    };
    document.addEventListener('visibilitychange', onVisible);
    return () => document.removeEventListener('visibilitychange', onVisible);
  }, [refresh, flowUiKind]);

  // Success notices fade on their own; errors stay until dismissed.
  useEffect(() => {
    if (!notice || notice.tone !== 'info') return undefined;
    const timer = window.setTimeout(() => setNotice(null), 6000);
    return () => window.clearTimeout(timer);
  }, [notice]);

  const logout = useCallback(async () => {
    if (phaseRef.current.kind !== 'connected') return;
    try {
      await client.logout(phaseRef.current.login.id);
      setNotice({ tone: 'info', text: t('notice.logged-out') });
    } catch (err) {
      setNotice({ tone: 'error', text: describeApiError(err, t) });
    }
    await refresh();
  }, [client, refresh, t]);

  const [refreshingCard, setRefreshingCard] = useState(false);
  const refreshCard = useCallback(async () => {
    if (refreshingCard) return;
    setRefreshingCard(true);
    const start = Date.now();
    await refresh();
    const elapsed = Date.now() - start;
    window.setTimeout(() => setRefreshingCard(false), Math.max(0, 500 - elapsed));
  }, [refresh, refreshingCard]);

  const renderDisconnected = () => {
    if (flow.ui.kind === 'form') {
      const FormComponent =
        flow.ui.form === 'phone' ? PhoneForm : flow.ui.form === 'code' ? CodeForm : PasswordForm;
      return (
        <section class="section">
          <FormComponent flow={flow} t={t} />
        </section>
      );
    }
    if (flow.ui.kind === 'qr') {
      return (
        <section class="section">
          <QrPanel url={flow.ui.url} t={t} onCancel={flow.cancel} />
        </section>
      );
    }
    if (flow.ui.kind === 'starting') {
      return (
        <section class="section">
          <div class="section-recovery-row">
            <StatusPill tone="checking">
              {flow.ui.flow === 'qr' ? t('auth-card.qr.preparing') : t('status.checking')}
            </StatusPill>
            <button type="button" class="recovery-action" onClick={flow.cancel}>
              {t('auth-card.cancel')}
            </button>
          </div>
        </section>
      );
    }
    return (
      <section class="section">
        <StatusPill tone="disconnected">{t('status.disconnected')}</StatusPill>
        <div class="command-grid">
          <CommandCard
            icon={<PhoneIcon />}
            name={t('card.login.name')}
            desc={t('card.login.desc')}
            onClick={() => flow.start('phone')}
          />
          <CommandCard
            icon={<QrIcon />}
            name={t('card.login-qr.name')}
            desc={t('card.login-qr.desc')}
            onClick={() => flow.start('qr')}
          />
          <CommandCard
            icon={<RefreshIcon />}
            name={t('card.refresh.name')}
            desc={refreshingCard ? t('card.refresh.in-flight') : t('card.refresh.desc')}
            onClick={() => void refreshCard()}
            disabled={refreshingCard}
            spinning={refreshingCard}
          />
          <CommandCard
            icon={<InfoIcon />}
            name={t('card.about.name')}
            desc={t('card.about.desc')}
            onClick={() => setAboutOpen(true)}
          />
        </div>
      </section>
    );
  };

  return (
    <div class="app">
      {notice ? (
        <div class="notice-slot">
          <Notice
            tone={notice.tone === 'info' ? 'info' : 'error'}
            onDismiss={() => setNotice(null)}
          >
            {notice.text}
          </Notice>
        </div>
      ) : null}

      {phase.kind === 'boot' ? (
        <section class="section">
          <StatusPill tone="checking">
            {handshakeOk ? t('status.checking') : t('boot.connecting')}
          </StatusPill>
        </section>
      ) : null}

      {phase.kind === 'error' ? (
        <section class="section">
          <div class="section-recovery-row">
            <StatusPill tone="checking">{phase.message}</StatusPill>
            <button type="button" class="recovery-action" onClick={() => void refresh()}>
              <RefreshIcon />
              {t('error.retry')}
            </button>
          </div>
        </section>
      ) : null}

      {phase.kind === 'disconnected' ? renderDisconnected() : null}

      {phase.kind === 'connected' ? (
        <ConnectedView
          login={phase.login}
          client={client}
          api={api}
          t={t}
          onNotice={setNotice}
          onLogout={logout}
          onOpenAbout={() => setAboutOpen(true)}
        />
      ) : null}

      {aboutOpen ? <AboutModal t={t} onClose={() => setAboutOpen(false)} /> : null}
    </div>
  );
};

// --- App root --------------------------------------------------------------------

export function App({ bootstrap, api }: Props) {
  const [theme, setTheme] = useState<'light' | 'dark'>(bootstrap.theme);
  const [handshakeOk, setHandshakeOk] = useState(false);
  const t = useMemo(() => createT(bootstrap.clientLanguage), [bootstrap.clientLanguage]);

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
  }, [theme]);

  useEffect(() => {
    api.on('ready', () => setHandshakeOk(true));
    api.on('themeChange', (name) => setTheme(name));
    return () => api.dispose();
    // `api` is a module-level singleton owned by main.tsx; the effect
    // intentionally runs once at mount.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Capture-phase interceptor for `<a target="_blank">` — cross-origin
  // iframes inside Capacitor's Android WebView silently drop those clicks,
  // so we route them through the host's openExternalUrl side-channel.
  // Modifier-clicks pass through for desktop new-tab behavior.
  useEffect(() => {
    const onClick = (e: MouseEvent) => {
      const anchor = (e.target as HTMLElement | null)?.closest?.(
        'a[target="_blank"]'
      ) as HTMLAnchorElement | null;
      if (!anchor?.href) return;
      if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
      e.preventDefault();
      api.openExternalUrl(anchor.href);
    };
    document.addEventListener('click', onClick, true);
    return () => document.removeEventListener('click', onClick, true);
  }, [api]);

  const client = useMemo(
    () =>
      bootstrap.provisioningUrl
        ? new ProvisioningClient(bootstrap.provisioningUrl, bootstrap.userId, () =>
            api.getOpenIdCredentials()
          )
        : null,
    [api, bootstrap.provisioningUrl, bootstrap.userId]
  );

  if (!client) {
    return (
      <div class="app">
        <div class="error-banner">
          <strong>{t('config.missing.title')}</strong>
          {t('config.missing.body')}
        </div>
      </div>
    );
  }

  return <Main api={api} client={client} t={t} handshakeOk={handshakeOk} />;
}
