/**
 * Modal popped open when an `<openhuman-link path="...">` pill is clicked
 * inside an agent message bubble.
 *
 * The pill dispatches a `window` `CustomEvent('openhuman-link', { detail: { path } })`;
 * this component listens for it, opens the modal, and routes to a focused
 * mini-flow per path. Keeps the chat in view (no react-router navigation)
 * so the user can complete the action and return to the agent without
 * losing the conversation.
 *
 * Mounted once at AppShell root.
 */
import { useCallback, useEffect, useMemo, useState } from 'react';

import { useChannelDefinitions } from '../hooks/useChannelDefinitions';
import { useT } from '../lib/i18n/I18nContext';
import {
  ensureNotificationPermission,
  getNotificationPermissionState,
  type NotificationPermissionState,
  showNativeNotification,
} from '../lib/nativeNotifications/tauriBridge';
import { isTauri, purgeWebviewAccount } from '../services/webviewAccountService';
import { addAccount, removeAccount, setActiveAccount } from '../store/accountsSlice';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import {
  type Account,
  type AccountProvider,
  type AccountStatus,
  PROVIDERS,
} from '../types/accounts';
import { BILLING_DASHBOARD_URL, DISCORD_INVITE_URL } from '../utils/links';
import { openUrl } from '../utils/openUrl';
import { ProviderIcon } from './accounts/providerIcons';
import ChannelSetupModal from './channels/ChannelSetupModal';

interface OpenhumanLinkEvent {
  path: string;
}

export const OPENHUMAN_LINK_EVENT = 'openhuman-link';

const ALLOWED_PATHS = [
  'settings/notifications',
  'settings/billing',
  'settings/messaging',
  'community/discord',
  'community/discord-report',
  'accounts/setup',
] as const;

type AllowedPath = (typeof ALLOWED_PATHS)[number];

const ALLOWED_PATHS_SET = new Set<string>(ALLOWED_PATHS);

const OpenhumanLinkModal = () => {
  const { t } = useT();
  const [activePath, setActivePath] = useState<AllowedPath | null>(null);

  useEffect(() => {
    const handler = (event: Event) => {
      const detail = (event as CustomEvent<OpenhumanLinkEvent>).detail;
      if (detail?.path && ALLOWED_PATHS_SET.has(detail.path)) {
        setActivePath(detail.path as AllowedPath);
      }
    };
    window.addEventListener(OPENHUMAN_LINK_EVENT, handler);
    return () => window.removeEventListener(OPENHUMAN_LINK_EVENT, handler);
  }, []);

  const close = useCallback(() => setActivePath(null), []);

  if (!activePath) return null;

  // Telegram (and any future channel) gets the dedicated `ChannelSetupModal`
  // already used by Skills + Settings instead of a bespoke body wrapper.
  // It manages its own portal + backdrop, so render it standalone.
  if (activePath === 'settings/messaging') {
    return <MessagingSetupBridge onClose={close} />;
  }

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
      onClick={close}
      role="dialog"
      aria-modal="true">
      <div
        className="w-full max-w-md rounded-2xl bg-white dark:bg-neutral-900 shadow-xl overflow-hidden"
        onClick={e => e.stopPropagation()}>
        <div className="flex items-center justify-between border-b border-stone-100 dark:border-neutral-800 px-5 py-3">
          <h2 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
            {titleForPath(activePath, t)}
          </h2>
          <button
            type="button"
            onClick={close}
            aria-label={t('common.close')}
            className="rounded p-1 text-stone-500 dark:text-neutral-400 hover:bg-stone-100 dark:hover:bg-neutral-800/60 hover:text-stone-800 dark:hover:text-neutral-100">
            <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M6 6l12 12M6 18L18 6"
              />
            </svg>
          </button>
        </div>
        <div className="p-5">{renderBody(activePath, close)}</div>
      </div>
    </div>
  );
};

/**
 * Resolves the Telegram channel definition and hands it to the shared
 * `ChannelSetupModal` (same component the Settings → Messaging panel
 * uses). When definitions are still loading we render a tiny placeholder
 * so the user gets feedback instead of a flashing screen.
 */
const MessagingSetupBridge = ({ onClose }: { onClose: () => void }) => {
  const { t } = useT();
  const { definitions, loading } = useChannelDefinitions();
  const telegram = useMemo(() => definitions.find(d => d.id === 'telegram') ?? null, [definitions]);

  if (loading && !telegram) {
    return (
      <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
        <div className="rounded-2xl bg-white dark:bg-neutral-900 px-6 py-4 text-sm text-stone-600 dark:text-neutral-300 shadow-xl">
          {t('app.openhumanLink.loadingChannelSetup')}
        </div>
      </div>
    );
  }

  if (!telegram) {
    return (
      <div
        className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
        onClick={onClose}>
        <div
          className="rounded-2xl bg-white dark:bg-neutral-900 p-6 text-sm text-stone-700 dark:text-neutral-200 shadow-xl max-w-sm"
          onClick={e => e.stopPropagation()}>
          <p>{t('app.openhumanLink.telegramUnavailable')}</p>
          <div className="mt-3 flex justify-end">
            <button
              type="button"
              onClick={onClose}
              className="rounded-lg border border-stone-200 dark:border-neutral-800 px-3 py-1.5 text-xs font-medium text-stone-700 dark:text-neutral-200 hover:bg-stone-50 dark:hover:bg-neutral-800/60">
              {t('common.close')}
            </button>
          </div>
        </div>
      </div>
    );
  }

  return <ChannelSetupModal definition={telegram} onClose={onClose} />;
};

function titleForPath(path: AllowedPath, t: (k: string) => string): string {
  switch (path) {
    case 'settings/notifications':
      return t('app.openhumanLink.title.notifications');
    case 'settings/billing':
      return t('app.openhumanLink.title.billing');
    case 'settings/messaging':
      return t('app.openhumanLink.title.messaging');
    case 'community/discord':
      return t('app.openhumanLink.title.discord');
    case 'community/discord-report':
      return t('app.openhumanLink.title.discordReport');
    case 'accounts/setup':
      return t('app.openhumanLink.title.accounts');
  }
}

function renderBody(path: AllowedPath, close: () => void) {
  switch (path) {
    case 'settings/notifications':
      return <NotificationsBody close={close} />;
    case 'settings/billing':
      return <BillingBody close={close} />;
    case 'settings/messaging':
      // Routed via the dedicated `MessagingSetupBridge` above; this case
      // is kept to satisfy the path-completeness check but is unreachable
      // because the parent component returns the bridge before calling
      // `renderBody`.
      return null;
    case 'community/discord':
      return <DiscordBody close={close} />;
    case 'community/discord-report':
      return <DiscordReportBody close={close} />;
    case 'accounts/setup':
      return <AccountsSetupBody close={close} />;
  }
}

// ── Notifications ────────────────────────────────────────────────────────

const NotificationsBody = ({ close }: { close: () => void }) => {
  const { t } = useT();
  const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
  const [error, setError] = useState<string | null>(null);
  const [permissionState, setPermissionState] = useState<NotificationPermissionState>('unknown');

  useEffect(() => {
    let mounted = true;
    void getNotificationPermissionState({ requestIfNeeded: false }).then(next => {
      if (!mounted) return;
      setPermissionState(next);
    });
    return () => {
      mounted = false;
    };
  }, []);

  const handleAllow = async () => {
    if (status === 'sending') {
      return;
    }

    setStatus('sending');
    setError(null);
    try {
      if (!isTauri()) {
        setStatus('error');
        setError(t('app.openhumanLink.notifications.desktopOnly'));
        return;
      }

      const granted = await ensureNotificationPermission();
      if (!granted) {
        const nextState = await getNotificationPermissionState({ requestIfNeeded: false });
        setPermissionState(nextState);
        setStatus('error');
        setError(t('app.openhumanLink.notifications.permissionOff'));
        return;
      }
      const sendResult = await showNativeNotification({
        title: t('app.openhumanLink.notifications.welcomeTitle'),
        body: t('app.openhumanLink.notifications.welcomeBody'),
        tag: 'welcome-notification-test',
      });
      if (!sendResult.delivered) {
        setStatus('error');
        setError(sendResult.error ?? t('app.openhumanLink.notifications.triggerFailed'));
        return;
      }
      setPermissionState('granted');
      setStatus('sent');
    } catch (e) {
      setStatus('error');
      setError(e instanceof Error ? e.message : String(e));
    }
  };

  return (
    <div className="space-y-4 text-sm text-stone-700 dark:text-neutral-200">
      <p>{t('app.openhumanLink.notifications.intro')}</p>
      {permissionState === 'denied' && (
        <div className="rounded-xl border border-coral-200 bg-coral-50 dark:bg-coral-500/15 p-3 text-xs text-coral-700 dark:text-coral-300">
          {t('app.openhumanLink.notifications.blocked')}
          <br />
          {t('app.openhumanLink.notifications.blockedStep1')}
          <br />
          {t('app.openhumanLink.notifications.blockedStep2')}
          <br />
          {t('app.openhumanLink.notifications.blockedStep3')}
        </div>
      )}
      {(permissionState === 'prompt' || permissionState === 'unknown') && (
        <div className="rounded-xl border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 p-3 text-xs text-stone-700 dark:text-neutral-200">
          {t('app.openhumanLink.notifications.promptHint')}
        </div>
      )}
      <button
        type="button"
        onClick={() => void handleAllow()}
        disabled={status === 'sending'}
        className="w-full rounded-xl bg-primary-500 text-white text-sm font-medium py-2.5 hover:bg-primary-600 transition-colors disabled:opacity-60">
        {status === 'sending'
          ? t('app.openhumanLink.notifications.asking')
          : status === 'error'
            ? t('app.openhumanLink.notifications.retry')
            : t('app.openhumanLink.notifications.send')}
      </button>
      {status === 'sent' && (
        <p className="text-xs text-sage-700">{t('app.openhumanLink.notifications.sent')}</p>
      )}
      {status === 'error' && (
        <p className="text-xs text-coral-600">
          {t('app.openhumanLink.notifications.sendFailed').replace('{error}', error ?? '')}
        </p>
      )}
      <DoneFooter close={close} />
    </div>
  );
};

// ── Billing ──────────────────────────────────────────────────────────────

const BillingBody = ({ close }: { close: () => void }) => {
  const { t } = useT();
  return (
    <div className="space-y-4 text-sm text-stone-700 dark:text-neutral-200">
      <div className="rounded-xl border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 p-4">
        <p className="text-xs uppercase tracking-wide text-stone-500 dark:text-neutral-400">
          {t('app.openhumanLink.billing.trialCredit')}
        </p>
        <p className="mt-1 text-2xl font-semibold text-stone-900 dark:text-neutral-100">
          {t('onboarding.runtimeChoice.cloud.creditHighlight')}
        </p>
        <p className="mt-1 text-xs text-stone-500 dark:text-neutral-400">
          {t('app.openhumanLink.billing.trialDesc')}
        </p>
      </div>
      <button
        type="button"
        onClick={() => {
          void openUrl(BILLING_DASHBOARD_URL).catch(() => {});
        }}
        className="w-full rounded-xl bg-primary-500 text-white text-sm font-medium py-2.5 hover:bg-primary-600 transition-colors">
        {t('app.openhumanLink.billing.openDashboard')}
      </button>
      <DoneFooter close={close} skipLabel={t('app.openhumanLink.billing.stayOnTrial')} />
    </div>
  );
};

// ── Discord ──────────────────────────────────────────────────────────────

const DiscordBody = ({ close }: { close: () => void }) => {
  const { t } = useT();
  return (
    <div className="space-y-4 text-sm text-stone-700 dark:text-neutral-200">
      <p>{t('app.openhumanLink.discord.intro')}</p>
      <ul className="space-y-1.5 text-xs text-stone-600 dark:text-neutral-300 pl-1">
        <li className="flex items-center gap-2">
          <span className="h-1.5 w-1.5 rounded-full bg-primary-400 flex-shrink-0" />
          {t('app.openhumanLink.discord.perk1')}
        </li>
        <li className="flex items-center gap-2">
          <span className="h-1.5 w-1.5 rounded-full bg-primary-400 flex-shrink-0" />
          {t('app.openhumanLink.discord.perk2')}
        </li>
        <li className="flex items-center gap-2">
          <span className="h-1.5 w-1.5 rounded-full bg-primary-400 flex-shrink-0" />
          {t('app.openhumanLink.discord.perk3')}
        </li>
        <li className="flex items-center gap-2">
          <span className="h-1.5 w-1.5 rounded-full bg-primary-400 flex-shrink-0" />
          {t('app.openhumanLink.discord.perk4')}
        </li>
      </ul>
      <button
        type="button"
        onClick={async () => {
          try {
            await openUrl(DISCORD_INVITE_URL);
          } catch {
            // Ignore launcher errors from OS URL handler failures.
          }
        }}
        className="w-full rounded-xl bg-primary-500 text-white text-sm font-medium py-2.5 hover:bg-primary-600 transition-colors">
        {t('app.openhumanLink.discord.openInvite')}
      </button>
      <DoneFooter close={close} skipLabel={t('app.openhumanLink.maybeLater')} />
    </div>
  );
};

/**
 * Error-report variant of the Discord modal. Shown when an agent error pill
 * with path "community/discord-report" is clicked. Distinct from DiscordBody
 * (join-community flow):
 *  - Leads with an apology/acknowledgement copy.
 *  - Offers an "Open Discord" primary button to jump straight to the server
 *    (and closes the modal).
 */
const DiscordReportBody = ({ close }: { close: () => void }) => {
  const { t } = useT();

  return (
    <div className="space-y-4 text-sm text-stone-700 dark:text-neutral-200">
      <p>{t('app.openhumanLink.discordReport.intro')}</p>
      <button
        type="button"
        onClick={async () => {
          try {
            await openUrl(DISCORD_INVITE_URL);
          } finally {
            close();
          }
        }}
        className="w-full rounded-xl bg-primary-500 px-3 py-2.5 text-sm font-medium text-white hover:bg-primary-600 transition-colors">
        {t('app.openhumanLink.discordReport.openDiscord')}
      </button>
    </div>
  );
};

// ── Accounts setup (multi-channel toggle list) ──────────────────────────

/**
 * Curated list of providers shown in the welcome flow's "Connect your apps"
 * step. Excludes call-only surfaces (`google-meet`, `zoom`) and dev-only
 * (`browserscan`) — those still appear in the full Add Account modal but
 * aren't a "set this up during onboarding" target.
 */
const ACCOUNTS_SETUP_PROVIDERS: readonly AccountProvider[] = [
  'whatsapp',
  'wechat',
  'telegram',
  'slack',
  'discord',
  'linkedin',
];

function makeAccountId(): string {
  const c = globalThis.crypto;
  if (c && typeof c.randomUUID === 'function') return c.randomUUID();
  if (c && typeof c.getRandomValues === 'function') {
    const bytes = new Uint8Array(4);
    c.getRandomValues(bytes);
    const suffix = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
    return `acct-${Date.now().toString(36)}-${suffix}`;
  }
  return `acct-${Date.now().toString(36)}`;
}

/**
 * Translation key + color for a given account lifecycle status. Returns a
 * `labelKey` (not a literal) so the caller can localize it via `useT()` —
 * this is a module-level helper with no hook scope of its own.
 */
export function statusDisplay(status: AccountStatus): { labelKey: string; dotClass: string } {
  switch (status) {
    case 'open':
      return { labelKey: 'app.openhumanLink.status.connected', dotClass: 'bg-emerald-500' };
    case 'loading':
      return { labelKey: 'app.openhumanLink.status.loading', dotClass: 'bg-amber-400' };
    case 'pending':
      return { labelKey: 'app.openhumanLink.status.needsSignIn', dotClass: 'bg-amber-400' };
    case 'timeout':
      return { labelKey: 'app.openhumanLink.status.timedOut', dotClass: 'bg-red-400' };
    case 'error':
      return { labelKey: 'app.openhumanLink.status.error', dotClass: 'bg-red-400' };
    case 'closed':
      return { labelKey: 'app.openhumanLink.status.closed', dotClass: 'bg-stone-300' };
  }
}

const AccountsSetupBody = ({ close }: { close: () => void }) => {
  const { t } = useT();
  const dispatch = useAppDispatch();
  const accountsById = useAppSelector(s => s.accounts.accounts);
  const order = useAppSelector(s => s.accounts.order);

  // Track accounts added during this modal session so "Done" can navigate.
  // Uses state (not ref) so the CTA label re-renders when toggles change.
  const [newlyAdded, setNewlyAdded] = useState<Map<string, string>>(new Map());

  // Map provider → first existing account (one provider, one row).
  const accountByProvider = useMemo(() => {
    const map = new Map<AccountProvider, Account>();
    for (const id of order) {
      const acct = accountsById[id];
      if (acct && !map.has(acct.provider)) map.set(acct.provider, acct);
    }
    return map;
  }, [accountsById, order]);

  const providerDescriptors = useMemo(
    () =>
      ACCOUNTS_SETUP_PROVIDERS.map(id => PROVIDERS.find(p => p.id === id)).filter(
        Boolean
      ) as typeof PROVIDERS,
    []
  );

  const handleToggle = (providerId: AccountProvider, label: string, currentlyOn: boolean) => {
    if (currentlyOn) {
      const existing = accountByProvider.get(providerId);
      if (!existing) return;
      void purgeWebviewAccount(existing.id).catch(() => {});
      setNewlyAdded(prev => {
        const next = new Map(prev);
        next.delete(existing.id);
        return next;
      });
      dispatch(removeAccount({ accountId: existing.id }));
      return;
    }
    const acct: Account = {
      id: makeAccountId(),
      provider: providerId,
      label,
      createdAt: new Date().toISOString(),
      status: 'pending',
    };
    setNewlyAdded(prev => new Map(prev).set(acct.id, label));
    dispatch(addAccount(acct));
  };

  const handleDone = () => {
    close();
    // Activate the first newly-added account so the shell-level WebviewHost
    // opens the auth flow immediately without mutating the current route.
    const firstNew = [...newlyAdded.keys()][0];
    if (firstNew) {
      dispatch(setActiveAccount(firstNew));
    }
  };

  // Dynamic CTA based on what's been toggled on
  const firstNewLabel = [...newlyAdded.values()][0];
  const doneLabel = firstNewLabel
    ? t('app.openhumanLink.accounts.continueWith').replace('{label}', firstNewLabel)
    : t('app.openhumanLink.accounts.done');

  return (
    <div className="space-y-4 text-sm text-stone-700 dark:text-neutral-200">
      <p>{t('app.openhumanLink.accounts.intro')}</p>
      <div className="space-y-2">
        {providerDescriptors.map(p => {
          const acct = accountByProvider.get(p.id);
          const on = !!acct;
          const status = acct?.status;
          return (
            <div
              key={p.id}
              className="flex items-center gap-3 rounded-xl border border-stone-100 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-3">
              <ProviderIcon provider={p.id} className="h-5 w-5 flex-none" />
              <div className="min-w-0 flex-1">
                <div className="text-sm font-medium text-stone-900 dark:text-neutral-100">
                  {p.label}
                </div>
                {on && status ? (
                  <div className="flex items-center gap-1.5">
                    <span
                      className={`inline-block h-1.5 w-1.5 rounded-full ${statusDisplay(status).dotClass}`}
                    />
                    <span className="text-xs text-stone-500 dark:text-neutral-400">
                      {t(statusDisplay(status).labelKey)}
                    </span>
                  </div>
                ) : (
                  <p className="line-clamp-1 text-xs text-stone-500 dark:text-neutral-400">
                    {p.description}
                  </p>
                )}
              </div>
              <button
                type="button"
                role="switch"
                aria-checked={on}
                aria-label={`${on ? t('skills.disconnect') : t('skills.connect')} ${p.label}`}
                onClick={() => handleToggle(p.id, p.label, on)}
                className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${
                  on ? 'bg-primary-500' : 'bg-stone-200'
                }`}>
                <span
                  className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${
                    on ? 'translate-x-5' : 'translate-x-0.5'
                  }`}
                />
              </button>
            </div>
          );
        })}
      </div>
      <p className="text-xs text-stone-400 dark:text-neutral-500">
        {t('app.openhumanLink.accounts.webviewNote')}
      </p>
      <DoneFooter close={close} onDone={handleDone} doneLabel={doneLabel} />
    </div>
  );
};

// ── Shared footer ────────────────────────────────────────────────────────

const DoneFooter = ({
  close,
  onDone,
  doneLabel,
  skipLabel,
}: {
  close: () => void;
  onDone?: () => void;
  doneLabel?: string;
  skipLabel?: string;
}) => {
  const { t } = useT();
  const resolvedDone = doneLabel ?? t('app.openhumanLink.done');
  const resolvedSkip = skipLabel ?? t('app.openhumanLink.skipForNow');
  return (
    <div className="flex items-center justify-between gap-3 pt-1">
      <button
        type="button"
        onClick={close}
        className="text-xs font-medium text-stone-500 dark:text-neutral-400 hover:text-stone-800 dark:hover:text-neutral-100">
        {resolvedSkip}
      </button>
      <button
        type="button"
        onClick={onDone ?? close}
        className="rounded-lg border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-3 py-1.5 text-xs font-medium text-stone-700 dark:text-neutral-200 hover:bg-stone-50 dark:hover:bg-neutral-800/60">
        {resolvedDone}
      </button>
    </div>
  );
};

export default OpenhumanLinkModal;
